1//! xshell is a swiss-army knife for writing cross-platform "bash" scripts in
2//! Rust.
3//!
4//! It doesn't use the shell directly, but rather re-implements parts of
5//! scripting environment in Rust. The intended use-case is various bits of glue
6//! code, which could be written in bash or python. The original motivation is
7//! [`xtask`](https://github.com/matklad/cargo-xtask) development.
8//!
9//! Here's a quick example:
10//!
11//! ```no_run
12//! use xshell::{Shell, cmd};
13//!
14//! let sh = Shell::new()?;
15//! let branch = "main";
16//! let commit_hash = cmd!(sh, "git rev-parse {branch}").read()?;
17//! # Ok::<(), xshell::Error>(())
18//! ```
19//!
20//! **Goals:**
21//!
22//! * Ergonomics and DWIM ("do what I mean"): `cmd!` macro supports
23//! interpolation, writing to a file automatically creates parent directories,
24//! etc.
25//! * Reliability: no [shell injection] by construction, good error messages
26//! with file paths, non-zero exit status is an error, independence of the
27//! host environment, etc.
28//! * Frugality: fast compile times, few dependencies, low-tech API.
29//!
30//! # Guide
31//!
32//! For a short API overview, let's implement a script to clone a github
33//! repository and publish it as a crates.io crate. The script will do the
34//! following:
35//!
36//! 1. Clone the repository.
37//! 2. `cd` into the repository's directory.
38//! 3. Run the tests.
39//! 4. Create a git tag using a version from `Cargo.toml`.
40//! 5. Publish the crate with an optional `--dry-run`.
41//!
42//! Start with the following skeleton:
43//!
44//! ```no_run
45//! use xshell::{cmd, Shell};
46//!
47//! fn main() -> anyhow::Result<()> {
48//! let sh = Shell::new()?;
49//!
50//! Ok(())
51//! }
52//! ```
53//!
54//! Only two imports are needed -- the [`Shell`] struct the and [`cmd!`] macro.
55//! By convention, an instance of a [`Shell`] is stored in a variable named
56//! `sh`. All the API is available as methods, so a short name helps here. For
57//! "scripts", the [`anyhow`](https://docs.rs/anyhow) crate is a great choice
58//! for an error-handling library.
59//!
60//! Next, clone the repository:
61//!
62//! ```no_run
63//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
64//! cmd!(sh, "git clone https://github.com/matklad/xshell.git").run()?;
65//! # Ok::<(), xshell::Error>(())
66//! ```
67//!
68//! The [`cmd!`] macro provides a convenient syntax for creating a command --
69//! the [`Cmd`] struct. The [`Cmd::run`] method runs the command as if you
70//! typed it into the shell. The whole program outputs:
71//!
72//! ```console
73//! $ git clone https://github.com/matklad/xshell.git
74//! Cloning into 'xshell'...
75//! remote: Enumerating objects: 676, done.
76//! remote: Counting objects: 100% (220/220), done.
77//! remote: Compressing objects: 100% (123/123), done.
78//! remote: Total 676 (delta 106), reused 162 (delta 76), pack-reused 456
79//! Receiving objects: 100% (676/676), 136.80 KiB | 222.00 KiB/s, done.
80//! Resolving deltas: 100% (327/327), done.
81//! ```
82//!
83//! Note that the command itself is echoed to stderr (the `$ git ...` bit in the
84//! output). You can use [`Cmd::quiet`] to override this behavior:
85//!
86//! ```no_run
87//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
88//! cmd!(sh, "git clone https://github.com/matklad/xshell.git")
89//! .quiet()
90//! .run()?;
91//! # Ok::<(), xshell::Error>(())
92//! ```
93//!
94//! To make the code more general, let's use command interpolation to extract
95//! the username and the repository:
96//!
97//! ```no_run
98//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
99//! let user = "matklad";
100//! let repo = "xshell";
101//! cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?;
102//! # Ok::<(), xshell::Error>(())
103//! ```
104//!
105//! Note that the `cmd!` macro parses the command string at compile time, so you
106//! don't have to worry about escaping the arguments. For example, the following
107//! command "touches" a single file whose name is `contains a space`:
108//!
109//! ```no_run
110//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
111//! let file = "contains a space";
112//! cmd!(sh, "touch {file}").run()?;
113//! # Ok::<(), xshell::Error>(())
114//! ```
115//!
116//! Next, `cd` into the folder you have just cloned:
117//!
118//! ```no_run
119//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
120//! # let repo = "xshell";
121//! sh.change_dir(repo);
122//! ```
123//!
124//! Each instance of [`Shell`] has a current directory, which is independent of
125//! the process-wide [`std::env::current_dir`]. The same applies to the
126//! environment.
127//!
128//! Next, run the tests:
129//!
130//! ```no_run
131//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
132//! let test_args = ["-Zunstable-options", "--report-time"];
133//! cmd!(sh, "cargo test -- {test_args...}").run()?;
134//! # Ok::<(), xshell::Error>(())
135//! ```
136//!
137//! Note how the so-called splat syntax (`...`) is used to interpolate an
138//! iterable of arguments.
139//!
140//! Next, read the Cargo.toml so that we can fetch crate' declared version:
141//!
142//! ```no_run
143//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
144//! let manifest = sh.read_file("Cargo.toml")?;
145//! # Ok::<(), xshell::Error>(())
146//! ```
147//!
148//! [`Shell::read_file`] works like [`std::fs::read_to_string`], but paths are
149//! relative to the current directory of the [`Shell`]. Unlike [`std::fs`],
150//! error messages are much more useful. For example, if there isn't a
151//! `Cargo.toml` in the repository, the error message is:
152//!
153//! ```text
154//! Error: failed to read file `xshell/Cargo.toml`: no such file or directory (os error 2)
155//! ```
156//!
157//! `xshell` doesn't implement string processing utils like `grep`, `sed` or
158//! `awk` -- there's no need to, built-in language features work fine, and it's
159//! always possible to pull extra functionality from crates.io.
160//!
161//! To extract the `version` field from Cargo.toml, [`str::split_once`] is
162//! enough:
163//!
164//! ```no_run
165//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
166//! let manifest = sh.read_file("Cargo.toml")?;
167//! let version = manifest
168//! .split_once("version = \"")
169//! .and_then(|it| it.1.split_once('\"'))
170//! .map(|it| it.0)
171//! .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?;
172//!
173//! cmd!(sh, "git tag {version}").run()?;
174//! # Ok::<(), anyhow::Error>(())
175//! ```
176//!
177//! The splat (`...`) syntax works with any iterable, and in Rust options are
178//! iterable. This means that `...` can be used to implement optional arguments.
179//! For example, here's how to pass `--dry-run` when *not* running in CI:
180//!
181//! ```no_run
182//! # use xshell::{Shell, cmd}; let sh = Shell::new().unwrap();
183//! let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") };
184//! cmd!(sh, "cargo publish {dry_run...}").run()?;
185//! # Ok::<(), xshell::Error>(())
186//! ```
187//!
188//! Putting everything altogether, here's the whole script:
189//!
190//! ```no_run
191//! use xshell::{cmd, Shell};
192//!
193//! fn main() -> anyhow::Result<()> {
194//! let sh = Shell::new()?;
195//!
196//! let user = "matklad";
197//! let repo = "xshell";
198//! cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?;
199//! sh.change_dir(repo);
200//!
201//! let test_args = ["-Zunstable-options", "--report-time"];
202//! cmd!(sh, "cargo test -- {test_args...}").run()?;
203//!
204//! let manifest = sh.read_file("Cargo.toml")?;
205//! let version = manifest
206//! .split_once("version = \"")
207//! .and_then(|it| it.1.split_once('\"'))
208//! .map(|it| it.0)
209//! .ok_or_else(|| anyhow::format_err!("can't find version field in the manifest"))?;
210//!
211//! cmd!(sh, "git tag {version}").run()?;
212//!
213//! let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") };
214//! cmd!(sh, "cargo publish {dry_run...}").run()?;
215//!
216//! Ok(())
217//! }
218//! ```
219//!
220//! `xshell` itself uses a similar script to automatically publish oneself to
221//! crates.io when the version in Cargo.toml changes:
222//!
223//! <https://github.com/matklad/xshell/blob/master/examples/ci.rs>
224//!
225//! # Maintenance
226//!
227//! Minimum Supported Rust Version: 1.59.0. MSRV bump is not considered semver
228//! breaking. MSRV is updated conservatively.
229//!
230//! The crate isn't comprehensive yet, but this is a goal. You are hereby
231//! encouraged to submit PRs with missing functionality!
232//!
233//! # Related Crates
234//!
235//! [`duct`] is a crate for heavy-duty process herding, with support for
236//! pipelines.
237//!
238//! Most of what this crate provides can be open-coded using
239//! [`std::process::Command`] and [`std::fs`]. If you only need to spawn a
240//! single process, using `std` is probably better (but don't forget to check
241//! the exit status!).
242//!
243//! [`duct`]: https://github.com/oconnor663/duct.rs
244//! [shell injection]:
245//! https://en.wikipedia.org/wiki/Code_injection#Shell_injection
246//!
247//! # Implementation Notes
248//!
249//! The design is heavily inspired by the Julia language:
250//!
251//! * [Shelling Out
252//! Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/)
253//! * [Put This In Your
254//! Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/)
255//! * [Running External
256//! Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/)
257//! * [Filesystem](https://docs.julialang.org/en/v1/base/file/)
258//!
259//! Smaller influences are the [`duct`] crate and Ruby's
260//! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html)
261//! module.
262//!
263//! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on
264//! helper libraries, so the fixed-cost impact on compile times is moderate.
265//! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second.
266//! Equivalent program using only `std::process::Command` compiles in 0.25
267//! seconds.
268//!
269//! To make IDEs infer correct types without expanding proc-macro, it is wrapped
270//! into a declarative macro which supplies type hints.
271
272#![deny(missing_debug_implementations)]
273#![deny(missing_docs)]
274#![deny(rust_2018_idioms)]
275
276mod error;
277
278use std::{
279 cell::RefCell,
280 collections::HashMap,
281 env::{self, current_dir, VarError},
282 ffi::{OsStr, OsString},
283 fmt, fs,
284 io::{self, ErrorKind, Write},
285 mem,
286 path::{Path, PathBuf},
287 process::{Command, ExitStatus, Output, Stdio},
288 sync::atomic::{AtomicUsize, Ordering},
289};
290
291pub use crate::error::{Error, Result};
292#[doc(hidden)]
293pub use xshell_macros::__cmd;
294
295/// Constructs a [`Cmd`] from the given string.
296///
297/// # Examples
298///
299/// Basic:
300///
301/// ```no_run
302/// # use xshell::{cmd, Shell};
303/// let sh = Shell::new()?;
304/// cmd!(sh, "echo hello world").run()?;
305/// # Ok::<(), xshell::Error>(())
306/// ```
307///
308/// Interpolation:
309///
310/// ```
311/// # use xshell::{cmd, Shell}; let sh = Shell::new()?;
312/// let greeting = "hello world";
313/// let c = cmd!(sh, "echo {greeting}");
314/// assert_eq!(c.to_string(), r#"echo "hello world""#);
315///
316/// let c = cmd!(sh, "echo '{greeting}'");
317/// assert_eq!(c.to_string(), r#"echo {greeting}"#);
318///
319/// let c = cmd!(sh, "echo {greeting}!");
320/// assert_eq!(c.to_string(), r#"echo "hello world!""#);
321///
322/// let c = cmd!(sh, "echo 'spaces '{greeting}' around'");
323/// assert_eq!(c.to_string(), r#"echo "spaces hello world around""#);
324///
325/// # Ok::<(), xshell::Error>(())
326/// ```
327///
328/// Splat interpolation:
329///
330/// ```
331/// # use xshell::{cmd, Shell}; let sh = Shell::new()?;
332/// let args = ["hello", "world"];
333/// let c = cmd!(sh, "echo {args...}");
334/// assert_eq!(c.to_string(), r#"echo hello world"#);
335///
336/// let arg1: Option<&str> = Some("hello");
337/// let arg2: Option<&str> = None;
338/// let c = cmd!(sh, "echo {arg1...} {arg2...}");
339/// assert_eq!(c.to_string(), r#"echo hello"#);
340/// # Ok::<(), xshell::Error>(())
341/// ```
342#[macro_export]
343macro_rules! cmd {
344 ($sh:expr, $cmd:literal) => {{
345 #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)]
346 format_args!($cmd);
347 let f = |prog| $sh.cmd(prog);
348 let cmd: $crate::Cmd = $crate::__cmd!(f $cmd);
349 cmd
350 }};
351}
352
353/// A `Shell` is the main API entry point.
354///
355/// Almost all of the crate's functionality is available as methods of the
356/// `Shell` object.
357///
358/// `Shell` is a stateful object. It maintains a logical working directory and
359/// an environment map. They are independent from process's
360/// [`std::env::current_dir`] and [`std::env::var`], and only affect paths and
361/// commands passed to the [`Shell`].
362///
363///
364/// By convention, variable holding the shell is named `sh`.
365///
366/// # Example
367///
368/// ```no_run
369/// use xshell::{cmd, Shell};
370///
371/// let sh = Shell::new()?;
372/// let _d = sh.push_dir("./target");
373/// let cwd = sh.current_dir();
374/// cmd!(sh, "echo current dir is {cwd}").run()?;
375///
376/// let process_cwd = std::env::current_dir().unwrap();
377/// assert_eq!(cwd, process_cwd.join("./target"));
378/// # Ok::<(), xshell::Error>(())
379/// ```
380#[derive(Debug)]
381pub struct Shell {
382 cwd: RefCell<PathBuf>,
383 env: RefCell<HashMap<OsString, OsString>>,
384}
385
386impl std::panic::UnwindSafe for Shell {}
387impl std::panic::RefUnwindSafe for Shell {}
388
389impl Shell {
390 /// Creates a new [`Shell`].
391 ///
392 /// Fails if [`std::env::current_dir`] returns an error.
393 pub fn new() -> Result<Shell> {
394 let cwd = current_dir().map_err(|err| Error::new_current_dir(err, None))?;
395 let cwd = RefCell::new(cwd);
396 let env = RefCell::new(HashMap::new());
397 Ok(Shell { cwd, env })
398 }
399
400 // region:env
401 /// Returns the working directory for this [`Shell`].
402 ///
403 /// All relative paths are interpreted relative to this directory, rather
404 /// than [`std::env::current_dir`].
405 #[doc(alias = "pwd")]
406 pub fn current_dir(&self) -> PathBuf {
407 self.cwd.borrow().clone()
408 }
409
410 /// Changes the working directory for this [`Shell`].
411 ///
412 /// Note that this doesn't affect [`std::env::current_dir`].
413 #[doc(alias = "pwd")]
414 pub fn change_dir<P: AsRef<Path>>(&self, dir: P) {
415 self._change_dir(dir.as_ref())
416 }
417 fn _change_dir(&self, dir: &Path) {
418 let dir = self.path(dir);
419 *self.cwd.borrow_mut() = dir;
420 }
421
422 /// Temporary changes the working directory of this [`Shell`].
423 ///
424 /// Returns a RAII guard which reverts the working directory to the old
425 /// value when dropped.
426 ///
427 /// Note that this doesn't affect [`std::env::current_dir`].
428 #[doc(alias = "pushd")]
429 pub fn push_dir<P: AsRef<Path>>(&self, path: P) -> PushDir<'_> {
430 self._push_dir(path.as_ref())
431 }
432 fn _push_dir(&self, path: &Path) -> PushDir<'_> {
433 let path = self.path(path);
434 PushDir::new(self, path)
435 }
436
437 /// Fetches the environmental variable `key` for this [`Shell`].
438 ///
439 /// Returns an error if the variable is not set, or set to a non-utf8 value.
440 ///
441 /// Environment of the [`Shell`] affects all commands spawned via this
442 /// shell.
443 pub fn var<K: AsRef<OsStr>>(&self, key: K) -> Result<String> {
444 self._var(key.as_ref())
445 }
446 fn _var(&self, key: &OsStr) -> Result<String> {
447 match self._var_os(key) {
448 Some(it) => it.into_string().map_err(VarError::NotUnicode),
449 None => Err(VarError::NotPresent),
450 }
451 .map_err(|err| Error::new_var(err, key.to_os_string()))
452 }
453
454 /// Fetches the environmental variable `key` for this [`Shell`] as
455 /// [`OsString`] Returns [`None`] if the variable is not set.
456 ///
457 /// Environment of the [`Shell`] affects all commands spawned via this
458 /// shell.
459 pub fn var_os<K: AsRef<OsStr>>(&self, key: K) -> Option<OsString> {
460 self._var_os(key.as_ref())
461 }
462 fn _var_os(&self, key: &OsStr) -> Option<OsString> {
463 self.env.borrow().get(key).cloned().or_else(|| env::var_os(key))
464 }
465
466 /// Sets the value of `key` environment variable for this [`Shell`] to
467 /// `val`.
468 ///
469 /// Note that this doesn't affect [`std::env::var`].
470 pub fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) {
471 self._set_var(key.as_ref(), val.as_ref())
472 }
473 fn _set_var(&self, key: &OsStr, val: &OsStr) {
474 self.env.borrow_mut().insert(key.to_os_string(), val.to_os_string());
475 }
476
477 /// Temporary sets the value of `key` environment variable for this
478 /// [`Shell`] to `val`.
479 ///
480 /// Returns a RAII guard which restores the old environment when dropped.
481 ///
482 /// Note that this doesn't affect [`std::env::var`].
483 pub fn push_env<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) -> PushEnv<'_> {
484 self._push_env(key.as_ref(), val.as_ref())
485 }
486 fn _push_env(&self, key: &OsStr, val: &OsStr) -> PushEnv<'_> {
487 PushEnv::new(self, key.to_os_string(), val.to_os_string())
488 }
489 // endregion:env
490
491 // region:fs
492 /// Read the entire contents of a file into a string.
493 #[doc(alias = "cat")]
494 pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<String> {
495 self._read_file(path.as_ref())
496 }
497 fn _read_file(&self, path: &Path) -> Result<String> {
498 let path = self.path(path);
499 fs::read_to_string(&path).map_err(|err| Error::new_read_file(err, path))
500 }
501
502 /// Read the entire contents of a file into a vector of bytes.
503 pub fn read_binary_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
504 self._read_binary_file(path.as_ref())
505 }
506 fn _read_binary_file(&self, path: &Path) -> Result<Vec<u8>> {
507 let path = self.path(path);
508 fs::read(&path).map_err(|err| Error::new_read_file(err, path))
509 }
510
511 /// Returns a sorted list of paths directly contained in the directory at
512 /// `path`.
513 #[doc(alias = "ls")]
514 pub fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Vec<PathBuf>> {
515 self._read_dir(path.as_ref())
516 }
517 fn _read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
518 let path = self.path(path);
519 let mut res = Vec::new();
520 || -> _ {
521 for entry in fs::read_dir(&path)? {
522 let entry = entry?;
523 res.push(entry.path())
524 }
525 Ok(())
526 }()
527 .map_err(|err| Error::new_read_dir(err, path))?;
528 res.sort();
529 Ok(res)
530 }
531
532 /// Write a slice as the entire contents of a file.
533 ///
534 /// This function will create the file and all intermediate directories if
535 /// they don't exist.
536 // TODO: probably want to make this an atomic rename write?
537 pub fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, contents: C) -> Result<()> {
538 self._write_file(path.as_ref(), contents.as_ref())
539 }
540 fn _write_file(&self, path: &Path, contents: &[u8]) -> Result<()> {
541 let path = self.path(path);
542 if let Some(p) = path.parent() {
543 self.create_dir(p)?;
544 }
545 fs::write(&path, contents).map_err(|err| Error::new_write_file(err, path))
546 }
547
548 /// Copies `src` into `dst`.
549 ///
550 /// `src` must be a file, but `dst` need not be. If `dst` is an existing
551 /// directory, `src` will be copied into a file in the `dst` directory whose
552 /// name is same as that of `src`.
553 ///
554 /// Otherwise, `dst` is a file or does not exist, and `src` will be copied into
555 /// it.
556 #[doc(alias = "cp")]
557 pub fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> {
558 self._copy_file(src.as_ref(), dst.as_ref())
559 }
560 fn _copy_file(&self, src: &Path, dst: &Path) -> Result<()> {
561 let src = self.path(src);
562 let dst = self.path(dst);
563 let dst = dst.as_path();
564 let mut _tmp;
565 let mut dst = dst;
566 if dst.is_dir() {
567 if let Some(file_name) = src.file_name() {
568 _tmp = dst.join(file_name);
569 dst = &_tmp;
570 }
571 }
572 std::fs::copy(&src, dst)
573 .map_err(|err| Error::new_copy_file(err, src.to_path_buf(), dst.to_path_buf()))?;
574 Ok(())
575 }
576
577 /// Hardlinks `src` to `dst`.
578 #[doc(alias = "ln")]
579 pub fn hard_link<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> {
580 self._hard_link(src.as_ref(), dst.as_ref())
581 }
582 fn _hard_link(&self, src: &Path, dst: &Path) -> Result<()> {
583 let src = self.path(src);
584 let dst = self.path(dst);
585 fs::hard_link(&src, &dst).map_err(|err| Error::new_hard_link(err, src, dst))
586 }
587
588 /// Creates the specified directory.
589 ///
590 /// All intermediate directories will also be created.
591 #[doc(alias("mkdir_p", "mkdir"))]
592 pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
593 self._create_dir(path.as_ref())
594 }
595 fn _create_dir(&self, path: &Path) -> Result<PathBuf> {
596 let path = self.path(path);
597 match fs::create_dir_all(&path) {
598 Ok(()) => Ok(path),
599 Err(err) => Err(Error::new_create_dir(err, path)),
600 }
601 }
602
603 /// Creates an empty named world-readable temporary directory.
604 ///
605 /// Returns a [`TempDir`] RAII guard with the path to the directory. When
606 /// dropped, the temporary directory and all of its contents will be
607 /// removed.
608 ///
609 /// Note that this is an **insecure method** -- any other process on the
610 /// system will be able to read the data.
611 #[doc(alias = "mktemp")]
612 pub fn create_temp_dir(&self) -> Result<TempDir> {
613 let base = std::env::temp_dir();
614 self.create_dir(&base)?;
615
616 static CNT: AtomicUsize = AtomicUsize::new(0);
617
618 let mut n_try = 0u32;
619 loop {
620 let cnt = CNT.fetch_add(1, Ordering::Relaxed);
621 let path = base.join(format!("xshell-tmp-dir-{}", cnt));
622 match fs::create_dir(&path) {
623 Ok(()) => return Ok(TempDir { path }),
624 Err(err) if n_try == 1024 => return Err(Error::new_create_dir(err, path)),
625 Err(_) => n_try += 1,
626 }
627 }
628 }
629
630 /// Removes the file or directory at the given path.
631 #[doc(alias("rm_rf", "rm"))]
632 pub fn remove_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
633 self._remove_path(path.as_ref())
634 }
635 fn _remove_path(&self, path: &Path) -> Result<(), Error> {
636 let path = self.path(path);
637 match path.metadata() {
638 Ok(meta) => if meta.is_dir() { remove_dir_all(&path) } else { fs::remove_file(&path) }
639 .map_err(|err| Error::new_remove_path(err, path)),
640 Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
641 Err(err) => Err(Error::new_remove_path(err, path)),
642 }
643 }
644
645 /// Returns whether a file or directory exists at the given path.
646 #[doc(alias("stat"))]
647 pub fn path_exists<P: AsRef<Path>>(&self, path: P) -> bool {
648 self.path(path.as_ref()).exists()
649 }
650 // endregion:fs
651
652 /// Creates a new [`Cmd`] that executes the given `program`.
653 pub fn cmd<P: AsRef<Path>>(&self, program: P) -> Cmd<'_> {
654 // TODO: path lookup?
655 Cmd::new(self, program.as_ref())
656 }
657
658 fn path(&self, p: &Path) -> PathBuf {
659 let cd = self.cwd.borrow();
660 cd.join(p)
661 }
662}
663
664/// RAII guard returned from [`Shell::push_dir`].
665///
666/// Dropping `PushDir` restores the working directory of the [`Shell`] to the
667/// old value.
668#[derive(Debug)]
669#[must_use]
670pub struct PushDir<'a> {
671 old_cwd: PathBuf,
672 shell: &'a Shell,
673}
674
675impl<'a> PushDir<'a> {
676 fn new(shell: &'a Shell, path: PathBuf) -> PushDir<'a> {
677 PushDir { old_cwd: mem::replace(&mut *shell.cwd.borrow_mut(), src:path), shell }
678 }
679}
680
681impl Drop for PushDir<'_> {
682 fn drop(&mut self) {
683 mem::swap(&mut *self.shell.cwd.borrow_mut(), &mut self.old_cwd)
684 }
685}
686
687/// RAII guard returned from [`Shell::push_env`].
688///
689/// Dropping `PushEnv` restores the old value of the environmental variable.
690#[derive(Debug)]
691#[must_use]
692pub struct PushEnv<'a> {
693 key: OsString,
694 old_value: Option<OsString>,
695 shell: &'a Shell,
696}
697
698impl<'a> PushEnv<'a> {
699 fn new(shell: &'a Shell, key: OsString, val: OsString) -> PushEnv<'a> {
700 let old_value: Option = shell.env.borrow_mut().insert(k:key.clone(), v:val);
701 PushEnv { shell, key, old_value }
702 }
703}
704
705impl Drop for PushEnv<'_> {
706 fn drop(&mut self) {
707 let mut env: RefMut<'_, HashMap> = self.shell.env.borrow_mut();
708 let key: OsString = mem::take(&mut self.key);
709 match self.old_value.take() {
710 Some(value: OsString) => {
711 env.insert(k:key, v:value);
712 }
713 None => {
714 env.remove(&key);
715 }
716 }
717 }
718}
719
720/// A builder object for constructing a subprocess.
721///
722/// A [`Cmd`] is usually created with the [`cmd!`] macro. The command exists
723/// within a context of a [`Shell`] and uses its working directory and
724/// environment.
725///
726/// # Example
727///
728/// ```no_run
729/// use xshell::{Shell, cmd};
730///
731/// let sh = Shell::new()?;
732///
733/// let branch = "main";
734/// let cmd = cmd!(sh, "git switch {branch}").quiet().run()?;
735/// # Ok::<(), xshell::Error>(())
736/// ```
737#[derive(Debug)]
738#[must_use]
739pub struct Cmd<'a> {
740 shell: &'a Shell,
741 data: CmdData,
742}
743
744#[derive(Debug, Default, Clone)]
745struct CmdData {
746 prog: PathBuf,
747 args: Vec<OsString>,
748 env_changes: Vec<EnvChange>,
749 ignore_status: bool,
750 quiet: bool,
751 secret: bool,
752 stdin_contents: Option<Vec<u8>>,
753 ignore_stdout: bool,
754 ignore_stderr: bool,
755}
756
757// We just store a list of functions to call on the `Command` — the alternative
758// would require mirroring the logic that `std::process::Command` (or rather
759// `sys_common::CommandEnvs`) uses, which is moderately complex, involves
760// special-casing `PATH`, and plausibly could change.
761#[derive(Debug, Clone)]
762enum EnvChange {
763 Set(OsString, OsString),
764 Remove(OsString),
765 Clear,
766}
767
768impl fmt::Display for Cmd<'_> {
769 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770 fmt::Display::fmt(&self.data, f)
771 }
772}
773
774impl fmt::Display for CmdData {
775 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
776 if self.secret {
777 return write!(f, "<secret>");
778 }
779
780 write!(f, "{}", self.prog.display())?;
781 for arg: &OsString in &self.args {
782 // TODO: this is potentially not copy-paste safe.
783 let arg: Cow<'_, str> = arg.to_string_lossy();
784 if arg.chars().any(|it: char| it.is_ascii_whitespace()) {
785 write!(f, " \"{}\"", arg.escape_default())?
786 } else {
787 write!(f, " {}", arg)?
788 };
789 }
790 Ok(())
791 }
792}
793
794impl From<Cmd<'_>> for Command {
795 fn from(cmd: Cmd<'_>) -> Command {
796 cmd.to_command()
797 }
798}
799
800impl<'a> Cmd<'a> {
801 fn new(shell: &'a Shell, prog: &Path) -> Cmd<'a> {
802 let mut data = CmdData::default();
803 data.prog = prog.to_path_buf();
804 Cmd { shell, data }
805 }
806
807 // region:builder
808 /// Adds an argument to this commands.
809 pub fn arg<P: AsRef<OsStr>>(mut self, arg: P) -> Cmd<'a> {
810 self._arg(arg.as_ref());
811 self
812 }
813 fn _arg(&mut self, arg: &OsStr) {
814 self.data.args.push(arg.to_owned())
815 }
816
817 /// Adds all of the arguments to this command.
818 pub fn args<I>(mut self, args: I) -> Cmd<'a>
819 where
820 I: IntoIterator,
821 I::Item: AsRef<OsStr>,
822 {
823 args.into_iter().for_each(|it| self._arg(it.as_ref()));
824 self
825 }
826
827 #[doc(hidden)]
828 pub fn __extend_arg<P: AsRef<OsStr>>(mut self, arg_fragment: P) -> Cmd<'a> {
829 self.___extend_arg(arg_fragment.as_ref());
830 self
831 }
832 fn ___extend_arg(&mut self, arg_fragment: &OsStr) {
833 match self.data.args.last_mut() {
834 Some(last_arg) => last_arg.push(arg_fragment),
835 None => {
836 let mut prog = mem::take(&mut self.data.prog).into_os_string();
837 prog.push(arg_fragment);
838 self.data.prog = prog.into();
839 }
840 }
841 }
842
843 /// Overrides the value of the environmental variable for this command.
844 pub fn env<K: AsRef<OsStr>, V: AsRef<OsStr>>(mut self, key: K, val: V) -> Cmd<'a> {
845 self._env_set(key.as_ref(), val.as_ref());
846 self
847 }
848
849 fn _env_set(&mut self, key: &OsStr, val: &OsStr) {
850 self.data.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned()));
851 }
852
853 /// Overrides the values of specified environmental variables for this
854 /// command.
855 pub fn envs<I, K, V>(mut self, vars: I) -> Cmd<'a>
856 where
857 I: IntoIterator<Item = (K, V)>,
858 K: AsRef<OsStr>,
859 V: AsRef<OsStr>,
860 {
861 vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref()));
862 self
863 }
864
865 /// Removes the environment variable from this command.
866 pub fn env_remove<K: AsRef<OsStr>>(mut self, key: K) -> Cmd<'a> {
867 self._env_remove(key.as_ref());
868 self
869 }
870 fn _env_remove(&mut self, key: &OsStr) {
871 self.data.env_changes.push(EnvChange::Remove(key.to_owned()));
872 }
873
874 /// Removes all of the environment variables from this command.
875 pub fn env_clear(mut self) -> Cmd<'a> {
876 self.data.env_changes.push(EnvChange::Clear);
877 self
878 }
879
880 /// Don't return an error if command the command exits with non-zero status.
881 ///
882 /// By default, non-zero exit status is considered an error.
883 pub fn ignore_status(mut self) -> Cmd<'a> {
884 self.set_ignore_status(true);
885 self
886 }
887 /// Controls whether non-zero exit status is considered an error.
888 pub fn set_ignore_status(&mut self, yes: bool) {
889 self.data.ignore_status = yes;
890 }
891
892 /// Don't echo the command itself to stderr.
893 ///
894 /// By default, the command itself will be printed to stderr when executed via [`Cmd::run`].
895 pub fn quiet(mut self) -> Cmd<'a> {
896 self.set_quiet(true);
897 self
898 }
899 /// Controls whether the command itself is printed to stderr.
900 pub fn set_quiet(&mut self, yes: bool) {
901 self.data.quiet = yes;
902 }
903
904 /// Marks the command as secret.
905 ///
906 /// If a command is secret, it echoes `<secret>` instead of the program and
907 /// its arguments, even in error messages.
908 pub fn secret(mut self) -> Cmd<'a> {
909 self.set_secret(true);
910 self
911 }
912 /// Controls whether the command is secret.
913 pub fn set_secret(&mut self, yes: bool) {
914 self.data.secret = yes;
915 }
916
917 /// Pass the given slice to the standard input of the spawned process.
918 pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd<'a> {
919 self._stdin(stdin.as_ref());
920 self
921 }
922 fn _stdin(&mut self, stdin: &[u8]) {
923 self.data.stdin_contents = Some(stdin.to_vec());
924 }
925
926 /// Ignores the standard output stream of the process.
927 ///
928 /// This is equivalent to redirecting stdout to `/dev/null`. By default, the
929 /// stdout is inherited or captured.
930 pub fn ignore_stdout(mut self) -> Cmd<'a> {
931 self.set_ignore_stdout(true);
932 self
933 }
934 /// Controls whether the standard output is ignored.
935 pub fn set_ignore_stdout(&mut self, yes: bool) {
936 self.data.ignore_stdout = yes;
937 }
938
939 /// Ignores the standard output stream of the process.
940 ///
941 /// This is equivalent redirecting stderr to `/dev/null`. By default, the
942 /// stderr is inherited or captured.
943 pub fn ignore_stderr(mut self) -> Cmd<'a> {
944 self.set_ignore_stderr(true);
945 self
946 }
947 /// Controls whether the standard error is ignored.
948 pub fn set_ignore_stderr(&mut self, yes: bool) {
949 self.data.ignore_stderr = yes;
950 }
951 // endregion:builder
952
953 // region:running
954 /// Runs the command.
955 ///
956 /// By default the command itself is echoed to stderr, its standard streams
957 /// are inherited, and non-zero return code is considered an error. These
958 /// behaviors can be overridden by using various builder methods of the [`Cmd`].
959 pub fn run(&self) -> Result<()> {
960 if !self.data.quiet {
961 eprintln!("$ {}", self);
962 }
963 self.output_impl(false, false).map(|_| ())
964 }
965
966 /// Run the command and return its stdout as a string.
967 pub fn read(&self) -> Result<String> {
968 self.read_stream(false)
969 }
970
971 /// Run the command and return its stderr as a string.
972 pub fn read_stderr(&self) -> Result<String> {
973 self.read_stream(true)
974 }
975
976 /// Run the command and return its output.
977 pub fn output(&self) -> Result<Output> {
978 self.output_impl(true, true)
979 }
980 // endregion:running
981
982 fn read_stream(&self, read_stderr: bool) -> Result<String> {
983 let read_stdout = !read_stderr;
984 let output = self.output_impl(read_stdout, read_stderr)?;
985 self.check_status(output.status)?;
986
987 let stream = if read_stderr { output.stderr } else { output.stdout };
988 let mut stream = String::from_utf8(stream).map_err(|err| Error::new_cmd_utf8(self, err))?;
989
990 if stream.ends_with('\n') {
991 stream.pop();
992 }
993 if stream.ends_with('\r') {
994 stream.pop();
995 }
996
997 Ok(stream)
998 }
999
1000 fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> Result<Output> {
1001 let mut child = {
1002 let mut command = self.to_command();
1003
1004 if !self.data.ignore_stdout {
1005 command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
1006 }
1007 if !self.data.ignore_stderr {
1008 command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
1009 }
1010
1011 command.stdin(match &self.data.stdin_contents {
1012 Some(_) => Stdio::piped(),
1013 None => Stdio::null(),
1014 });
1015
1016 command.spawn().map_err(|err| {
1017 // Try to determine whether the command failed because the current
1018 // directory does not exist. Return an appropriate error in such a
1019 // case.
1020 if matches!(err.kind(), io::ErrorKind::NotFound) {
1021 let cwd = self.shell.cwd.borrow();
1022 if let Err(err) = cwd.metadata() {
1023 return Error::new_current_dir(err, Some(cwd.clone()));
1024 }
1025 }
1026 Error::new_cmd_io(self, err)
1027 })?
1028 };
1029
1030 let mut io_thread = None;
1031 if let Some(stdin_contents) = self.data.stdin_contents.clone() {
1032 let mut stdin = child.stdin.take().unwrap();
1033 io_thread = Some(std::thread::spawn(move || {
1034 stdin.write_all(&stdin_contents)?;
1035 stdin.flush()
1036 }));
1037 }
1038 let out_res = child.wait_with_output();
1039 let err_res = io_thread.map(|it| it.join().unwrap());
1040 let output = out_res.map_err(|err| Error::new_cmd_io(self, err))?;
1041 if let Some(err_res) = err_res {
1042 err_res.map_err(|err| Error::new_cmd_stdin(self, err))?;
1043 }
1044 self.check_status(output.status)?;
1045 Ok(output)
1046 }
1047
1048 fn to_command(&self) -> Command {
1049 let mut res = Command::new(&self.data.prog);
1050 res.current_dir(self.shell.current_dir());
1051 res.args(&self.data.args);
1052
1053 for (key, val) in &*self.shell.env.borrow() {
1054 res.env(key, val);
1055 }
1056 for change in &self.data.env_changes {
1057 match change {
1058 EnvChange::Clear => res.env_clear(),
1059 EnvChange::Remove(key) => res.env_remove(key),
1060 EnvChange::Set(key, val) => res.env(key, val),
1061 };
1062 }
1063
1064 if self.data.ignore_stdout {
1065 res.stdout(Stdio::null());
1066 }
1067
1068 if self.data.ignore_stderr {
1069 res.stderr(Stdio::null());
1070 }
1071
1072 res
1073 }
1074
1075 fn check_status(&self, status: ExitStatus) -> Result<()> {
1076 if status.success() || self.data.ignore_status {
1077 return Ok(());
1078 }
1079 Err(Error::new_cmd_status(self, status))
1080 }
1081}
1082
1083/// A temporary directory.
1084///
1085/// This is a RAII object which will remove the underlying temporary directory
1086/// when dropped.
1087#[derive(Debug)]
1088#[must_use]
1089pub struct TempDir {
1090 path: PathBuf,
1091}
1092
1093impl TempDir {
1094 /// Returns the path to the underlying temporary directory.
1095 pub fn path(&self) -> &Path {
1096 &self.path
1097 }
1098}
1099
1100impl Drop for TempDir {
1101 fn drop(&mut self) {
1102 let _ = remove_dir_all(&self.path);
1103 }
1104}
1105
1106#[cfg(not(windows))]
1107fn remove_dir_all(path: &Path) -> io::Result<()> {
1108 std::fs::remove_dir_all(path)
1109}
1110
1111#[cfg(windows)]
1112fn remove_dir_all(path: &Path) -> io::Result<()> {
1113 for _ in 0..99 {
1114 if fs::remove_dir_all(path).is_ok() {
1115 return Ok(());
1116 }
1117 std::thread::sleep(std::time::Duration::from_millis(10))
1118 }
1119 fs::remove_dir_all(path)
1120}
1121