sheller/
lib.rs

1use tracing::{debug, error, info};
2
3mod macros;
4
5#[derive(Debug)]
6pub enum Error {
7    Io(std::io::Error),
8    ExitCode(i32),
9    Signal(i32),
10    NoExitCodeAndSignal,
11}
12
13impl std::fmt::Display for Error {
14    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
15        match self {
16            Error::Io(e) => write!(f, "I/O error: {e}"),
17            Error::ExitCode(exit_code) => write!(f, "Exit code: {exit_code}"),
18            Error::Signal(signal) => write!(f, "Signal: {signal}"),
19            Error::NoExitCodeAndSignal => write!(f, "No exit code and signal"),
20        }
21    }
22}
23
24impl std::error::Error for Error {}
25
26impl From<std::io::Error> for Error {
27    fn from(e: std::io::Error) -> Self {
28        Error::Io(e)
29    }
30}
31
32pub type Result<T> = std::result::Result<T, Error>;
33
34#[derive(Debug)]
35struct Metadata<'a> {
36    env_key: &'a str,
37    program: &'a str,
38    args: &'a [&'a str],
39}
40
41#[cfg(windows)]
42static DEFAULT_METADATA: Metadata = Metadata {
43    env_key: "COMSPEC",
44    program: "cmd.exe",
45    args: &["/D", "/S", "/C"],
46};
47
48#[cfg(unix)]
49static DEFAULT_METADATA: Metadata = Metadata {
50    env_key: "SHELL",
51    program: "/bin/sh",
52    args: &["-c"],
53};
54
55fn parse_program() -> String {
56    std::env::var(DEFAULT_METADATA.env_key).unwrap_or_else(|e| {
57        debug!(
58            default_program = DEFAULT_METADATA.program,
59            env_key = DEFAULT_METADATA.env_key,
60            error = ?e,
61            "Failed to get shell environment variable, falling back to default program."
62        );
63        DEFAULT_METADATA.program.to_string()
64    })
65}
66
67/// Sheller is a builder for `std::process::Command` that sets the shell program and arguments.
68///
69/// Please see the `Sheller::new` method for more information.
70#[derive(Debug)]
71pub struct Sheller {
72    program: String,
73    args: Vec<&'static str>,
74    script: String,
75}
76
77impl Default for Sheller {
78    fn default() -> Self {
79        Self {
80            program: parse_program(),
81            args: DEFAULT_METADATA.args.into(),
82            script: String::new(),
83        }
84    }
85}
86
87impl Sheller {
88    /// Create a new `Sheller` with the given `script` and platform-specific defaults.
89    ///
90    /// # Platform-specific defaults
91    ///
92    /// ## Windows
93    ///
94    /// When `target_family` is `windows`.
95    ///
96    /// Set the `COMSPEC` environment variable to `program`, and if the environment variable is not set, use `cmd.exe` as the fallback program.
97    ///
98    /// Also set the `args` to `["/D", "/S", "/C"]`.
99    ///
100    /// ## Unix
101    ///
102    /// When `target_family` is `unix`.
103    ///
104    /// Set the `SHELL` environment variable to `program`, and if the environment variable is not set, use `/bin/sh` as the fallback program.
105    ///
106    /// Also set the `args` to `["-c"]`.
107    ///
108    /// # Arguments
109    ///
110    /// * `script` - The shell script to run. This is dependent on the shell program.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use sheller::Sheller;
116    ///
117    /// let mut command = Sheller::new("echo hello").build();
118    /// assert!(command.status().unwrap().success());
119    /// ```
120    #[must_use]
121    pub fn new<T>(script: T) -> Self
122    where
123        T: Into<String>,
124    {
125        Self {
126            script: script.into(),
127            ..Default::default()
128        }
129    }
130
131    /// Returns `std::process::Command` with the shell program and arguments set.
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// use sheller::Sheller;
137    ///
138    /// let mut command = Sheller::new("echo hello").build();
139    /// assert!(command.status().unwrap().success());
140    /// ```
141    #[must_use]
142    pub fn build(self) -> std::process::Command {
143        let mut command = std::process::Command::new(&self.program);
144        command.args(&self.args);
145        command.arg(self.script);
146        command
147    }
148
149    /// Run the shell command and panic if the command failed to run.
150    ///
151    /// # Examples
152    /// ```
153    /// use sheller::{CommandExt, Sheller};
154    ///
155    /// Sheller::new("echo hello").run();
156    /// ```
157    ///
158    /// # Panics
159    /// Panics if the command failed to run.
160    pub fn run(self) {
161        self.build().run();
162    }
163
164    /// Run the shell command and return a `Result`.
165    ///
166    /// # Examples
167    /// ```
168    /// use sheller::{CommandExt, Sheller};
169    ///
170    /// Sheller::new("echo hello").try_run().unwrap();
171    /// ```
172    ///
173    /// # Errors
174    /// Returns an `Err` if the command failed to run.
175    pub fn try_run(self) -> Result<()> {
176        self.build().try_run()
177    }
178}
179
180pub trait CommandExt {
181    /// Run the command and panic if the command failed to run.
182    ///
183    /// # Examples
184    /// ```
185    /// use sheller::CommandExt;
186    /// use std::process::Command;
187    ///
188    /// #[cfg(windows)]
189    /// fn example() {
190    ///     let mut command = Command::new("cmd.exe");
191    ///     command.args(["/D", "/S", "/C", "echo hello"]).run();
192    /// }
193    ///
194    /// #[cfg(unix)]
195    /// fn example() {
196    ///     let mut command = Command::new("echo");
197    ///     command.arg("hello").run();
198    /// }
199    ///
200    /// example();
201    /// ```
202    ///
203    /// # Panics
204    /// Panics if the command failed to run.
205    fn run(&mut self);
206
207    /// Run the command and return a `Result`.
208    ///
209    /// # Examples
210    /// ```
211    /// use sheller::CommandExt;
212    /// use std::process::Command;
213    ///
214    /// #[cfg(windows)]
215    /// fn example() {
216    ///     let mut command = Command::new("cmd.exe");
217    ///     command
218    ///         .args(["/D", "/S", "/C", "echo hello"])
219    ///         .try_run()
220    ///         .unwrap();
221    /// }
222    ///
223    /// #[cfg(unix)]
224    /// fn example() {
225    ///     let mut command = Command::new("echo");
226    ///     command.arg("hello").try_run().unwrap();
227    /// }
228    ///
229    /// example();
230    /// ```
231    ///
232    /// # Errors
233    /// Returns an `Err` if the command failed to run.
234    fn try_run(&mut self) -> Result<()>;
235}
236
237#[cfg(unix)]
238fn get_signal(a: std::process::ExitStatus) -> Option<i32> {
239    use std::os::unix::process::ExitStatusExt;
240    a.signal()
241}
242
243#[cfg(windows)]
244fn get_signal(_: std::process::ExitStatus) -> Option<i32> {
245    None
246}
247
248impl CommandExt for std::process::Command {
249    /// Run the command and panic if the command failed to run.
250    ///
251    /// # Examples
252    /// ```
253    /// use sheller::CommandExt;
254    /// use std::process::Command;
255    ///
256    /// #[cfg(windows)]
257    /// fn example() {
258    ///     let mut command = Command::new("cmd.exe");
259    ///     command.args(["/D", "/S", "/C", "echo hello"]).run();
260    /// }
261    ///
262    /// #[cfg(unix)]
263    /// fn example() {
264    ///     let mut command = Command::new("echo");
265    ///     command.arg("hello").run();
266    /// }
267    ///
268    /// example();
269    /// ```
270    ///
271    /// # Panics
272    /// Panics if the command failed to run.
273    fn run(&mut self) {
274        self.try_run().unwrap();
275    }
276
277    /// Run the command and return a `Result` with `Ok` if the command was successful, and `Err` if the command failed.
278    ///
279    /// # Examples
280    /// ```
281    /// use sheller::CommandExt;
282    /// use std::process::Command;
283    ///
284    /// #[cfg(windows)]
285    /// fn example() {
286    ///     let mut command = Command::new("cmd.exe");
287    ///     command
288    ///         .args(["/D", "/S", "/C", "echo hello"])
289    ///         .try_run()
290    ///         .unwrap();
291    /// }
292    ///
293    /// #[cfg(unix)]
294    /// fn example() {
295    ///     let mut command = Command::new("echo");
296    ///     command.arg("hello").try_run().unwrap();
297    /// }
298    ///
299    /// example();
300    /// ```
301    ///
302    /// # Errors
303    /// Returns an `Err` if the command failed to run.
304    fn try_run(&mut self) -> Result<()> {
305        info!(command = ?self, "Running command.");
306        let mut command = self.spawn().map_err(|e| {
307            error!(command = ?self, error = ?e, "Failed to spawn command.");
308            e
309        })?;
310        let status = command.wait().map_err(|e| {
311            error!(command = ?self, error = ?e, "Failed to wait for command.");
312            e
313        })?;
314        if let Some(exit_code) = status.code() {
315            if exit_code == 0 {
316                info!(command = ?self, "Succeeded to run command with zero exit code.");
317                Ok(())
318            } else {
319                error!(command = ?self, exit_code = ?exit_code, "Failed to run command with non-zero exit code.");
320                Err(Error::ExitCode(exit_code))
321            }
322        } else if let Some(signal) = get_signal(status) {
323            error!(command = ?self, signal = ?signal, "Failed to run command with signal.");
324            Err(Error::Signal(signal))
325        } else {
326            error!(command = ?self, "Failed to run command with no exit code and signal.");
327            Err(Error::NoExitCodeAndSignal)
328        }
329    }
330}