Skip to main content

command_run/
lib.rs

1#![deny(missing_docs)]
2// This library is not likely to be used in a context where a 144 byte
3// error type is a meaningful performance problem.
4#![allow(clippy::result_large_err)]
5
6//! Utility for running a command in a subprocess.
7//!
8//! The [`Command`] type is a wrapper around the [`std::process::Command`]
9//! type that adds a few convenient features:
10//!
11//! - Print or log the command before running it
12//! - Optionally return an error if the command is not successful
13//! - The command can be formatted as a command-line string
14//! - The [`Command`] type can be cloned and its fields are public
15
16use std::borrow::Cow;
17use std::collections::HashMap;
18use std::ffi::{OsStr, OsString};
19use std::io::Read;
20use std::path::PathBuf;
21use std::{fmt, io, process};
22
23/// Type of error.
24#[derive(Debug)]
25pub enum ErrorKind {
26    /// An error occurred in the calls used to run the command. For
27    /// example, this variant is used if the program does not exist.
28    Run(io::Error),
29
30    /// The command exited non-zero or due to a signal.
31    Exit(process::ExitStatus),
32}
33
34/// Error returned by [`Command::run`].
35#[derive(Debug)]
36pub struct Error {
37    /// The command that caused the error.
38    pub command: Command,
39
40    /// The type of error.
41    pub kind: ErrorKind,
42}
43
44impl Error {
45    /// Check if the error kind is `Run`.
46    pub fn is_run_error(&self) -> bool {
47        matches!(self.kind, ErrorKind::Run(_))
48    }
49
50    /// Check if the error kind is `Exit`.
51    pub fn is_exit_error(&self) -> bool {
52        matches!(self.kind, ErrorKind::Exit(_))
53    }
54}
55
56/// Internal trait for converting an io::Error to an Error.
57trait IntoError<T> {
58    fn into_run_error(self, command: &Command) -> Result<T, Error>;
59}
60
61impl<T> IntoError<T> for Result<T, io::Error> {
62    fn into_run_error(self, command: &Command) -> Result<T, Error> {
63        self.map_err(|err| Error {
64            command: command.clone(),
65            kind: ErrorKind::Run(err),
66        })
67    }
68}
69
70impl fmt::Display for Error {
71    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
72        match &self.kind {
73            ErrorKind::Run(err) => write!(
74                f,
75                "failed to run '{}': {}",
76                self.command.command_line_lossy(),
77                err
78            ),
79            ErrorKind::Exit(err) => write!(
80                f,
81                "command '{}' failed: {}",
82                self.command.command_line_lossy(),
83                err
84            ),
85        }
86    }
87}
88
89impl std::error::Error for Error {}
90
91/// The output of a finished process.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct Output {
94    /// The status (exit code) of the process.
95    pub status: process::ExitStatus,
96
97    /// The data that the process wrote to stdout.
98    pub stdout: Vec<u8>,
99
100    /// The data that the process wrote to stderr.
101    pub stderr: Vec<u8>,
102}
103
104impl Output {
105    /// Get stdout as a string.
106    pub fn stdout_string_lossy(&self) -> Cow<'_, str> {
107        String::from_utf8_lossy(&self.stdout)
108    }
109
110    /// Get stderr as a string.
111    pub fn stderr_string_lossy(&self) -> Cow<'_, str> {
112        String::from_utf8_lossy(&self.stderr)
113    }
114}
115
116impl From<process::Output> for Output {
117    fn from(o: process::Output) -> Output {
118        Output {
119            status: o.status,
120            stdout: o.stdout,
121            stderr: o.stderr,
122        }
123    }
124}
125
126fn combine_output(mut cmd: process::Command) -> Result<Output, io::Error> {
127    let (mut reader, writer) = io::pipe()?;
128    let writer_clone = writer.try_clone()?;
129    cmd.stdout(writer);
130    cmd.stderr(writer_clone);
131
132    let mut handle = cmd.spawn()?;
133
134    drop(cmd);
135
136    let mut output = Vec::new();
137    reader.read_to_end(&mut output)?;
138    let status = handle.wait()?;
139
140    Ok(Output {
141        stdout: output,
142        stderr: Vec::new(),
143        status,
144    })
145}
146
147/// Where log messages go.
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149pub enum LogTo {
150    /// Print to stdout.
151    Stdout,
152
153    /// Use the standard `log` crate.
154    #[cfg(feature = "logging")]
155    Log,
156}
157
158/// A command to run in a subprocess and options for how it is run.
159///
160/// Some notable trait implementations:
161/// - Derives [`Clone`], [`Debug`], [`Eq`], and [`PartialEq`]
162/// - [`Default`] (see docstrings for each field for what the
163///   corresponding default is)
164/// - `From<&Command> for std::process::Command` to convert to a
165///   [`std::process::Command`]
166///
167/// [`Debug`]: std::fmt::Debug
168#[derive(Clone, Debug, Eq, PartialEq)]
169#[must_use]
170pub struct Command {
171    /// Program path.
172    ///
173    /// The path can be just a file name, in which case the `$PATH` is
174    /// searched.
175    pub program: PathBuf,
176
177    /// Arguments passed to the program.
178    pub args: Vec<OsString>,
179
180    /// Directory from which to run the program.
181    ///
182    /// If not set (the default), the current working directory is
183    /// used.
184    pub dir: Option<PathBuf>,
185
186    /// Where log messages go. The default is stdout.
187    pub log_to: LogTo,
188
189    /// If `true` (the default), log the command before running it.
190    pub log_command: bool,
191
192    /// If `true`, log the output if the command exits non-zero or due
193    /// to a signal. This does nothing is `capture` is `false` or if
194    /// `check` is `false`. The default is `false`.
195    pub log_output_on_error: bool,
196
197    /// If `true` (the default), check if the command exited
198    /// successfully and return an error if not.
199    pub check: bool,
200
201    /// If `true`, capture the stdout and stderr of the
202    /// command. The default is `false`.
203    pub capture: bool,
204
205    /// If `true`, send stderr to stdout; the `stderr` field in
206    /// `Output` will be empty. The default is `false.`
207    pub combine_output: bool,
208
209    /// If `false` (the default), inherit environment variables from the
210    /// current process.
211    pub clear_env: bool,
212
213    /// Add or update environment variables in the child process.
214    pub env: HashMap<OsString, OsString>,
215}
216
217impl Command {
218    /// Make a new `Command` with the given program.
219    ///
220    /// All other fields are set to the defaults.
221    pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
222        Self {
223            program: program.as_ref().into(),
224            ..Default::default()
225        }
226    }
227
228    /// Make a new `Command` with the given program and args.
229    ///
230    /// All other fields are set to the defaults.
231    pub fn with_args<I, S1, S2>(program: S1, args: I) -> Self
232    where
233        S1: AsRef<OsStr>,
234        S2: AsRef<OsStr>,
235        I: IntoIterator<Item = S2>,
236    {
237        Self {
238            program: program.as_ref().into(),
239            args: args.into_iter().map(|arg| arg.as_ref().into()).collect(),
240            ..Default::default()
241        }
242    }
243
244    /// Create a `Command` from a whitespace-separated string. If the
245    /// string is empty or all whitespace, `None` is returned.
246    ///
247    /// This function does not do unquoting or escaping.
248    pub fn from_whitespace_separated_str(s: &str) -> Option<Self> {
249        let mut parts = s.split_whitespace();
250        let program = parts.next()?;
251        Some(Self::with_args(program, parts))
252    }
253
254    /// Append a single argument.
255    pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
256        self.args.push(arg.as_ref().into());
257        self
258    }
259
260    /// Append two arguments.
261    ///
262    /// This is equivalent to calling `add_arg` twice; it is for the
263    /// common case where the arguments have different types, e.g. a
264    /// literal string for the first argument and a `Path` for the
265    /// second argument.
266    pub fn add_arg_pair<S1, S2>(&mut self, arg1: S1, arg2: S2) -> &mut Self
267    where
268        S1: AsRef<OsStr>,
269        S2: AsRef<OsStr>,
270    {
271        self.add_arg(arg1);
272        self.add_arg(arg2);
273        self
274    }
275
276    /// Append multiple arguments.
277    pub fn add_args<I, S>(&mut self, args: I) -> &mut Self
278    where
279        S: AsRef<OsStr>,
280        I: IntoIterator<Item = S>,
281    {
282        for arg in args {
283            self.add_arg(arg);
284        }
285        self
286    }
287
288    /// Set `capture` to `true`.
289    pub fn enable_capture(&mut self) -> &mut Self {
290        self.capture = true;
291        self
292    }
293
294    /// Set `combine_output` to `true`.
295    pub fn combine_output(&mut self) -> &mut Self {
296        self.combine_output = true;
297        self
298    }
299
300    /// Set the directory from which to run the program.
301    pub fn set_dir<S: AsRef<OsStr>>(&mut self, dir: S) -> &mut Self {
302        self.dir = Some(dir.as_ref().into());
303        self
304    }
305
306    /// Set `check` to `false`.
307    pub fn disable_check(&mut self) -> &mut Self {
308        self.check = false;
309        self
310    }
311
312    /// Run the command.
313    ///
314    /// If `capture` is `true`, the command's output (stdout and
315    /// stderr) is returned along with the status. If not, the stdout
316    /// and stderr are empty.
317    ///
318    /// If the command fails to start an error is returned. If check
319    /// is set, an error is also returned if the command exits
320    /// non-zero or due to a signal.
321    ///
322    /// If `log_command` is `true` then the command line is logged
323    /// before running it. If the command fails the error is not
324    /// logged or printed, but the resulting error type implements
325    /// `Display` and can be used for this purpose.
326    pub fn run(&self) -> Result<Output, Error> {
327        let cmd_str = self.command_line_lossy();
328        if self.log_command {
329            match self.log_to {
330                LogTo::Stdout => println!("{}", cmd_str),
331
332                #[cfg(feature = "logging")]
333                LogTo::Log => log::info!("{}", cmd_str),
334            }
335        }
336
337        let mut cmd: process::Command = self.into();
338        let out = if self.capture {
339            if self.combine_output {
340                combine_output(cmd).into_run_error(self)?
341            } else {
342                cmd.output().into_run_error(self)?.into()
343            }
344        } else {
345            let status = cmd.status().into_run_error(self)?;
346            Output {
347                stdout: Vec::new(),
348                stderr: Vec::new(),
349                status,
350            }
351        };
352        if self.check && !out.status.success() {
353            if self.capture && self.log_output_on_error {
354                let mut msg =
355                    format!("command '{}' failed: {}", cmd_str, out.status);
356                if self.combine_output {
357                    msg = format!(
358                        "{}\noutput:\n{}",
359                        msg,
360                        out.stdout_string_lossy()
361                    );
362                } else {
363                    msg = format!(
364                        "{}\nstdout:\n{}\nstderr:\n{}",
365                        msg,
366                        out.stdout_string_lossy(),
367                        out.stderr_string_lossy()
368                    );
369                }
370                match self.log_to {
371                    LogTo::Stdout => println!("{}", msg),
372
373                    #[cfg(feature = "logging")]
374                    LogTo::Log => log::error!("{}", msg),
375                }
376            }
377
378            return Err(Error {
379                command: self.clone(),
380                kind: ErrorKind::Exit(out.status),
381            });
382        }
383        Ok(out)
384    }
385
386    /// Format as a space-separated command line.
387    ///
388    /// The program path and the arguments are converted to strings
389    /// with [`OsStr::to_string_lossy`].
390    ///
391    /// If any component contains characters that are not ASCII
392    /// alphanumeric or in the set `/-_,:.=+`, the component is
393    /// quoted with `'` (single quotes). This is both too aggressive
394    /// (unnecessarily quoting things that don't need to be quoted)
395    /// and incorrect (e.g. a single quote will itself be quoted with
396    /// a single quote). This method is mostly intended for logging
397    /// though, and it should work reasonably well for that.
398    pub fn command_line_lossy(&self) -> String {
399        fn convert_word<S: AsRef<OsStr>>(word: S) -> String {
400            fn char_requires_quoting(c: char) -> bool {
401                if c.is_ascii_alphanumeric() {
402                    return false;
403                }
404                let allowed_chars = "/-_,:.=+";
405                !allowed_chars.contains(c)
406            }
407
408            let s = word.as_ref().to_string_lossy().to_string();
409            if s.chars().any(char_requires_quoting) {
410                format!("'{}'", s)
411            } else {
412                s
413            }
414        }
415
416        let mut out = convert_word(&self.program);
417        for arg in &self.args {
418            out.push(' ');
419            out.push_str(&convert_word(arg));
420        }
421        out
422    }
423}
424
425impl Default for Command {
426    fn default() -> Self {
427        Self {
428            program: PathBuf::new(),
429            args: Vec::new(),
430            dir: None,
431            log_to: LogTo::Stdout,
432            log_command: true,
433            log_output_on_error: false,
434            check: true,
435            capture: false,
436            combine_output: false,
437            clear_env: false,
438            env: HashMap::new(),
439        }
440    }
441}
442
443impl From<&Command> for process::Command {
444    fn from(cmd: &Command) -> Self {
445        let mut out = process::Command::new(&cmd.program);
446        out.args(&cmd.args);
447        if let Some(dir) = &cmd.dir {
448            out.current_dir(dir);
449        }
450        if cmd.clear_env {
451            out.env_clear();
452        }
453        out.envs(&cmd.env);
454        out
455    }
456}