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}