fluvio_command/
lib.rs

1//! Helper traits and types to make `std::process::Command` more ergonomic
2
3#![warn(missing_docs)]
4
5use std::process::{Command, Output};
6use tracing::debug;
7
8/// `Ok(Output)` when a child process successfully runs and returns exit code `0`.
9///
10/// All other circumstances are regarded as `Err(CommandError)`. This includes
11/// when there is an error invoking a child process, if a child process is
12/// terminated, or if the child process runs and returns a non-zero exit code.
13pub type CommandResult = Result<Output, CommandError>;
14
15/// An error type describing the kinds of failure a child process may have
16#[derive(thiserror::Error, Debug)]
17#[error("Failed to run \"{command}\"")]
18pub struct CommandError {
19    /// The command that was attempting to run
20    pub command: String,
21    /// The kind of error that the command encountered
22    pub source: CommandErrorKind,
23}
24
25/// Describes the particular kinds of errors that may occur while running a command
26#[derive(thiserror::Error, Debug)]
27pub enum CommandErrorKind {
28    /// The child process was terminated, so did not exit successfully
29    #[error("Child process was terminated and has no exit code")]
30    Terminated,
31    /// The child process completed with a non-zero exit code
32    #[error("\
33Child process completed with non-zero exit code {0}
34  stdout: {}
35  stderr: {}",
36        String::from_utf8_lossy(&.1.stdout).to_string(),
37        String::from_utf8_lossy(&.1.stderr).to_string())]
38    ExitError(i32, Output),
39    /// There was an error invoking the command
40    #[error("An error occurred while invoking child process")]
41    IoError(#[from] std::io::Error),
42}
43
44/// Adds useful extension methods to the `Command` type
45pub trait CommandExt {
46    /// Inherit both `stdout` and `stderr` from this process.
47    ///
48    /// # Example
49    ///
50    /// ```
51    /// use std::process::Command;
52    /// use fluvio_command::CommandExt;
53    /// let _ = Command::new("echo")
54    ///     .arg("Hello world")
55    ///     .inherit()
56    ///     .status();
57    /// ```
58    fn inherit(&mut self) -> &mut Self;
59    /// Print a stringified version of the Command to the debug log.
60    ///
61    /// # Example
62    ///
63    /// ```
64    /// use std::process::Command;
65    /// use fluvio_command::CommandExt;
66    /// let _ = Command::new("echo")
67    ///     .arg("How are you Fluvio")
68    ///     .log()
69    ///     .spawn();
70    /// ```
71    fn log(&mut self) -> &mut Self;
72    /// Print a stringified version of the Command to stdout.
73    ///
74    /// # Example
75    ///
76    /// ```
77    /// use std::process::Command;
78    /// use fluvio_command::CommandExt;
79    /// let _ = Command::new("echo")
80    ///     .arg("How are you Fluvio")
81    ///     .print()
82    ///     .spawn();
83    /// ```
84    fn print(&mut self) -> &mut Self;
85    /// Return a stringified version of the Command.
86    ///
87    /// # Example
88    ///
89    /// ```
90    /// use std::process::Command;
91    /// use fluvio_command::CommandExt;
92    /// let mut command = Command::new("echo");
93    /// command.arg("one").arg("two three");
94    /// let command_string: String = command.display();
95    /// assert_eq!(command_string, "echo one two three");
96    /// ```
97    fn display(&self) -> String;
98    /// Returns a result signaling the outcome of executing this command.
99    ///
100    /// # Example
101    ///
102    /// ```
103    /// use std::process::{Command, Output};
104    /// use fluvio_command::{CommandExt, CommandErrorKind};
105    ///
106    /// // On success, we get the stdout and stderr output
107    /// let output: Output = Command::new("true").result().unwrap();
108    ///
109    /// let error = Command::new("bash")
110    ///     .args(&["-c", r#"echo "this command failed with this stderr" 1>&2 && false"#])
111    ///     .result()
112    ///     .unwrap_err();
113    /// if let CommandErrorKind::ExitError(1i32, output) = error.source {
114    ///     assert_eq!(
115    ///         String::from_utf8_lossy(&output.stderr).to_string(),
116    ///         "this command failed with this stderr\n",
117    ///     );
118    /// } else {
119    ///     panic!("should fail with stderr output");
120    /// }
121    ///
122    /// let error = Command::new("foobar").result().unwrap_err();
123    /// assert!(matches!(error.source, CommandErrorKind::IoError(_)));
124    /// assert_eq!(error.command, "foobar");
125    /// ```
126    fn result(&mut self) -> CommandResult;
127}
128
129impl CommandExt for Command {
130    fn inherit(&mut self) -> &mut Self {
131        use std::process::Stdio;
132        self.stdout(Stdio::inherit()).stderr(Stdio::inherit())
133    }
134
135    fn log(&mut self) -> &mut Self {
136        debug!("Command> {:?}", self.display());
137        self
138    }
139
140    fn print(&mut self) -> &mut Self {
141        println!("Command> {:?}", self.display());
142        self
143    }
144
145    fn display(&self) -> String {
146        format!("{:?}", self).replace("\"", "")
147    }
148
149    fn result(&mut self) -> CommandResult {
150        debug!("Executing> {}", self.display());
151
152        self.output()
153            .map_err(|e| CommandError {
154                command: self.display(),
155                source: CommandErrorKind::IoError(e),
156            })
157            .and_then(|output| match output.status.code() {
158                Some(0i32) => Ok(output),
159                None => Err(CommandError {
160                    command: self.display(),
161                    source: CommandErrorKind::Terminated,
162                }),
163                Some(code) => Err(CommandError {
164                    command: self.display(),
165                    source: CommandErrorKind::ExitError(code, output),
166                }),
167            })
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_output_display() {
177        let error = Command::new("ls")
178            .arg("does-not-exist")
179            .result()
180            .unwrap_err();
181        let error_display = format!("{}", error.source);
182        assert!(error_display.starts_with("Child process completed with non-zero exit code"));
183        assert!(error_display.contains("stdout:"));
184        assert!(error_display.contains("stderr:"));
185    }
186}