ezcmd/
lib.rs

1//! A crate with an "easy" child process API called [`EasyCommand`] that facilitates common use
2//! cases for using child processes. It offers the following:
3//!
4//! * A "nice" [`Display`] implementation that
5//! * Straightforward error-handling; you should have most of the context you need for debugging
6//!   from the errors that this API returns, minus anything application-specific you wish to add on
7//!   top.
8//! * Logging using the [`log`] crate.
9
10use std::{
11    ffi::OsStr,
12    fmt::{self, Debug, Display, Formatter},
13    io,
14    iter::once,
15    process::{Command, ExitStatus, Output},
16};
17
18/// A convenience API around [`Command`].
19pub struct EasyCommand {
20    inner: Command,
21}
22
23impl EasyCommand {
24    /// Equivalent to [`Command::new`].
25    pub fn new<C>(cmd: C) -> Self
26    where
27        C: AsRef<OsStr>,
28    {
29        Self::new_with(cmd, |cmd| cmd)
30    }
31
32    /// A convenience constructor that allows other method calls to be chained onto this one.
33    pub fn new_with<C>(cmd: C, f: impl FnOnce(&mut Command) -> &mut Command) -> Self
34    where
35        C: AsRef<OsStr>,
36    {
37        let mut cmd = Command::new(cmd);
38        f(&mut cmd);
39        Self { inner: cmd }
40    }
41
42    /// Like [`Self::new_with`], but optimized for ergonomic usage of an [`IntoIterator`] for
43    /// arguments.
44    pub fn simple<C, A, I>(cmd: C, args: I) -> Self
45    where
46        C: AsRef<OsStr>,
47        A: AsRef<OsStr>,
48        I: IntoIterator<Item = A>,
49    {
50        Self::new_with(cmd, |cmd| cmd.args(args))
51    }
52
53    fn spawn_and_wait_impl(&mut self) -> Result<ExitStatus, SpawnAndWaitErrorKind> {
54        log::debug!("spawning child process with {self}…");
55
56        self.inner
57            .spawn()
58            .map_err(|source| SpawnAndWaitErrorKind::Spawn { source })
59            .and_then(|mut child| {
60                log::trace!("waiting for exit from {self}…");
61                let status = child
62                    .wait()
63                    .map_err(|source| SpawnAndWaitErrorKind::WaitForExitCode { source })?;
64                log::debug!("received exit code {:?} from {self}", status.code());
65                Ok(status)
66            })
67    }
68
69    /// Execute this command, returning its exit status.
70    ///
71    /// This command wraps around [`Command::spawn`], which causes `stdout` and `stderr` to be
72    /// inherited from its parent.
73    pub fn spawn_and_wait(&mut self) -> Result<ExitStatus, ExecuteError<SpawnAndWaitErrorKind>> {
74        self.spawn_and_wait_impl()
75            .map_err(|source| ExecuteError::new(self, source))
76    }
77
78    fn run_impl(&mut self) -> Result<(), RunErrorKind> {
79        let status = self.spawn_and_wait_impl()?;
80
81        if status.success() {
82            Ok(())
83        } else {
84            Err(RunErrorKind::UnsuccessfulExitCode {
85                code: status.code(),
86            })
87        }
88    }
89
90    /// Execute this command, returning an error if it did not return a successful exit code.
91    ///
92    /// This command wraps around [`Command::spawn`], which causes `stdout` and `stderr` to be
93    /// inherited from its parent.
94    pub fn run(&mut self) -> Result<(), ExecuteError<RunErrorKind>> {
95        self.run_impl()
96            .map_err(|source| ExecuteError::new(self, source))
97    }
98
99    fn output_impl(&mut self) -> Result<Output, io::Error> {
100        log::debug!("getting output from {self}…");
101        let output = self.inner.output()?;
102        log::debug!("received exit code {:?} from {self}", output.status.code());
103        Ok(output)
104    }
105
106    /// Execute this command, capturing its output.
107    pub fn output(&mut self) -> Result<Output, ExecuteError<io::Error>> {
108        self.output_impl()
109            .map_err(|source| ExecuteError::new(self, source))
110    }
111}
112
113impl Debug for EasyCommand {
114    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
115        Debug::fmt(&self.inner, f)
116    }
117}
118
119impl Display for EasyCommand {
120    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
121        let Self { inner } = self;
122        let prog = inner.get_program().to_string_lossy();
123        let args = inner.get_args().map(|a| a.to_string_lossy());
124        let shell_words = ::shell_words::join(once(prog).chain(args));
125        write!(f, "`{shell_words}`")
126    }
127}
128
129#[derive(Debug)]
130struct EasyCommandInvocation {
131    shell_words: String,
132}
133
134impl EasyCommandInvocation {
135    fn new(cmd: &EasyCommand) -> Self {
136        let EasyCommand { inner } = cmd;
137        let prog = inner.get_program().to_string_lossy();
138        let args = inner.get_args().map(|a| a.to_string_lossy());
139        let shell_words = ::shell_words::join(once(prog).chain(args));
140        Self { shell_words }
141    }
142}
143
144impl Display for EasyCommandInvocation {
145    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
146        let Self { shell_words } = self;
147        write!(f, "{shell_words}")
148    }
149}
150
151/// An error returned by [`EasyCommand`]'s methods.
152#[derive(Debug, thiserror::Error)]
153#[error("failed to execute {cmd}")]
154pub struct ExecuteError<E> {
155    cmd: EasyCommandInvocation,
156    pub source: E,
157}
158
159impl<E> ExecuteError<E> {
160    fn new(cmd: &EasyCommand, source: E) -> Self {
161        Self {
162            cmd: EasyCommandInvocation::new(cmd),
163            source,
164        }
165    }
166}
167
168/// The specific error case encountered with [`EasyCommand::spawn_and_wait`].
169#[derive(Debug, thiserror::Error)]
170pub enum SpawnAndWaitErrorKind {
171    #[error("failed to spawn")]
172    Spawn { source: io::Error },
173    #[error("failed to wait for exit code")]
174    WaitForExitCode { source: io::Error },
175}
176
177/// The specific error case encountered with a [`EasyCommand::run`].
178#[derive(Debug, thiserror::Error)]
179pub enum RunErrorKind {
180    #[error(transparent)]
181    SpawnAndWait(#[from] SpawnAndWaitErrorKind),
182    #[error("returned exit code {code:?}")]
183    UnsuccessfulExitCode { code: Option<i32> },
184}