command_error/
output_error.rs

1use std::fmt::Debug;
2use std::fmt::Display;
3
4use crate::CommandDisplay;
5use crate::DebugDisplay;
6use crate::OutputLike;
7
8#[cfg(doc)]
9use crate::CommandExt;
10#[cfg(doc)]
11use crate::ExecError;
12#[cfg(feature = "miette")]
13use miette::Diagnostic;
14
15/// An error from a failed command, typically due to a non-zero exit status.
16///
17/// Produced by [`CommandExt`]. This indicates a command that failed, typically with a non-zero
18/// exit code, rather than a command that failed to start (as in [`ExecError`]).
19///
20/// ```
21/// # use pretty_assertions::assert_eq;
22/// # use indoc::indoc;
23/// # use std::process::Command;
24/// # use std::process::Output;
25/// # use std::process::ExitStatus;
26/// # use command_error::Utf8ProgramAndArgs;
27/// # use command_error::CommandDisplay;
28/// # use command_error::OutputError;
29/// let mut command = Command::new("sh");
30/// command.args(["-c", "echo puppy doggy"]);
31/// let displayed: Utf8ProgramAndArgs = (&command).into();
32/// let error = OutputError::new(
33///     Box::new(displayed),
34///     Box::new(Output {
35///         status: ExitStatus::default(),
36///         stdout: "puppy doggy\n".as_bytes().to_vec(),
37///         stderr: Vec::new(),
38///     })
39/// );
40/// assert_eq!(
41///     error.to_string(),
42///     indoc!(
43///         "`sh` failed: exit status: 0
44///         Command failed: `sh -c 'echo puppy doggy'`
45///         Stdout:
46///           puppy doggy"
47///     ),
48/// );
49/// assert_eq!(
50///     error.with_message(Box::new("no kitties found!")).to_string(),
51///     indoc!(
52///         "`sh` failed: no kitties found!
53///         exit status: 0
54///         Command failed: `sh -c 'echo puppy doggy'`
55///         Stdout:
56///           puppy doggy"
57///     )
58/// );
59/// ```
60pub struct OutputError {
61    /// The program and arguments that ran.
62    pub(crate) command: Box<dyn CommandDisplay + Send + Sync>,
63    /// The program's output and exit code.
64    pub(crate) output: Box<dyn OutputLike + Send + Sync>,
65    /// A user-defined error message.
66    pub(crate) user_error: Option<Box<dyn DebugDisplay + Send + Sync>>,
67}
68
69impl OutputError {
70    /// Construct a new [`OutputError`].
71    pub fn new(
72        command: Box<dyn CommandDisplay + Send + Sync>,
73        output: Box<dyn OutputLike + Send + Sync>,
74    ) -> Self {
75        Self {
76            command,
77            output,
78            user_error: None,
79        }
80    }
81
82    /// Attach a user-defined message to this error.
83    pub fn with_message(mut self, message: Box<dyn DebugDisplay + Send + Sync>) -> Self {
84        self.user_error = Some(message);
85        self
86    }
87
88    /// Remove a user-defined message from this error, if any.
89    pub fn without_message(mut self) -> Self {
90        self.user_error = None;
91        self
92    }
93}
94
95impl Debug for OutputError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.debug_struct("OutputError")
98            .field("program", &self.command.program())
99            .field("status", &self.output.status())
100            .field("stdout_utf8", &self.output.stdout())
101            .field("stderr_utf8", &self.output.stderr())
102            .field("user_error", &self.user_error)
103            .finish()
104    }
105}
106
107impl Display for OutputError {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        write!(f, "`{}` failed: ", self.command.program_quoted())?;
110
111        match &self.user_error {
112            Some(user_error) => {
113                // `nix` failed: output didn't contain a valid store path
114                // exit status 0
115                write!(f, "{user_error}\n{}", self.output.status())?;
116            }
117            None => {
118                // `nix` failed: exit status: 1
119                write!(f, "{}", self.output.status())?;
120            }
121        }
122
123        // Command failed: `nix build .#default`
124        write!(f, "\nCommand failed: `{}`", self.command,)?;
125
126        const INDENT: &str = "  ";
127
128        let stdout = self.output.stdout();
129        let stdout = stdout.trim();
130        if !stdout.is_empty() {
131            writeln!(f, "\nStdout:")?;
132            write_indented(f, stdout, INDENT)?;
133        }
134
135        // Stdout:
136        //   ...
137        // Stderr:
138        //   ...
139        //   ...
140        let stderr = self.output.stderr();
141        let stderr = stderr.trim();
142        if !stderr.is_empty() {
143            writeln!(f, "\nStderr:")?;
144            write_indented(f, stderr, INDENT)?;
145        }
146        Ok(())
147    }
148}
149
150impl std::error::Error for OutputError {}
151
152#[cfg(feature = "miette")]
153impl Diagnostic for OutputError {}
154
155fn write_indented(f: &mut std::fmt::Formatter<'_>, text: &str, indent: &str) -> std::fmt::Result {
156    let mut lines = text.lines();
157    if let Some(line) = lines.next() {
158        write!(f, "{indent}{line}")?;
159        for line in lines {
160            write!(f, "\n{indent}{line}")?;
161        }
162    }
163    Ok(())
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use static_assertions::assert_impl_all;
170
171    assert_impl_all!(OutputError: Send, Sync);
172}