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