1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! A crate with an "easy" child process API called [`EasyCommand`] that facilitates common use
//! cases for using child processes. It offers the following:
//!
//! * A "nice" [`Display`] implementation that
//! * Straightforward error-handling; you should have most of the context you need for debugging
//!   from the errors that this API returns, minus anything application-specific you wish to add on
//!   top.
//! * Logging using the [`log`] crate.

use std::{
    ffi::OsStr,
    fmt::{self, Debug, Display, Formatter},
    io,
    iter::once,
    process::{Command, ExitStatus, Output},
};

/// A convenience API around [`Command`].
pub struct EasyCommand {
    inner: Command,
}

impl EasyCommand {
    /// Equivalent to [`Command::new`].
    pub fn new<C>(cmd: C) -> Self
    where
        C: AsRef<OsStr>,
    {
        Self::new_with(cmd, |cmd| cmd)
    }

    /// A convenience constructor that allows other method calls to be chained onto this one.
    pub fn new_with<C>(cmd: C, f: impl FnOnce(&mut Command) -> &mut Command) -> Self
    where
        C: AsRef<OsStr>,
    {
        let mut cmd = Command::new(cmd);
        f(&mut cmd);
        Self { inner: cmd }
    }

    /// Like [`Self::new_with`], but optimized for ergonomic usage of an [`IntoIterator`] for
    /// arguments.
    pub fn simple<C, A, I>(cmd: C, args: I) -> Self
    where
        C: AsRef<OsStr>,
        A: AsRef<OsStr>,
        I: IntoIterator<Item = A>,
    {
        Self::new_with(cmd, |cmd| cmd.args(args))
    }

    fn spawn_and_wait_impl(&mut self) -> Result<ExitStatus, SpawnAndWaitErrorKind> {
        log::debug!("spawning child process with {self}…");

        self.inner
            .spawn()
            .map_err(|source| SpawnAndWaitErrorKind::Spawn { source })
            .and_then(|mut child| {
                log::trace!("waiting for exit from {self}…");
                let status = child
                    .wait()
                    .map_err(|source| SpawnAndWaitErrorKind::WaitForExitCode { source })?;
                log::debug!("received exit code {:?} from {self}", status.code());
                Ok(status)
            })
    }

    /// Execute this command, returning its exit status.
    ///
    /// This command wraps around [`Command::spawn`], which causes `stdout` and `stderr` to be
    /// inherited from its parent.
    pub fn spawn_and_wait(&mut self) -> Result<ExitStatus, ExecuteError<SpawnAndWaitErrorKind>> {
        self.spawn_and_wait_impl()
            .map_err(|source| ExecuteError::new(self, source))
    }

    fn run_impl(&mut self) -> Result<(), RunErrorKind> {
        let status = self.spawn_and_wait_impl()?;

        if status.success() {
            Ok(())
        } else {
            Err(RunErrorKind::UnsuccessfulExitCode {
                code: status.code(),
            })
        }
    }

    /// Execute this command, returning an error if it did not return a successful exit code.
    ///
    /// This command wraps around [`Command::spawn`], which causes `stdout` and `stderr` to be
    /// inherited from its parent.
    pub fn run(&mut self) -> Result<(), ExecuteError<RunErrorKind>> {
        self.run_impl()
            .map_err(|source| ExecuteError::new(self, source))
    }

    fn output_impl(&mut self) -> Result<Output, io::Error> {
        log::debug!("getting output from {self}…");
        let output = self.inner.output()?;
        log::debug!("received exit code {:?} from {self}", output.status.code());
        Ok(output)
    }

    /// Execute this command, capturing its output.
    pub fn output(&mut self) -> Result<Output, ExecuteError<io::Error>> {
        self.output_impl()
            .map_err(|source| ExecuteError::new(self, source))
    }
}

impl Debug for EasyCommand {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        Debug::fmt(&self.inner, f)
    }
}

impl Display for EasyCommand {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let Self { inner } = self;
        let prog = inner.get_program().to_string_lossy();
        let args = inner.get_args().map(|a| a.to_string_lossy());
        let shell_words = ::shell_words::join(once(prog).chain(args));
        write!(f, "`{shell_words}`")
    }
}

#[derive(Debug)]
struct EasyCommandInvocation {
    shell_words: String,
}

impl EasyCommandInvocation {
    fn new(cmd: &EasyCommand) -> Self {
        let EasyCommand { inner } = cmd;
        let prog = inner.get_program().to_string_lossy();
        let args = inner.get_args().map(|a| a.to_string_lossy());
        let shell_words = ::shell_words::join(once(prog).chain(args));
        Self { shell_words }
    }
}

impl Display for EasyCommandInvocation {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let Self { shell_words } = self;
        write!(f, "{shell_words}")
    }
}

/// An error returned by [`EasyCommand`]'s methods.
#[derive(Debug, thiserror::Error)]
#[error("failed to execute {cmd}")]
pub struct ExecuteError<E> {
    cmd: EasyCommandInvocation,
    pub source: E,
}

impl<E> ExecuteError<E> {
    fn new(cmd: &EasyCommand, source: E) -> Self {
        Self {
            cmd: EasyCommandInvocation::new(cmd),
            source,
        }
    }
}

/// The specific error case encountered with [`EasyCommand::spawn_and_wait`].
#[derive(Debug, thiserror::Error)]
pub enum SpawnAndWaitErrorKind {
    #[error("failed to spawn")]
    Spawn { source: io::Error },
    #[error("failed to wait for exit code")]
    WaitForExitCode { source: io::Error },
}

/// The specific error case encountered with a [`EasyCommand::run`].
#[derive(Debug, thiserror::Error)]
pub enum RunErrorKind {
    #[error(transparent)]
    SpawnAndWait(#[from] SpawnAndWaitErrorKind),
    #[error("returned exit code {code:?}")]
    UnsuccessfulExitCode { code: Option<i32> },
}