-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexit.go
More file actions
161 lines (141 loc) · 5.78 KB
/
exit.go
File metadata and controls
161 lines (141 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package errors
import (
stderrors "errors"
"os"
)
// This file is the single process-exit funnel for the fleet (§3.5). It maps any
// error deterministically to a process exit code via the [exitCodeCarrier]
// behaviour interface (Law 5), and [Exit] is the *only* place this package — and
// by convention the fleet — calls [os.Exit]. Every CLI ends:
//
// func main() { errs.Exit(run()) }
//
// cli-go / borealis.Execute map ErrHelp (clean early exit, code 0) and usage
// errors (EX_USAGE) into these codes via [WithExitCode]; they do not own a
// second 0/1/2 vocabulary.
// The exit-code vocabulary is the BSD sysexits.h set, the de-facto convention
// for well-behaved CLIs (curl, postfix, urfave/cli's ExitCoder all speak it).
// Re-expressed in-package (Law 6: <300 LOC well-understood constants, no
// vendoring) so the spelling stays uniform fleet-wide.
const (
// ExitOK is a successful, clean exit. It is also the code ErrHelp maps to:
// printing help on request is a success, not a failure.
ExitOK = 0
// ExitError is the generic failure code for an error that carries no more
// specific mapping — the default [ExitCodeOf] returns for a SeverityError.
ExitError = 1
// ExitUsage (EX_USAGE) signals the command was used incorrectly: bad flags,
// wrong argument count, malformed syntax. cli-go maps usage errors here.
ExitUsage = 64
// ExitDataErr (EX_DATAERR) signals the input data was incorrect.
ExitDataErr = 65
// ExitNoInput (EX_NOINPUT) signals an input file did not exist or was
// unreadable.
ExitNoInput = 66
// ExitNoUser (EX_NOUSER) signals a specified user did not exist.
ExitNoUser = 67
// ExitNoHost (EX_NOHOST) signals a specified host did not exist.
ExitNoHost = 68
// ExitUnavailable (EX_UNAVAILABLE) signals a needed service is unavailable —
// a catch-all for "a required dependency failed for reasons unrelated to the
// invocation".
ExitUnavailable = 69
// ExitSoftware (EX_SOFTWARE) signals an internal software error (an invariant
// the program itself violated).
ExitSoftware = 70
// ExitNoPerm (EX_NOPERM) signals insufficient permission to perform the
// operation (distinct from a protocol-level auth failure).
ExitNoPerm = 77
// ExitConfig (EX_CONFIG) signals a configuration error.
ExitConfig = 78
// ExitTempFail (EX_TEMPFAIL) signals a temporary failure: the operation may
// succeed if retried later. It is the natural mapping for an error that
// [IsTemporary] classifies as transient.
ExitTempFail = 75
)
// exitCodeCarrier is any error that maps itself to a process exit code. *[Error]
// (when [WithExitCode] was used) and the [joinError] aggregate implement it; an
// external error type opts into [ExitCodeOf] by exposing the same method without
// importing this package (Law 5).
type exitCodeCarrier interface {
error
// ExitCode returns the process exit code this error maps to.
ExitCode() int
}
// ExitCode reports this single layer's explicitly-set exit code. It is only
// meaningful when [WithExitCode] was used; [ExitCodeOf] checks presence via the
// hasExit flag before trusting it, so an *Error without WithExitCode does not
// claim exit code 0.
func (e *Error) ExitCode() int { return e.exitCode }
// hasExitCode reports whether this *Error explicitly set an exit code.
func (e *Error) hasExitCode() bool { return e.hasExit }
// ExitCodeOf reduces err to a deterministic process exit code (§3.5). The
// reduction, in order:
//
// 1. nil err → [ExitOK] (0).
// 2. An explicit [WithExitCode] anywhere in the chain (outermost wins) — this
// is how ErrHelp maps to 0 and usage errors map to [ExitUsage].
// 3. An external [exitCodeCarrier] (behaviour-over-type, Law 5).
// 4. A transient error ([IsTemporary]) → [ExitTempFail] (75).
// 5. Otherwise the severity default: [SeverityNotice]/[SeverityWarning] →
// [ExitOK] (a notice/warning is not a process failure), [SeverityError] →
// [ExitError] (1).
//
// The severity default means even a plain stdlib error has a well-defined exit
// code (1) without any annotation.
func ExitCodeOf(err error) int {
if err == nil {
return ExitOK
}
// An aggregate is consulted by its precomputed value, NOT by fanning out
// into members: errors.As on a []error multi-error would match the first
// member that happens to carry a code, ignoring the most-severe reduction
// Join already computed. So check a directly-held *joinError first.
if j, ok := err.(*joinError); ok {
if j.hasExitJoin() {
return j.exitCode
}
goto fallback
}
// Outermost explicit mapping wins, including a deliberate 0.
for cur := err; cur != nil; {
var ours *Error
if stderrors.As(cur, &ours) && ours.hasExitCode() {
return ours.ExitCode()
}
// External carrier that is not a non-opted-in *Error.
var c exitCodeCarrier
if stderrors.As(cur, &c) {
if oe, ok := c.(*Error); ok && !oe.hasExitCode() {
cur = stderrors.Unwrap(oe)
continue
}
return c.ExitCode()
}
break
}
fallback:
if IsTemporary(err) {
return ExitTempFail
}
switch SeverityOf(err) {
case SeverityNotice, SeverityWarning:
return ExitOK
default:
return ExitError
}
}
// exitFunc is the process-exit primitive, indirected so tests can observe the
// resolved code without terminating the test binary. Production code never
// reassigns it; it is the sole reference to [os.Exit] in the fleet.
var exitFunc = os.Exit
// Exit is the single process-exit funnel: it reduces err via [ExitCodeOf] and
// terminates the process with that code. It is the *only* owner of [os.Exit] —
// no other package (cli-go, borealis, a service main) calls os.Exit directly.
// A clean run ends:
//
// func main() { errs.Exit(run()) }
//
// Exit does NOT print err; rendering is the caller's job (via borealis), so the
// funnel stays output-policy-free. A nil err exits 0.
func Exit(err error) { exitFunc(ExitCodeOf(err)) }