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}