Standardize Command Line Interface (CLI) program exit code semantics similar to Web response statuses, towards an ecosystem of more intuitive, scriptable, and CI/CD friendly terminal applications.
Unlike modern REST apps, CLI program behavior has continued to vary in the extreme. Simply checking whether a child process succeeds vs. fails from the parent is often a complicated affair, involving tons of error handling logic specific to that particular child command, and all too often involving flaky stdout/stderr log parsing as well.
UNIX-like applications conventionally signal a command's exit status with zero (0x00
) to indicate a successful operation, and a nonzero value such as one (1
) to indicate errors or other kinds of actionable notices. However, many applications and platforms use alternative exit code standards, or no standards.
We believe lightweight frameworks like SemExit increase interoperability between applications, including CLI, GUI, REST, and beyond. We anticipate that the transparency SemExit brings to applications will tend to increase safety, security, and maintainability.
Like Web standards, SemExit may evolve over time. We hope SemExit will one day be obsolesced by even grander ideas.
The UNIX convention provides a strong foundation with simple, eight bit, unsigned bytes.
We use C style, lowercase hexadecimal notation: 0x
followed by two base sixteen digits. This provides a hint at the bit width in terms of any data types or masks involved. As well, the notation is agnostic of the signedness of API's/ABI's involved in manipulating exit statuses.
0x00
Successful0x01
...0xff
Error
When a program reacts to a child process's exit code, then the logic should be comprehensive, implenting at least one appropriate reaction per possible eight bit unsigned exit status. For simplicity, this is accomplished with a simple zero vs nonzero logic, such as an if
/else
block, or a switch
block with a default
case.
At this time, we do not recommend any particular sub-scheme for finer granularity of program termination detail.
0x01
and 0xff
(depending on accident of bit width) are both essentially reserved for general errors; these cannot be reused to indicate fine grained successful operation characteristics without breaking very many other programs. Although a CLI status notation equivalent to HTTP 201 would be useful, implementing that in any given application is extremely likely to break conventional scripts.
[0x02
... 0xff
] has conflicting meanings that vary from application to application. Certain classes of errors, such as trivially retryable network errors, could conceivably be designated a SemExit range or value. We may propose such ranges. Applications interested in adopting SemExit should treat all values other than 0x00
, 0x01
, and 0xff
as reserved / undefined behafior until then.
Some computer systems may involve larger bit widths, in which case take care to mask higher order bits when querying an application's exit status.
Sometimes a process fails to register an exit status. For example, when the command is not found, or when the host is unable to execute a binary application built for a different platform. These rare but reachable parent program states explain why modern standard libraries tend to model exit status querying as an operation that can itself fail.
Sometimes a process does register a status code, but a parent shell script invoking the process mishandles the staus, triggering all manner of unintended behavior. This happens quite often in shell scripts, a dark art of computing. Even apparently simple shell scripts can trigger subtle bugs. Various safety options are available, depending on the particular shell interpreter used. Even so, we recommend that even small shell scripts be rewritten in safer, general purpose programming languages.
CLI warning messages tend to exhibit poor UX. Warnings that are dire and actionable, neglect to yield error exit codes. Warnings that are minor and/or unactionable, neglect to yield successful exit codes. Applications should provide configuration parameters for users to treat warnings as errors, vs. silence warnings, vs. show warnings but treat them as successful.
If a problem is worth logging, then it is often worth failing the application early with a clear error status code. If it's not worth killing the user's application then it's probably not worth logging.
When forwarding child process exit status codes, then the parent application may apply either wrapping or passthrough logic to the original code. Either way, the application should document how the logic(s) apply.
Showing a help menu triggered by a user request to show the menu, does not constitute an error.
Showing a help menu triggered by a validation error, does constitute an error.
A process that concludes without an actionable error should yield zero (0x00
).
Unactionable errors include, for example, intermittent network outages that an application already accounts for via a successful retry operation. Applications may elect to emit a warning via logs, or better yet, telemetry. But as long as the high level operation indicated by the command succeeds, then no positive or negative status should be indicated.
Some environments provide misleading shims in lieu of genuine programs, emitting advice on how to install the genuine programs. Such shims should yield error status codes, so that automated scripts can be correctly made aware of missing commands.
Many applications neglect to register a successful status code for unactionable errors. If the user would reasonably take no action upon encountering that state, then the exit status should indicate success, not error.
When a linter, SAST, or other scanner application finds no content that could trigger any actionable findings, then that is normally not an actionable error.
Conversely, some linters, SAST, and other scanner applications make the mistake of failing to yield an error exit code when reporting actionable findings, such as reporting security vulnerabilities. The user reasonably would like to not be attacked and so such error statuses should propagate, to the calling process and various other downstream processes.
Many linter applications make the mistake of assuming that their tool is sooo cutting edge that it will not be used in automated scripting, CI/CD, etc. It will. If the application continues to yield meaningless exit status codes (e.g. 100% 0x00
even when findings are present or the application crashes) then users will shift to alternative linters that do provide semantic exit status codes. At a minimum, use an error code to indicate general problems. Users may elect to ignore the exit code, but the information should be made available for them to decide how to react. An application too cool for meaningful exit codes is too erratic for meaningful audiences.
A process that concludes with an actionable error should yield a value in the range [0x01
... 0xff
].
By UNIX convention, the sole error status code one (0x01
) is used to indicate errors generally, at least on relatively UNIX compliant environments, even including Windows.
Actionable errors include, for example:
- When the application validates data and determines that the data is invalid.
- When the application is terminated early.
- When the application experiences core resource capacity issues (e.g. CPU, RAM, disk, network).
- When a requisite local resource is unavailable.
- When the application encounters a numerical problem.
- When the application encounters a problem with a remote resource.
- When the application encounters an unrecoverable problem with a child process.
- When a related hardware problem occurs.
If your application uses a more advanced exit status scheme than "zero good one bad" please do your users one kindness and publish the exact exit status scheme, gift wrapped in a reasonably convenient text file format (e.g. GFM Markdown, TXT, HTML; UTF-8, LF), instead of scrawling the codes across awful documentation formats like word processors, rich text formats, XML, PostScript's such as PDF, niche ebook formats, or other such garbage.
For now, C and FFI derivatives may include sysexits. Windows users must supply some equivalent header. Go has https://pkg.go.dev/github.com/MatthiasPetermann/sysexits and Rust has https://docs.rs/sysexits/latest/sysexits/