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}