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}