cling/
error.rs

1use std::fmt::{self, Display, Formatter};
2use std::io::Write;
3
4use clap::CommandFactory;
5use itertools::{Itertools, Position};
6use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
7
8use crate::prelude::ClingFinished;
9use crate::Run;
10
11pub trait CliErrorHandler {
12    type Output;
13    fn unwrap_or_exit(self) -> Self::Output;
14    fn then_exit(self) -> !;
15}
16
17/// An error type for the CLI application.
18///
19/// This error type handles exit codes, pretty printing of error messages, and
20/// include some handy utilities.
21pub enum CliError {
22    InvalidHandler(String),
23    Failed,
24    FailedWithMessage(String),
25    FailedWithMessageAndCode(String, u8),
26    ClapError(clap::Error),
27    InputString,
28    Other(anyhow::Error),
29    OtherWithCode(anyhow::Error, u8),
30}
31
32impl std::error::Error for CliError {}
33
34/// A helper to allow anyhow users from returning anyhow::Error errors in cling
35/// handlers without writing an [[`Into<CliError>`]] implementation for every
36/// error type.
37impl From<anyhow::Error> for CliError {
38    fn from(value: anyhow::Error) -> Self {
39        CliError::Other(value)
40    }
41}
42
43impl From<std::io::Error> for CliError {
44    fn from(value: std::io::Error) -> Self {
45        CliError::Other(value.into())
46    }
47}
48
49impl From<clap::Error> for CliError {
50    fn from(value: clap::Error) -> Self {
51        CliError::ClapError(value)
52    }
53}
54
55impl<T, E> CliErrorHandler for Result<T, E>
56where
57    E: Into<CliError>,
58{
59    type Output = T;
60
61    /// Returns the result if it is `Ok`, otherwise exit the program with the
62    /// appropriate exit code after printing the error.
63    fn unwrap_or_exit(self) -> T {
64        match self {
65            | Ok(x) => x,
66            | Err(e) => {
67                let e = e.into();
68                e.print().unwrap();
69                e.exit()
70            }
71        }
72    }
73
74    /// Exit the program with appropriate exit code. This will also print the
75    /// error if the result is an error.
76    fn then_exit(self) -> ! {
77        match self {
78            | Ok(_) => std::process::exit(0),
79            | Err(e) => {
80                let e = e.into();
81                e.print().unwrap();
82                e.exit()
83            }
84        }
85    }
86}
87
88impl Display for CliError {
89    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
90        match self {
91            | CliError::ClapError(e) => {
92                // Clap handles colors
93                write!(f, "{}", e)
94            }
95            | CliError::Failed => {
96                write!(f, "Failed!")
97            }
98            | CliError::FailedWithMessage(e) => {
99                write!(f, "Failed: {}", e)
100            }
101            | CliError::FailedWithMessageAndCode(e, _) => {
102                write!(f, "Error: {}", e)
103            }
104            | CliError::Other(e) => {
105                write!(f, "Error: {:#}", e)
106            }
107            | CliError::OtherWithCode(e, _) => {
108                write!(f, "Error: {:#}", e)
109            }
110            | CliError::InputString => {
111                write!(f, "Input string cannot be parsed as UNIX shell command")
112            }
113            #[allow(unused_variables)]
114            | CliError::InvalidHandler(msg) => {
115                #[cfg(not(debug_assertions))]
116                let r = write!(
117                    f,
118                    "\n\n** Cling Handler Design Error **\n\n{}",
119                    "Detailed error message available only in debug builds.",
120                );
121                #[cfg(debug_assertions)]
122                let r = write!(
123                    f,
124                    "\n\n** Cling Handler Design Error **\n\n{}",
125                    msg
126                );
127                r
128            }
129        }
130    }
131}
132
133impl std::fmt::Debug for CliError {
134    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
135        write!(f, "{}", self)
136    }
137}
138
139impl CliError {
140    /// Pretty print the error to stderr.
141    pub fn print(&self) -> std::io::Result<()> {
142        let mut stderr = StandardStream::stderr(ColorChoice::Auto);
143        match self {
144            | CliError::ClapError(e) => {
145                // Clap handles colors
146                e.print()
147            }
148            | CliError::Failed => {
149                print_formatted_error(&mut stderr, "Aborted!", "")
150            }
151            | CliError::FailedWithMessage(e) => {
152                print_formatted_error(&mut stderr, "", e)
153            }
154            | CliError::FailedWithMessageAndCode(e, _) => {
155                print_formatted_error(&mut stderr, "", e)
156            }
157            | CliError::Other(e) => {
158                print_anyhow_error(&mut stderr, "Error: ", e)
159            }
160            | CliError::OtherWithCode(e, _) => {
161                print_anyhow_error(&mut stderr, "Error: ", e)
162            }
163            | e @ CliError::InputString => {
164                print_formatted_error(&mut stderr, "", &e.to_string())
165            }
166            #[allow(unused_variables)]
167            | CliError::InvalidHandler(msg) => {
168                #[cfg(not(debug_assertions))]
169                let r = print_formatted_error(
170                    &mut stderr,
171                    "\n\n** Cling Handler Design Error **\n\n",
172                    "Detailed error message available only in debug builds.",
173                );
174                #[cfg(debug_assertions)]
175                let r = print_formatted_error(
176                    &mut stderr,
177                    "\n\n** Cling Handler Design Error **\n\n",
178                    msg,
179                );
180                r
181            }
182        }
183    }
184
185    /// What is the exit code for this error?
186    pub fn exit_code(&self) -> u8 {
187        match self {
188            | CliError::FailedWithMessageAndCode(_, code) => *code,
189            | CliError::OtherWithCode(_, code) => *code,
190            // Clap uses i32 for exit codes, we cast to u8 but fail with 255 if
191            // out of bound.
192            | CliError::ClapError(e) => {
193                let code = e.exit_code();
194                code.try_into().unwrap_or(255)
195            }
196            | _ => 1,
197        }
198    }
199
200    /// Terminate the program with this error's exit code.
201    pub fn exit(self) -> ! {
202        std::process::exit(self.exit_code() as i32)
203    }
204
205    pub fn into_finished<T: Run + clap::Parser>(self) -> ClingFinished<T> {
206        Into::into(self)
207    }
208}
209
210static_assertions::assert_impl_all!(CliError: Send, Sync);
211
212fn print_formatted_error(
213    f: &mut StandardStream,
214    heading: &str,
215    msg: &str,
216) -> std::io::Result<()> {
217    f.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?;
218    write!(f, "{}", heading)?;
219    f.reset()?;
220    writeln!(f, "{}", msg)?;
221    Ok(())
222}
223
224fn print_anyhow_error(
225    f: &mut StandardStream,
226    heading: &str,
227    err: &anyhow::Error,
228) -> std::io::Result<()> {
229    f.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?;
230    write!(f, "{}", heading)?;
231    f.reset()?;
232    writeln!(f, "{}", err)?;
233    err.chain()
234        .skip(1)
235        .with_position()
236        .for_each(|(position, cause)| {
237            if position == Position::First {
238                let _ =
239                    f.set_color(ColorSpec::new().set_fg(Some(Color::Magenta)));
240                let _ = writeln!(f, "");
241                let _ = writeln!(f, "Caused by:");
242                let _ = f.reset();
243            }
244            let symbol = if position == Position::Last {
245                "└─"
246            } else {
247                "├─"
248            };
249            let _ =
250                f.set_color(ColorSpec::new().set_italic(true).set_dimmed(true));
251            let _ = write!(f, "  {} ", symbol);
252            let _ = f.reset();
253            let _ = writeln!(f, "{}", cause);
254        });
255    Ok(())
256}
257
258pub(crate) fn format_clap_error<I: CommandFactory>(
259    err: clap::Error,
260) -> clap::Error {
261    let mut cmd = I::command();
262    err.format(&mut cmd)
263}