Skip to content

Instantly share code, notes, and snippets.

@mcandre
Last active April 12, 2025 22:29
Show Gist options
  • Save mcandre/accf4897b7e56ae28cddec15b306b220 to your computer and use it in GitHub Desktop.
Save mcandre/accf4897b7e56ae28cddec15b306b220 to your computer and use it in GitHub Desktop.
SemExit

SemExit: Semantic Exit Codes for Command Line Interface Processes

MOTIVATION

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.

INTRODUCTION

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.

CATEGORIES

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 Successful
  • 0x01 ... 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.

Successful

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.

Error

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.

SEE ALSO

@mcandre
Copy link
Author

mcandre commented Apr 12, 2025

I don't think I'm willing to die on a hill to break compatibility with (UNIX) tradition and start reserving any nonzero values for successful program states.

But some kinds of error sub-categories could reasonably migrate to a standardized scheme.

Quick mockup:

  • 0x00: Successful
  • 0x01: General error. This simple entry supports both legacy application development and rapid application development.
  • 0x02 ... 0x0f: Reserved
  • 0x10: Default warnings. Every warning message must carry application configuration parameter(s) to move the warning to /dev/null and emit 0x00 XOR stdout/stderr and emit 0x60 XOR stderr and emit 0x01.
  • 0x11: Retryable error. The operation should document whether it is idempotent or not. For trivial retryable operations, applications should go ahead and implement (exponential backoff and) retry logic on behalf of the user. Computers exist to automate, not create busywork.
  • 0x12 ... 0x3f: Reserved
  • 0x40: Validation error.
  • 0x41: Authentication error.
  • 0x42: Reserved
  • 0x43: Authorization error.
  • 0x44: Resource not found.
  • 0x45 ... 0x4f: Reserved
  • 0x50: Internal error. As much of the relevant, non-sensitive details should emit to stderr, a log file, and/or telemetry as possible for troubleshooting.
  • 0x51, 0x52: Reserved
  • 0x53: Yeah, it's DNS.
  • 0x54 ... 0x5f: Reserved
  • 0x60: Warning registered as opt-in error.
  • 0x61 ... 0x6f: Reserved
  • 0x70 ... 0x8f: Application specific.
  • 0x90 ... 0xee: Reserved. Planned for many things to do with networking and/or remote resource problems.
  • 0xef: Arithmetic error.
  • 0xf0: Null pointer dereference error.
  • 0xfa: File error. For example, existence where there should be absence, read/write I/O problems.
  • 0xfb: Core metric capacity error (insufficient CPU, GPU, RAM, disk, network I/O, etc.)
  • 0xfc: Wrapped child process error. When not simply forwarding the original child exit status identically up the process chain. When wrapping and altering the exit status, then the original child exit status should be stated in a message to stderr, a log file, and/or telemetry for ease of troubleshooting.
  • 0xfd: Unqueriable exit status. A child application failed to register an exit status, or the operating system was unable to launch the child process.
  • 0xfe: Hardware error
  • 0xff: Mask error. This value often occurs as a bug in applications that neglect to apply masks when querying child process exit codes.

Some categories like the early 0x4's here are mimicking the original HTTP status code scheme. However, that may lead to accidents since they're not truly one to one. And terminal applications may prove to have different category needs, creating hotspots and barren wastelands in the scheme.

In this mockup we have space for application specific codes (phrased to open the door for application specific successful codes), with the unassigned ranges explicitly reserved for future use.

If SemExit gains significant adoption, then we could one day use some reserved sections to indicate special success termination states, starting with an equivalent to HTTP 201 when a console program emits nothing to stdout but does succeed, one of the few UX quirks with otherwise golden UNIX program philosophy. Such a dedicated success exit status could help shells to trigger stylized prompts accordingly.

Hell, if we find that the RFC9110 captures the same kinds of errors relevant to command line programs well enough, maybe we could come up with a formula to compress a subsequence of HTTP codes into 8 bits!

@mcandre
Copy link
Author

mcandre commented Apr 12, 2025

@mcandre
Copy link
Author

mcandre commented Apr 12, 2025

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment