Skip to main content

use_exit_code/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6/// Commonly used exit code primitives.
7pub mod prelude {
8    pub use crate::{
9        CONFIG_ERROR, ExitCode, ExitCodeError, FAILURE, PERMISSION_DENIED, SUCCESS, UNAVAILABLE,
10        USAGE_ERROR,
11    };
12}
13
14/// A portable process exit code primitive.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct ExitCode(u8);
17
18impl ExitCode {
19    /// Creates an exit code from an unsigned byte.
20    #[must_use]
21    pub const fn from_u8(code: u8) -> Self {
22        Self(code)
23    }
24
25    /// Creates an exit code from an `i32` when it fits in `u8`.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`ExitCodeError::OutOfRange`] when `code` is outside `0..=255`.
30    pub fn try_from_i32(code: i32) -> Result<Self, ExitCodeError> {
31        u8::try_from(code)
32            .map(Self)
33            .map_err(|_| ExitCodeError::OutOfRange(code))
34    }
35
36    /// Returns the exit code as `u8`.
37    #[must_use]
38    pub const fn as_u8(self) -> u8 {
39        self.0
40    }
41
42    /// Returns the exit code as `i32`.
43    #[must_use]
44    pub const fn as_i32(self) -> i32 {
45        self.0 as i32
46    }
47
48    /// Returns whether this is a success exit code.
49    #[must_use]
50    pub const fn is_success(self) -> bool {
51        self.0 == 0
52    }
53}
54
55impl TryFrom<i32> for ExitCode {
56    type Error = ExitCodeError;
57
58    fn try_from(value: i32) -> Result<Self, Self::Error> {
59        Self::try_from_i32(value)
60    }
61}
62
63impl From<ExitCode> for u8 {
64    fn from(value: ExitCode) -> Self {
65        value.as_u8()
66    }
67}
68
69impl From<ExitCode> for i32 {
70    fn from(value: ExitCode) -> Self {
71        value.as_i32()
72    }
73}
74
75impl From<ExitCode> for std::process::ExitCode {
76    fn from(value: ExitCode) -> Self {
77        Self::from(value.as_u8())
78    }
79}
80
81impl fmt::Display for ExitCode {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(formatter, "{}", self.0)
84    }
85}
86
87/// Exit code conversion errors.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum ExitCodeError {
90    /// The signed integer did not fit in a portable `u8` exit code.
91    OutOfRange(i32),
92}
93
94impl fmt::Display for ExitCodeError {
95    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self {
97            Self::OutOfRange(code) => write!(formatter, "exit code {code} is outside 0..=255"),
98        }
99    }
100}
101
102impl std::error::Error for ExitCodeError {}
103
104/// Successful completion.
105pub const SUCCESS: ExitCode = ExitCode::from_u8(0);
106
107/// General failure.
108pub const FAILURE: ExitCode = ExitCode::from_u8(1);
109
110/// Command line usage error.
111pub const USAGE_ERROR: ExitCode = ExitCode::from_u8(64);
112
113/// Service or dependency unavailable.
114pub const UNAVAILABLE: ExitCode = ExitCode::from_u8(69);
115
116/// Permission denied.
117pub const PERMISSION_DENIED: ExitCode = ExitCode::from_u8(77);
118
119/// Configuration error.
120pub const CONFIG_ERROR: ExitCode = ExitCode::from_u8(78);
121
122#[cfg(test)]
123mod tests {
124    use super::{CONFIG_ERROR, ExitCode, ExitCodeError, FAILURE, SUCCESS, USAGE_ERROR};
125
126    #[test]
127    fn exposes_common_exit_codes() {
128        assert!(SUCCESS.is_success());
129        assert!(!FAILURE.is_success());
130        assert_eq!(USAGE_ERROR.as_i32(), 64);
131        assert_eq!(CONFIG_ERROR.as_u8(), 78);
132    }
133
134    #[test]
135    fn converts_between_integer_forms() -> Result<(), ExitCodeError> {
136        let code = ExitCode::try_from_i32(77)?;
137
138        assert_eq!(code.as_u8(), 77);
139        assert_eq!(i32::from(code), 77);
140        assert_eq!(u8::from(code), 77);
141        assert_eq!(
142            ExitCode::try_from_i32(256),
143            Err(ExitCodeError::OutOfRange(256))
144        );
145        Ok(())
146    }
147}