embuild/cmd.rs
1//! Command building and running utilities.
2
3use std::ffi::OsStr;
4use std::io;
5use std::process::{self, Command, ExitStatus};
6
7/// Error when trying to execute a command.
8#[derive(Debug, thiserror::Error)]
9pub enum CmdError {
10 /// The command failed to start.
11 #[error("command '{0}' failed to start")]
12 NoRun(String, #[source] io::Error),
13 /// The command exited unsucessfully (with non-zero exit status).
14 #[error("command '{0}' exited with non-zero status code {1}")]
15 Unsuccessful(String, i32, #[source] Option<anyhow::Error>),
16 /// The command was terminated unexpectedly.
17 #[error("command '{0}' was terminated unexpectedly")]
18 Terminated(String),
19}
20
21impl CmdError {
22 /// Create a [`CmdError::NoRun`].
23 pub fn no_run(cmd: &process::Command, error: io::Error) -> Self {
24 CmdError::NoRun(format!("{cmd:?}"), error)
25 }
26
27 /// Convert a [`process::ExitStatus`] into a `Result<(), CmdError>`.
28 pub fn status_into_result(
29 status: process::ExitStatus,
30 cmd: &process::Command,
31 cmd_output: impl FnOnce() -> Option<String>,
32 ) -> Result<(), Self> {
33 if status.success() {
34 Ok(())
35 } else if let Some(code) = status.code() {
36 Err(CmdError::Unsuccessful(
37 format!("{cmd:?}"),
38 code,
39 cmd_output().map(anyhow::Error::msg),
40 ))
41 } else {
42 Err(CmdError::Terminated(format!("{cmd:?}")))
43 }
44 }
45}
46
47/// A wrapper over a [`std::process::Command`] with more features.
48#[derive(Debug)]
49pub struct Cmd {
50 /// The actual [`std::process::Command`] wrapped.
51 pub cmd: std::process::Command,
52 ignore_exitcode: bool,
53}
54
55impl std::ops::Deref for Cmd {
56 type Target = std::process::Command;
57
58 fn deref(&self) -> &Self::Target {
59 &self.cmd
60 }
61}
62
63impl std::ops::DerefMut for Cmd {
64 fn deref_mut(&mut self) -> &mut Self::Target {
65 &mut self.cmd
66 }
67}
68
69impl From<std::process::Command> for Cmd {
70 fn from(cmd: std::process::Command) -> Self {
71 Cmd {
72 cmd,
73 ignore_exitcode: false,
74 }
75 }
76}
77
78impl From<Cmd> for std::process::Command {
79 fn from(cmd: Cmd) -> Self {
80 cmd.into_inner()
81 }
82}
83
84impl Cmd {
85 /// Construct a new [`Cmd`] for launching `program` (see
86 /// [`std::process::Command::new`]).
87 pub fn new(program: impl AsRef<OsStr>) -> Self {
88 Self {
89 cmd: Command::new(program),
90 ignore_exitcode: false,
91 }
92 }
93
94 /// Ignore the exit code when executing this command.
95 ///
96 /// Applies to:
97 /// - [`Cmd::run`]
98 /// - [`Cmd::output`]
99 /// - [`Cmd::stdout`]
100 /// - [`Cmd::stderr`]
101 pub fn ignore_exitcode(&mut self) -> &mut Self {
102 self.ignore_exitcode = true;
103 self
104 }
105
106 /// Run the command to completion.
107 ///
108 /// If [`Cmd::ignore_exitcode`] has been called a program that exited with an error
109 /// will also return [`Ok`], otherwise it will return [`Err`].
110 /// A program that failed to start will always return an [`Err`].
111 ///
112 /// [`std::process::Command::status`] is used internally.
113 pub fn run(&mut self) -> Result<(), CmdError> {
114 self.cmd
115 .status()
116 .map_err(|e| CmdError::no_run(&self.cmd, e))
117 .and_then(|v| {
118 if self.ignore_exitcode {
119 Ok(())
120 } else {
121 CmdError::status_into_result(v, &self.cmd, || None)
122 }
123 })
124 }
125
126 /// Run the command and get its [`ExitStatus`].
127 pub fn status(&mut self) -> Result<ExitStatus, CmdError> {
128 self.cmd
129 .status()
130 .map_err(|e| CmdError::no_run(&self.cmd, e))
131 }
132
133 fn print_output(&self, output: &std::process::Output) {
134 // TODO: add some way to quiet this output
135 use std::io::Write;
136 std::io::stdout().write_all(&output.stdout[..]).ok();
137 std::io::stderr().write_all(&output.stderr[..]).ok();
138 }
139
140 /// Run the command to completion and use its [`std::process::Output`] with `func`.
141 ///
142 /// If [`Cmd::ignore_exitcode`] has been called a program that exited with an error
143 /// will also return [`Ok`], otherwise it will return [`Err`].
144 /// A program that failed to start will always return an [`Err`].
145 ///
146 /// [`std::process::Command::output`] is used internally.
147 pub fn output<T>(
148 &mut self,
149 func: impl FnOnce(std::process::Output) -> T,
150 ) -> Result<T, CmdError> {
151 match self.cmd.output() {
152 Err(err) => Err(CmdError::no_run(&self.cmd, err)),
153 Ok(result) => if self.ignore_exitcode {
154 self.print_output(&result);
155 Ok(())
156 } else {
157 CmdError::status_into_result(result.status, &self.cmd, || {
158 Some(
159 String::from_utf8_lossy(&result.stderr[..])
160 .trim_end()
161 .to_string(),
162 )
163 })
164 }
165 .map_err(|e| {
166 self.print_output(&result);
167 e
168 })
169 .map(|_| func(result)),
170 }
171 }
172
173 /// Run the command to completion and get its stdout output.
174 ///
175 /// See [`Cmd::output`].
176 pub fn stdout(&mut self) -> Result<String, CmdError> {
177 self.output(|output| {
178 String::from_utf8_lossy(&output.stdout[..])
179 .trim_end()
180 .to_string()
181 })
182 }
183
184 /// Run the command to completion and get its stderr output.
185 ///
186 /// See [`Cmd::output`].
187 pub fn stderr(&mut self) -> Result<String, CmdError> {
188 self.output(|output| {
189 String::from_utf8_lossy(&output.stderr[..])
190 .trim_end()
191 .to_string()
192 })
193 }
194
195 /// Turn this [`Cmd`] into its underlying [`std::process::Command`].
196 pub fn into_inner(self) -> std::process::Command {
197 self.cmd
198 }
199}
200
201/// Build a command using a given [`std::process::Command`] or [`Cmd`] and return it.
202///
203/// The first argument is expected to be a [`std::process::Command`] or [`Cmd`] instance.
204///
205/// For a `new` builder the second argument, the program to run (passed to
206/// [`std::process::Command::new`]) is mandatory. Every comma seperated argument
207/// thereafter is added to the command's arguments. Arguments after an `@`-sign specify
208/// collections of arguments (specifically `impl IntoIterator<Item = impl AsRef<OsStr>`).
209/// The opional `key=value` arguments after a semicolon are simply translated to calling
210/// the `std::process::Command::<key>` method with `value` as its arguments.
211///
212/// **Note:**
213/// `@`-arguments must be followed by at least one normal argument. For example
214/// `cmd_build!(new, "cmd", @args)` will not compile but `cmd_build!(new, "cmd", @args,
215/// "other")` will. You can use `key=value` arguments to work around this limitation:
216/// `cmd_build!(new, "cmd"; args=(args))`.
217///
218/// At the end the built [`std::process::Command`] is returned.
219///
220/// # Examples
221/// ```
222/// # use embuild::{cmd::Cmd, cmd_build};
223/// let args_list = ["--foo", "--bar", "value"];
224/// let mut cmd = Cmd::new("git");
225/// let mut cmd = cmd_build!(cmd, @args_list, "clone"; arg=("url.com"), env=("var", "value"));
226/// ```
227#[macro_export]
228macro_rules! cmd_build {
229 ($builder:ident $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{
230 $(
231 $($builder .args($cmdargs);)*
232 $builder .arg($cmdarg);
233 )*
234 $($($builder . $k $v;)*)?
235
236 $builder
237 }}
238}
239
240/// Create a new [`Cmd`] instance.
241///
242/// This is a simple wrapper over the [`std::process::Command`] and [`Cmd`] API. It
243/// expects at least one argument for the program to run. Every comma seperated argument
244/// thereafter is added to the command's arguments. Arguments after an `@`-sign specify
245/// collections of arguments (specifically `impl IntoIterator<Item = impl AsRef<OsStr>`).
246/// The opional `key=value` arguments after a semicolon are simply translated to calling
247/// the `Cmd::<key>` method with `value` as its arguments.
248///
249/// **Note:**
250/// `@`-arguments must be followed by at least one normal argument. For example
251/// `cmd!("cmd", @args)` will not compile but `cmd!("cmd", @args, "other")` will. You can
252/// use `key=value` arguments to work around this limitation: `cmd!("cmd"; args=(args))`.
253///
254/// # Examples
255/// ```
256/// # use embuild::cmd;
257/// let args_list = ["--foo", "--bar", "value"];
258/// let mut cmd = cmd!("git", @args_list, "clone"; arg=("url.com"), env=("var", "value"));
259/// ```
260#[macro_export]
261macro_rules! cmd {
262 ($cmd:expr $(, $(@$cmdargs:expr,)* $cmdarg:expr)* $(; $($k:ident = $v:tt),*)?) => {{
263 let mut cmd = $crate::cmd::Cmd::new($cmd);
264 $crate::cmd_build!(cmd $(, $(@$cmdargs,)* $cmdarg)* $(; $($k = $v),* )?)
265 }};
266}