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}