semantic_exit/
lib.rs

1//! Defines semantic exit codes which may be used by Command Line tools to aid in debugging and instrumentation.
2//!
3//! This package defines exit codes in two ranges:
4//! - Exit Codes 80-99 indicate a user error of some sort.
5//! - Exit Codes 100-119 indicate software or system error of some sort.
6
7/// The exit code that is passed to the system call `exit` when the program terminates.
8/// Conventionally, the value zero indicates success and all other values (1-255) indicate failure.
9#[derive(Clone, Copy, Debug, PartialEq, num_derive::FromPrimitive, num_derive::ToPrimitive)]
10#[repr(i32)]
11pub enum Code {
12    /// Indicates that the program exited successfully.
13    OK = 0,
14
15    /// Indicates that the program exited unsuccessfully but gives no extra context as to what the failure was.
16    NotOK = 1,
17
18    // Exit Codes 80-99 are reserved for user errors.
19    /// UsageError indicates that the program exited unsuccessfully because it was was used incorrectly.
20    ///
21    /// Examples: a required argument was omitted or an invalid value was supplied for a flag.
22    UsageError = 80,
23
24    /// Indicates that the program exited unsuccessfully because an unrecognized subcommand was invoked.
25    ///
26    /// This is intended for CLI multi-tools.
27    /// When you run a command that doesn't exist from the shell, the shell exits 127.
28    /// This is distinct from that value in that the command itself exists but the subcommand does not (e.g. `git nope` could exit 81).
29    UnknownSubcommand = 81,
30
31    /// Indicates that the program exited unsuccessfully because a precondition wasn't satisfied.
32    ///
33    /// Examples: the user must be on a VPN before using the program or have a minimum version of some other software installed.
34    RequirementNotMet = 82,
35
36    /// Indicates that the program exited unsuccessfully because the user isn't authorized to perform the requested action.
37    Forbidden = 83,
38
39    /// Indicates that the program exited unsuccessfully because it has been migrated to a new location.
40    MovedPermanently = 84,
41
42    // Exit Codes 100-119 are reserved for software or system errors.
43    /// Indicates that the program exited unsuccessfully because of a problem in its own code.
44    ///
45    /// Used instead of 1 when the problem is known to be with the program's code or dependencies.
46    InternalError = 100,
47
48    /// Indicates that the program exited unsuccessfully because a service it depends on was not available.
49    ///
50    /// Examples: A local daemon or remote service did not respond, a connection was closed unexpectedly, an HTTP service responded with 503.
51    Unavailable = 101,
52}
53
54#[derive(Debug, thiserror::Error, PartialEq)]
55pub enum Error {
56    #[error("unknown exit code: {0}")]
57    UnknownExitCode(i32),
58}
59
60/// Reports whether an exit code is a user error.
61/// It returns true if the code is in the range 80-99 and false if not.
62pub fn is_user_error(code: Code) -> bool {
63    (code as i32) >= 80 && (code as i32) <= 99
64}
65
66/// Reports whether an exit code is a software error.
67/// It returns true if the code is in the range 100-119 and false if not.
68pub fn is_software_error(code: Code) -> bool {
69    (code as i32) >= 100 && (code as i32) <= 119
70}
71
72/// Reports whether an exit code is derived from a signal.
73/// It returns true if the code is in the range 128-255 and false if not.
74pub fn is_signal(code: Code) -> bool {
75    (code as i32) > 128 && (code as i32) < 255
76}
77
78/// Returns the exit code that corresponds to when a program exits in response to a signal.
79pub fn from_signal(signal: i32) -> i32 {
80    128 + signal
81}
82
83impl TryFrom<i32> for Code {
84    type Error = Error;
85
86    fn try_from(value: i32) -> Result<Self, Self::Error> {
87        num_traits::FromPrimitive::from_i32(value).ok_or(Error::UnknownExitCode(value))
88    }
89}
90
91impl From<Code> for i32 {
92    fn from(value: Code) -> Self {
93        value as i32
94    }
95}
96
97pub fn exit(code: Code) {
98    std::process::exit(code.into());
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_exit_code_match_readme() {
107        let readme = include_str!("../README.md");
108        let re = regex::Regex::new(r"\| (\d+) \| `(\w+)` \| .* \|\n").unwrap();
109        for captures in re.captures_iter(readme) {
110            let expected_code_num: i32 = captures.get(1).unwrap().as_str().parse().unwrap();
111            let expected_name = captures.get(2).unwrap().as_str();
112            let actual_code: Code = expected_code_num.try_into().expect(&format!(
113                "does not define the exit code {expected_name} ({expected_code_num})"
114            ));
115            let actual_name = format!("{:?}", actual_code);
116
117            assert_eq!(expected_name, actual_name, "maps {actual_name} to {expected_code_num}, README.md expected it to be {expected_name}");
118        }
119    }
120
121    #[test]
122    fn test_unknown_exit_code() {
123        let err = <i32 as TryInto<Code>>::try_into(-1).unwrap_err();
124        assert_eq!(err, Error::UnknownExitCode(-1))
125    }
126
127    #[test_case::test_case(libc::SIGINT, 130)]
128    fn test_from_signal(signal: i32, expected: i32) {
129        assert_eq!(from_signal(signal), expected);
130    }
131}