1 //! xshell makes it easy to write cross-platform "bash" scripts in Rust.
2 //!
3 //! It provides a `cmd!` macro for running subprocesses, as well as a number of
4 //! basic file manipulation utilities.
5 //!
6 //! ```
7 //! # if cfg!(windows) { return Ok(()); }
8 //! use xshell::{cmd, read_file};
9 //!
10 //! let name = "Julia";
11 //! let output = cmd!("echo hello {name}!").read()?;
12 //! assert_eq!(output, "hello Julia!");
13 //!
14 //! let err = read_file("feeling-lucky.txt").unwrap_err();
15 //! assert_eq!(
16 //!     err.to_string(),
17 //!     "`feeling-lucky.txt`: no such file or directory (os error 2)",
18 //! );
19 //! # Ok::<(), xshell::Error>(())
20 //! ```
21 //!
22 //! The intended use-case is various bits of glue code, which could be written
23 //! in bash or python. The original motivation is
24 //! [`xtask`](https://github.com/matklad/cargo-xtask) development.
25 //!
26 //! **Goals**: fast compile times, ergonomics, clear error messages.<br>
27 //! **Non goals**: completeness, robustness / misuse resistance.
28 //!
29 //! For "heavy-duty" code, consider using [`duct`] or [`std::process::Command`]
30 //! instead.
31 //!
32 //! # API Overview
33 //!
34 //! For a real-world example, see this crate's own CI script:
35 //!
36 //! [https://github.com/matklad/xshell/blob/master/examples/ci.rs](https://github.com/matklad/xshell/blob/master/examples/ci.rs)
37 //!
38 //! ## `cmd!` Macro
39 //!
40 //! Read output of the process into `String`. The final newline will be
41 //! stripped.
42 //!
43 //! ```
44 //! # use xshell::cmd;
45 //! let output = cmd!("date +%Y-%m-%d").read()?;
46 //! assert!(output.chars().all(|c| "01234567890-".contains(c)));
47 //! # Ok::<(), xshell::Error>(())
48 //! ```
49 //!
50 //! If the exist status is non-zero, an error is returned.
51 //!
52 //! ```
53 //! # use xshell::cmd;
54 //! let err = cmd!("false").read().unwrap_err();
55 //! assert!(err.to_string().starts_with("command `false` failed"));
56 //! ```
57 //!
58 //! <hr>
59 //!
60 //! Run the process, inheriting stdout and stderr. The command is echoed to
61 //! stdout.
62 //!
63 //! ```
64 //! # use xshell::cmd;
65 //! cmd!("echo hello!").run()?;
66 //! # Ok::<(), xshell::Error>(())
67 //! ```
68 //!
69 //! Output
70 //!
71 //! ```text
72 //! $ echo hello!
73 //! hello!
74 //! ```
75 //!
76 //! <hr>
77 //!
78 //! Interpolation is supported via `{name}` syntax. Use `{name...}` to
79 //! interpolate sequence of values.
80 //!
81 //! ```
82 //! # use xshell::cmd;
83 //! let greeting = "Guten Tag";
84 //! let people = &["Spica", "Boarst", "Georgina"];
85 //! assert_eq!(
86 //!     cmd!("echo {greeting} {people...}").to_string(),
87 //!     r#"echo "Guten Tag" Spica Boarst Georgina"#
88 //! );
89 //! ```
90 //!
91 //! Note that the argument with a space is handled correctly. This is because
92 //! `cmd!` macro parses the string template at compile time. The macro hands the
93 //! interpolated values to the underlying `std::process::Command` as is and is
94 //! not vulnerable to [shell
95 //! injection](https://en.wikipedia.org/wiki/Code_injection#Shell_injection).
96 //!
97 //! Single quotes in literal arguments are supported:
98 //!
99 //! ```
100 //! # use xshell::cmd;
101 //! assert_eq!(
102 //!     cmd!("echo 'hello world'").to_string(),
103 //!     r#"echo "hello world""#,
104 //! )
105 //! ```
106 //! Splat syntax is used for optional arguments idiom.
107 //!
108 //! ```
109 //! # use xshell::cmd;
110 //! let check = if true { &["--", "--check"] } else { &[][..] };
111 //! assert_eq!(
112 //!     cmd!("cargo fmt {check...}").to_string(),
113 //!     "cargo fmt -- --check"
114 //! );
115 //!
116 //! let dry_run = if true { Some("--dry-run") } else { None };
117 //! assert_eq!(
118 //!     cmd!("git push {dry_run...}").to_string(),
119 //!     "git push --dry-run"
120 //! );
121 //! ```
122 //!
123 //! <hr>
124 //!
125 //! xshell does not provide API for creating command pipelines. If you need
126 //! pipelines, consider using [`duct`] instead. Alternatively, you can convert
127 //! `xshell::Cmd` into [`std::process::Command`]:
128 //!
129 //! ```
130 //! # use xshell::cmd;
131 //! let command: std::process::Command = cmd!("echo 'hello world'").into();
132 //! ```
133 //!
134 //! ## Manipulating the Environment
135 //!
136 //! Instead of `cd` and `export`, xshell uses RAII based `pushd` and `pushenv`
137 //!
138 //! ```
139 //! use xshell::{cwd, pushd, pushenv};
140 //!
141 //! let initial_dir = cwd()?;
142 //! {
143 //!     let _p = pushd("src")?;
144 //!     assert_eq!(
145 //!         cwd()?,
146 //!         initial_dir.join("src"),
147 //!     );
148 //! }
149 //! assert_eq!(cwd()?, initial_dir);
150 //!
151 //! assert!(std::env::var("MY_VAR").is_err());
152 //! let _e = pushenv("MY_VAR", "92");
153 //! assert_eq!(
154 //!     std::env::var("MY_VAR").as_deref(),
155 //!     Ok("92")
156 //! );
157 //! # Ok::<(), xshell::Error>(())
158 //! ```
159 //!
160 //! ## Working with Files
161 //!
162 //! xshell provides the following utilities, which are mostly re-exports from
163 //! `std::fs` module with paths added to error messages: `rm_rf`, `read_file`,
164 //! `write_file`, `mkdir_p`, `cp`, `read_dir`, `cwd`.
165 //!
166 //! # Maintenance
167 //!
168 //! Minimum Supported Rust Version: 1.47.0. MSRV bump is not considered semver
169 //! breaking. MSRV is updated conservatively.
170 //!
171 //! The crate isn't comprehensive. Additional functionality is added on
172 //! as-needed bases, as long as it doesn't compromise compile times.
173 //! Function-level docs are an especially welcome addition :-)
174 //!
175 //! # Implementation details
176 //!
177 //! The design is heavily inspired by the Julia language:
178 //!
179 //! * [Shelling Out Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/)
180 //! * [Put This In Your Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/)
181 //! * [Running External Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/)
182 //! * [Filesystem](https://docs.julialang.org/en/v1/base/file/)
183 //!
184 //! Smaller influences are the [`duct`] crate and Ruby's
185 //! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html)
186 //! module.
187 //!
188 //! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on
189 //! helper libraries, so the fixed-cost impact on compile times is moderate.
190 //! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second.
191 //! Equivalent program using only `std::process::Command` compiles in 0.25
192 //! seconds.
193 //!
194 //! To make IDEs infer correct types without expanding proc-macro, it is wrapped
195 //! into a declarative macro which supplies type hints.
196 //!
197 //! Environment manipulation mutates global state and might have surprising
198 //! interactions with threads. Internally, everything is protected by a global
199 //! shell lock, so all functions in this crate are thread safe. However,
200 //! functions outside of xshell's control might experience race conditions:
201 //!
202 //! ```
203 //! use std::{thread, fs};
204 //!
205 //! use xshell::{pushd, read_file};
206 //!
207 //! let t1 = thread::spawn(|| {
208 //!     let _p = pushd("./src");
209 //! });
210 //!
211 //! // This is guaranteed to work: t2 will block while t1 is in `pushd`.
212 //! let t2 = thread::spawn(|| {
213 //!     let res = read_file("./src/lib.rs");
214 //!     assert!(res.is_ok());
215 //! });
216 //!
217 //! // This is a race: t3 might observe difference cwds depending on timing.
218 //! let t3 = thread::spawn(|| {
219 //!     let res = fs::read_to_string("./src/lib.rs");
220 //!     assert!(res.is_ok() || res.is_err());
221 //! });
222 //! # t1.join().unwrap(); t2.join().unwrap(); t3.join().unwrap();
223 //! ```
224 //!
225 //! # Naming
226 //!
227 //! xshell is an ex-shell, for those who grew tired of bash.<br>
228 //! xshell is an x-platform shell, for those who don't want to run `build.sh` on windows.<br>
229 //! xshell is built for [`xtask`](https://github.com/matklad/cargo-xtask).<br>
230 //! xshell uses x-traordinary level of [trickery](https://github.com/matklad/xshell/blob/843df7cd5b7d69fc9d2b884dc0852598335718fe/src/lib.rs#L233-L234),
231 //! just like `xtask` [does](https://matklad.github.io/2018/01/03/make-your-own-make.html).
232 //!
233 //! [`duct`]: https://github.com/oconnor663/duct.rs
234 //! [`std::process::Command`]: https://doc.rust-lang.org/stable/std/process/struct.Command.html
235 
236 #![deny(missing_debug_implementations)]
237 #![deny(missing_docs)]
238 #![deny(rust_2018_idioms)]
239 
240 mod env;
241 mod gsl;
242 mod error;
243 mod fs;
244 
245 use std::{
246     ffi::{OsStr, OsString},
247     fmt, io,
248     io::Write,
249     path::Path,
250     process::Output,
251     process::Stdio,
252 };
253 
254 use error::CmdErrorKind;
255 #[doc(hidden)]
256 pub use xshell_macros::__cmd;
257 
258 pub use crate::{
259     env::{pushd, pushenv, Pushd, Pushenv},
260     error::{Error, Result},
261     fs::{cp, cwd, hard_link, mkdir_p, mktemp_d, read_dir, read_file, rm_rf, write_file, TempDir},
262 };
263 
264 /// Constructs a [`Cmd`] from the given string.
265 #[macro_export]
266 macro_rules! cmd {
267     ($cmd:tt) => {{
268         #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)]
269         format_args!($cmd);
270         use $crate::Cmd as __CMD;
271         let cmd: $crate::Cmd = $crate::__cmd!(__CMD $cmd);
272         cmd
273     }};
274 }
275 
276 /// A command.
277 #[must_use]
278 #[derive(Debug)]
279 pub struct Cmd {
280     args: Vec<OsString>,
281     stdin_contents: Option<Vec<u8>>,
282     ignore_status: bool,
283     echo_cmd: bool,
284     secret: bool,
285     env_changes: Vec<EnvChange>,
286     ignore_stdout: bool,
287     ignore_stderr: bool,
288 }
289 
290 impl fmt::Display for Cmd {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result291     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292         if !self.secret {
293             let mut space = "";
294             for arg in &self.args {
295                 write!(f, "{}", space)?;
296                 space = " ";
297 
298                 let arg = arg.to_string_lossy();
299                 if arg.chars().any(|it| it.is_ascii_whitespace()) {
300                     write!(f, "\"{}\"", arg.escape_default())?
301                 } else {
302                     write!(f, "{}", arg)?
303                 };
304             }
305         } else {
306             write!(f, "<secret>")?;
307         }
308         Ok(())
309     }
310 }
311 
312 impl From<Cmd> for std::process::Command {
from(cmd: Cmd) -> Self313     fn from(cmd: Cmd) -> Self {
314         cmd.command()
315     }
316 }
317 
318 impl Cmd {
319     /// Creates a new `Cmd` that executes the given `program`.
new(program: impl AsRef<Path>) -> Cmd320     pub fn new(program: impl AsRef<Path>) -> Cmd {
321         Cmd::_new(program.as_ref())
322     }
_new(program: &Path) -> Cmd323     fn _new(program: &Path) -> Cmd {
324         Cmd {
325             args: vec![program.as_os_str().to_owned()],
326             stdin_contents: None,
327             ignore_status: false,
328             echo_cmd: true,
329             secret: false,
330             env_changes: vec![],
331             ignore_stdout: false,
332             ignore_stderr: false,
333         }
334     }
335 
336     /// Pushes an argument onto this `Cmd`.
arg(mut self, arg: impl AsRef<OsStr>) -> Cmd337     pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Cmd {
338         self._arg(arg.as_ref());
339         self
340     }
341 
342     /// Pushes the arguments onto this `Cmd`.
args<I>(mut self, args: I) -> Cmd where I: IntoIterator, I::Item: AsRef<OsStr>,343     pub fn args<I>(mut self, args: I) -> Cmd
344     where
345         I: IntoIterator,
346         I::Item: AsRef<OsStr>,
347     {
348         args.into_iter().for_each(|it| self._arg(it.as_ref()));
349         self
350     }
351 
_arg(&mut self, arg: &OsStr)352     fn _arg(&mut self, arg: &OsStr) {
353         self.args.push(arg.to_owned())
354     }
355 
356     /// Equivalent to [`std::process::Command::env`].
env<K, V>(mut self, key: K, val: V) -> Cmd where K: AsRef<OsStr>, V: AsRef<OsStr>,357     pub fn env<K, V>(mut self, key: K, val: V) -> Cmd
358     where
359         K: AsRef<OsStr>,
360         V: AsRef<OsStr>,
361     {
362         self._env_set(key.as_ref(), val.as_ref());
363         self
364     }
365 
_env_set(&mut self, key: &OsStr, val: &OsStr)366     fn _env_set(&mut self, key: &OsStr, val: &OsStr) {
367         self.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned()));
368     }
369 
370     /// Equivalent to [`std::process::Command::envs`].
371     ///
372     /// Note: This does not replace the child process's environment, unless you
373     /// call [`Cmd::env_clear`] first.
envs<I, K, V>(mut self, vars: I) -> Cmd where I: IntoIterator<Item = (K, V)>, K: AsRef<OsStr>, V: AsRef<OsStr>,374     pub fn envs<I, K, V>(mut self, vars: I) -> Cmd
375     where
376         I: IntoIterator<Item = (K, V)>,
377         K: AsRef<OsStr>,
378         V: AsRef<OsStr>,
379     {
380         vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref()));
381         self
382     }
383 
384     /// Equivalent to [`std::process::Command::env_remove`].
env_remove<K>(mut self, key: K) -> Cmd where K: AsRef<OsStr>,385     pub fn env_remove<K>(mut self, key: K) -> Cmd
386     where
387         K: AsRef<OsStr>,
388     {
389         self._env_remove(key.as_ref());
390         self
391     }
392 
_env_remove(&mut self, key: &OsStr)393     fn _env_remove(&mut self, key: &OsStr) {
394         self.env_changes.push(EnvChange::Remove(key.to_owned()));
395     }
396 
397     /// Equivalent to [`std::process::Command::env_clear`].
398     ///
399     /// Note that on Windows some environmental variables are required for
400     /// process spawning. See https://github.com/rust-lang/rust/issues/31259.
env_clear(mut self) -> Cmd401     pub fn env_clear(mut self) -> Cmd {
402         self.env_changes.push(EnvChange::Clear);
403         self
404     }
405 
406     /// Returns a `Cmd` that will ignore the stdout stream. This is equivalent of
407     /// attaching stdout to `/dev/null`.
ignore_stdout(mut self) -> Cmd408     pub fn ignore_stdout(mut self) -> Cmd {
409         self.ignore_stdout = true;
410         self
411     }
412 
413     /// Returns a `Cmd` that will ignore the stderr stream. This is equivalent of
414     /// attaching stderr to `/dev/null`.
ignore_stderr(mut self) -> Cmd415     pub fn ignore_stderr(mut self) -> Cmd {
416         self.ignore_stderr = true;
417         self
418     }
419 
420     #[doc(hidden)]
__extend_arg(mut self, arg: impl AsRef<OsStr>) -> Cmd421     pub fn __extend_arg(mut self, arg: impl AsRef<OsStr>) -> Cmd {
422         self.___extend_arg(arg.as_ref());
423         self
424     }
___extend_arg(&mut self, arg: &OsStr)425     fn ___extend_arg(&mut self, arg: &OsStr) {
426         self.args.last_mut().unwrap().push(arg)
427     }
428 
429     /// Returns a `Cmd` that ignores its exit status.
ignore_status(mut self) -> Cmd430     pub fn ignore_status(mut self) -> Cmd {
431         self.ignore_status = true;
432         self
433     }
434 
435     /// Returns a `Cmd` with the given stdin.
stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd436     pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd {
437         self._stdin(stdin.as_ref());
438         self
439     }
_stdin(&mut self, stdin: &[u8])440     fn _stdin(&mut self, stdin: &[u8]) {
441         self.stdin_contents = Some(stdin.to_vec());
442     }
443 
444     /// Returns a `Cmd` that echoes itself (or not) as specified.
echo_cmd(mut self, echo: bool) -> Cmd445     pub fn echo_cmd(mut self, echo: bool) -> Cmd {
446         self.echo_cmd = echo;
447         self
448     }
449 
450     /// Returns a `Cmd` that is secret (or not) as specified.
451     ///
452     /// If a command is secret, it echoes `<secret>` instead of the program and
453     /// its arguments.
secret(mut self, secret: bool) -> Cmd454     pub fn secret(mut self, secret: bool) -> Cmd {
455         self.secret = secret;
456         self
457     }
458 
459     /// Returns the stdout from running the command.
read(self) -> Result<String>460     pub fn read(self) -> Result<String> {
461         self.read_stream(false)
462     }
463 
464     /// Returns the stderr from running the command.
read_stderr(self) -> Result<String>465     pub fn read_stderr(self) -> Result<String> {
466         self.read_stream(true)
467     }
468 
469     /// Returns a [`std::process::Output`] from running the command.
output(self) -> Result<Output>470     pub fn output(self) -> Result<Output> {
471         match self.output_impl(true, true) {
472             Ok(output) if output.status.success() || self.ignore_status => Ok(output),
473             Ok(output) => Err(CmdErrorKind::NonZeroStatus(output.status).err(self)),
474             Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
475         }
476     }
477 
478     /// Runs the command.
run(self) -> Result<()>479     pub fn run(self) -> Result<()> {
480         let _guard = gsl::read();
481         if self.echo_cmd {
482             eprintln!("$ {}", self);
483         }
484         match self.command().status() {
485             Ok(status) if status.success() || self.ignore_status => Ok(()),
486             Ok(status) => Err(CmdErrorKind::NonZeroStatus(status).err(self)),
487             Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
488         }
489     }
490 
read_stream(self, read_stderr: bool) -> Result<String>491     fn read_stream(self, read_stderr: bool) -> Result<String> {
492         let read_stdout = !read_stderr;
493         match self.output_impl(read_stdout, read_stderr) {
494             Ok(output) if output.status.success() || self.ignore_status => {
495                 let stream = if read_stderr { output.stderr } else { output.stdout };
496                 let mut stream = String::from_utf8(stream)
497                     .map_err(|utf8_err| CmdErrorKind::NonUtf8Output(utf8_err).err(self))?;
498 
499                 if stream.ends_with('\n') {
500                     stream.pop();
501                 }
502                 if stream.ends_with('\r') {
503                     stream.pop();
504                 }
505 
506                 Ok(stream)
507             }
508             Ok(output) => Err(CmdErrorKind::NonZeroStatus(output.status).err(self)),
509             Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
510         }
511     }
512 
output_impl(&self, read_stdout: bool, read_stderr: bool) -> io::Result<Output>513     fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> io::Result<Output> {
514         let mut child = {
515             let _guard = gsl::read();
516             let mut command = self.command();
517 
518             command.stdin(match &self.stdin_contents {
519                 Some(_) => Stdio::piped(),
520                 None => Stdio::null(),
521             });
522 
523             if !self.ignore_stdout {
524                 command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
525             }
526 
527             if !self.ignore_stderr {
528                 command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
529             }
530 
531             command.spawn()?
532         };
533 
534         if let Some(stdin_contents) = &self.stdin_contents {
535             let mut stdin = child.stdin.take().unwrap();
536             stdin.write_all(stdin_contents)?;
537             stdin.flush()?;
538         }
539         child.wait_with_output()
540     }
541 
command(&self) -> std::process::Command542     fn command(&self) -> std::process::Command {
543         let mut res = std::process::Command::new(&self.args[0]);
544         res.args(&self.args[1..]);
545         self.apply_env(&mut res);
546         if self.ignore_stdout {
547             res.stdout(Stdio::null());
548         }
549         if self.ignore_stderr {
550             res.stderr(Stdio::null());
551         }
552         res
553     }
554 
apply_env(&self, cmd: &mut std::process::Command)555     fn apply_env(&self, cmd: &mut std::process::Command) {
556         for change in &self.env_changes {
557             match change {
558                 EnvChange::Clear => cmd.env_clear(),
559                 EnvChange::Remove(key) => cmd.env_remove(key),
560                 EnvChange::Set(key, val) => cmd.env(key, val),
561             };
562         }
563     }
564 }
565 
566 // We just store a list of functions to call on the `Command` — the alternative
567 // would require mirroring the logic that `std::process::Command` (or rather
568 // `sys_common::CommandEnvs`) uses, which is moderately complex, involves
569 // special-casing `PATH`, and plausbly could change.
570 #[derive(Debug)]
571 enum EnvChange {
572     Set(OsString, OsString),
573     Remove(OsString),
574     Clear,
575 }
576