clawless_core/exit.rs
1//! The way in which a command ends the process
2//!
3//! A command that returns `()` ends the process with the exit code 0, and a command that returns
4//! an error ends it with the exit code 1. A command that needs a different ending returns an
5//! [`Exit`], which carries the exit code and an optional text that the command rendered itself.
6//!
7//! An [`Exit`] is a plain value. The runner of an application returns it, and the `main`
8//! function of the application ends the process with it.
9
10use std::fmt::Debug;
11use std::process::ExitCode;
12
13use bon::Builder;
14use getset::CopyGetters;
15
16/// The exit code and the text with which a command ends the process
17///
18/// A wrapper, such as a script or a CI job, reads the exit code of a program to decide what to do
19/// next. A command therefore needs to choose the code. The chosen code is not always a failure: a
20/// check that finds a problem ends as planned, and it can still exit with a code other than 0 to
21/// tell the wrapper what it found.
22///
23/// The text is what the command wants the user to read last, such as a diagnostic that the
24/// application rendered itself. Clawless does not render the text. It writes the text to the
25/// standard error once, exactly as it is, and adds no prefix and no line break. End the text with
26/// a line break unless the text must not have one.
27///
28/// Clawless writes the text after it has rendered all the output that the command sent, and the
29/// process exits after the text. The text therefore appears last on the standard error. The
30/// standard output carries only the result of the command, so JSON output stays valid. The
31/// verbosity does not apply to the text, so `--quiet` does not hide it.
32///
33/// An error that a command returns always ends the process with the exit code 1. A command that
34/// needs another code for a failure turns the error into an `Exit` itself, and returns the `Exit`
35/// in `Ok`.
36///
37/// Clawless itself uses these exit codes. A command can still use them, because Clawless reserves
38/// none of them.
39///
40/// | Code | Meaning |
41/// | ---- | -------------------------------------------------------------------------- |
42/// | 0 | The command returned `()` |
43/// | 1 | The command returned an error |
44/// | 2 | The command line has an error, which the argument parser reports |
45/// | 101 | The command or Clawless panicked, for example on a closed standard output |
46/// | 130 | The user pressed Ctrl+C for the second time |
47///
48/// # Examples
49///
50/// ```
51/// use std::process::ExitCode;
52///
53/// use clawless_core::exit::Exit;
54///
55/// let exit = Exit::builder()
56/// .code(ExitCode::from(3))
57/// .text("error: 0.7.0 is not a stable release\n")
58/// .build();
59///
60/// assert_eq!(exit.code(), ExitCode::from(3));
61/// assert_eq!(exit.text(), Some("error: 0.7.0 is not a stable release\n"));
62/// ```
63///
64/// A command that only chooses the code converts an [`ExitCode`] into an `Exit` without text:
65///
66/// ```
67/// use std::process::ExitCode;
68///
69/// use clawless_core::exit::Exit;
70///
71/// let exit = Exit::from(ExitCode::from(3));
72///
73/// assert_eq!(exit.text(), None);
74/// ```
75///
76/// [`ExitCode`]: std::process::ExitCode
77#[derive(Clone, PartialEq, Debug, Builder, CopyGetters)]
78#[must_use = "an `Exit` ends the process only when `main` returns it"]
79pub struct Exit {
80 /// The exit code of the process
81 #[builder(into)]
82 #[getset(get_copy = "pub")]
83 code: ExitCode,
84
85 /// The text that the process writes to the standard error before it exits
86 #[builder(into)]
87 text: Option<String>,
88}
89
90impl Exit {
91 /// Returns the exit of a command that failed with the given error
92 ///
93 /// The exit code is 1. The text is `Error: ` and the debug representation of the error, the
94 /// same as when the `main` function of a Rust program returns the error.
95 ///
96 /// # Examples
97 ///
98 /// ```
99 /// use std::process::ExitCode;
100 ///
101 /// use clawless_core::exit::Exit;
102 ///
103 /// #[derive(Debug)]
104 /// struct MissingFile;
105 ///
106 /// let exit = Exit::from_error(&MissingFile);
107 ///
108 /// assert_eq!(exit.code(), ExitCode::FAILURE);
109 /// assert_eq!(exit.text(), Some("Error: MissingFile\n"));
110 /// ```
111 pub fn from_error(error: &impl Debug) -> Self {
112 Self {
113 code: ExitCode::FAILURE,
114 text: Some(format!("Error: {error:?}\n")),
115 }
116 }
117
118 /// Returns the text that the process writes to the standard error before it exits
119 ///
120 /// # Examples
121 ///
122 /// ```
123 /// use std::process::ExitCode;
124 ///
125 /// use clawless_core::exit::Exit;
126 ///
127 /// let exit = Exit::builder()
128 /// .code(ExitCode::from(3))
129 /// .text("error: the check failed\n")
130 /// .build();
131 ///
132 /// assert_eq!(exit.text(), Some("error: the check failed\n"));
133 /// ```
134 pub fn text(&self) -> Option<&str> {
135 self.text.as_deref()
136 }
137}
138
139/// Returns the exit of a command that ended without a choice of its own
140///
141/// The exit code is 0, and the process writes no text.
142impl From<()> for Exit {
143 fn from((): ()) -> Self {
144 Self {
145 code: ExitCode::SUCCESS,
146 text: None,
147 }
148 }
149}
150
151/// Returns the exit with the given code and without text
152impl From<ExitCode> for Exit {
153 fn from(code: ExitCode) -> Self {
154 Self { code, text: None }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 // An assertion in a test panics by design. A `# Panics` section on every test
161 // would repeat that and give the reader no information.
162 #![allow(clippy::missing_panics_doc)]
163
164 use super::*;
165
166 /// An error whose debug representation is known
167 #[derive(Debug)]
168 struct MissingFile;
169
170 #[test]
171 fn builder_without_text_has_no_text() {
172 let exit = Exit::builder().code(ExitCode::from(3)).build();
173
174 assert_eq!(exit.text(), None);
175 }
176
177 #[test]
178 fn from_error_returns_the_debug_representation_with_a_prefix() {
179 let error = MissingFile;
180
181 let exit = Exit::from_error(&error);
182
183 assert_eq!(exit.text(), Some("Error: MissingFile\n"));
184 }
185
186 #[test]
187 fn from_error_returns_the_failure_code() {
188 let exit = Exit::from_error(&"broken");
189
190 assert_eq!(exit.code(), ExitCode::FAILURE);
191 }
192
193 #[test]
194 fn from_exit_code_returns_the_code_without_text() {
195 let exit = Exit::from(ExitCode::from(3));
196
197 assert_eq!(exit, Exit::builder().code(ExitCode::from(3)).build());
198 }
199
200 #[test]
201 fn from_unit_returns_success_without_text() {
202 let exit = Exit::from(());
203
204 assert_eq!(exit, Exit::builder().code(ExitCode::SUCCESS).build());
205 }
206
207 #[test]
208 fn trait_send() {
209 fn assert_send<T: Send>() {}
210 assert_send::<Exit>();
211 }
212
213 #[test]
214 fn trait_sync() {
215 fn assert_sync<T: Sync>() {}
216 assert_sync::<Exit>();
217 }
218
219 #[test]
220 fn trait_unpin() {
221 fn assert_unpin<T: Unpin>() {}
222 assert_unpin::<Exit>();
223 }
224}