xshell/
lib.rs

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.63.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//! The [`dax`](https://github.com/dsherret/dax) library for Deno shares the overall philosophy with
248//! `xshell`, but is much more thorough and complete. If you don't need Rust, use `dax`.
249//!
250//! # Implementation Notes
251//!
252//! The design is heavily inspired by the Julia language:
253//!
254//! * [Shelling Out
255//!   Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/)
256//! * [Put This In Your
257//!   Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/)
258//! * [Running External
259//!   Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/)
260//! * [Filesystem](https://docs.julialang.org/en/v1/base/file/)
261//!
262//! Smaller influences are the [`duct`] crate and Ruby's
263//! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html)
264//! module.
265//!
266//! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on
267//! helper libraries, so the fixed-cost impact on compile times is moderate.
268//! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second.
269//! Equivalent program using only `std::process::Command` compiles in 0.25
270//! seconds.
271//!
272//! To make IDEs infer correct types without expanding proc-macro, it is wrapped
273//! into a declarative macro which supplies type hints.
274
275#![deny(missing_debug_implementations)]
276#![deny(missing_docs)]
277#![deny(rust_2018_idioms)]
278
279mod error;
280
281use std::{
282    cell::RefCell,
283    collections::HashMap,
284    env::{self, current_dir, VarError},
285    ffi::{OsStr, OsString},
286    fmt, fs,
287    io::{self, ErrorKind, Write},
288    mem,
289    path::{Path, PathBuf},
290    process::{Command, ExitStatus, Output, Stdio},
291    sync::atomic::{AtomicUsize, Ordering},
292};
293
294pub use crate::error::{Error, Result};
295#[doc(hidden)]
296pub use xshell_macros::__cmd;
297
298/// Constructs a [`Cmd`] from the given string.
299///
300/// # Examples
301///
302/// Basic:
303///
304/// ```no_run
305/// # use xshell::{cmd, Shell};
306/// let sh = Shell::new()?;
307/// cmd!(sh, "echo hello world").run()?;
308/// # Ok::<(), xshell::Error>(())
309/// ```
310///
311/// Interpolation:
312///
313/// ```
314/// # use xshell::{cmd, Shell}; let sh = Shell::new()?;
315/// let greeting = "hello world";
316/// let c = cmd!(sh, "echo {greeting}");
317/// assert_eq!(c.to_string(), r#"echo "hello world""#);
318///
319/// let c = cmd!(sh, "echo '{greeting}'");
320/// assert_eq!(c.to_string(), r#"echo {greeting}"#);
321///
322/// let c = cmd!(sh, "echo {greeting}!");
323/// assert_eq!(c.to_string(), r#"echo "hello world!""#);
324///
325/// let c = cmd!(sh, "echo 'spaces '{greeting}' around'");
326/// assert_eq!(c.to_string(), r#"echo "spaces hello world around""#);
327///
328/// # Ok::<(), xshell::Error>(())
329/// ```
330///
331/// Splat interpolation:
332///
333/// ```
334/// # use xshell::{cmd, Shell}; let sh = Shell::new()?;
335/// let args = ["hello", "world"];
336/// let c = cmd!(sh, "echo {args...}");
337/// assert_eq!(c.to_string(), r#"echo hello world"#);
338///
339/// let arg1: Option<&str> = Some("hello");
340/// let arg2: Option<&str> = None;
341/// let c = cmd!(sh, "echo {arg1...} {arg2...}");
342/// assert_eq!(c.to_string(), r#"echo hello"#);
343/// # Ok::<(), xshell::Error>(())
344/// ```
345#[macro_export]
346macro_rules! cmd {
347    ($sh:expr, $cmd:literal) => {{
348        #[cfg(any())] // Trick rust analyzer into highlighting interpolated bits
349        format_args!($cmd);
350        let f = |prog| $sh.cmd(prog);
351        let cmd: $crate::Cmd = $crate::__cmd!(f $cmd);
352        cmd
353    }};
354}
355
356/// A `Shell` is the main API entry point.
357///
358/// Almost all of the crate's functionality is available as methods of the
359/// `Shell` object.
360///
361/// `Shell` is a stateful object. It maintains a logical working directory and
362/// an environment map. They are independent from process's
363/// [`std::env::current_dir`] and [`std::env::var`], and only affect paths and
364/// commands passed to the [`Shell`].
365///
366///
367/// By convention, variable holding the shell is named `sh`.
368///
369/// # Example
370///
371/// ```no_run
372/// use xshell::{cmd, Shell};
373///
374/// let sh = Shell::new()?;
375/// let _d = sh.push_dir("./target");
376/// let cwd = sh.current_dir();
377/// cmd!(sh, "echo current dir is {cwd}").run()?;
378///
379/// let process_cwd = std::env::current_dir().unwrap();
380/// assert_eq!(cwd, process_cwd.join("./target"));
381/// # Ok::<(), xshell::Error>(())
382/// ```
383#[derive(Debug, Clone)]
384pub struct Shell {
385    cwd: RefCell<PathBuf>,
386    env: RefCell<HashMap<OsString, OsString>>,
387}
388
389impl std::panic::UnwindSafe for Shell {}
390impl std::panic::RefUnwindSafe for Shell {}
391
392impl Shell {
393    /// Creates a new [`Shell`].
394    ///
395    /// Fails if [`std::env::current_dir`] returns an error.
396    pub fn new() -> Result<Shell> {
397        let cwd = current_dir().map_err(|err| Error::new_current_dir(err, None))?;
398        let cwd = RefCell::new(cwd);
399        let env = RefCell::new(HashMap::new());
400        Ok(Shell { cwd, env })
401    }
402
403    // region:env
404    /// Returns the working directory for this [`Shell`].
405    ///
406    /// All relative paths are interpreted relative to this directory, rather
407    /// than [`std::env::current_dir`].
408    #[doc(alias = "pwd")]
409    pub fn current_dir(&self) -> PathBuf {
410        self.cwd.borrow().clone()
411    }
412
413    /// Changes the working directory for this [`Shell`].
414    ///
415    /// Note that this doesn't affect [`std::env::current_dir`].
416    #[doc(alias = "pwd")]
417    pub fn change_dir<P: AsRef<Path>>(&self, dir: P) {
418        self._change_dir(dir.as_ref())
419    }
420    fn _change_dir(&self, dir: &Path) {
421        let dir = self.path(dir);
422        *self.cwd.borrow_mut() = dir;
423    }
424
425    /// Temporary changes the working directory of this [`Shell`].
426    ///
427    /// Returns a RAII guard which reverts the working directory to the old
428    /// value when dropped.
429    ///
430    /// Note that this doesn't affect [`std::env::current_dir`].
431    #[doc(alias = "pushd")]
432    pub fn push_dir<P: AsRef<Path>>(&self, path: P) -> PushDir<'_> {
433        self._push_dir(path.as_ref())
434    }
435    fn _push_dir(&self, path: &Path) -> PushDir<'_> {
436        let path = self.path(path);
437        PushDir::new(self, path)
438    }
439
440    /// Fetches the environmental variable `key` for this [`Shell`].
441    ///
442    /// Returns an error if the variable is not set, or set to a non-utf8 value.
443    ///
444    /// Environment of the [`Shell`] affects all commands spawned via this
445    /// shell.
446    pub fn var<K: AsRef<OsStr>>(&self, key: K) -> Result<String> {
447        self._var(key.as_ref())
448    }
449    fn _var(&self, key: &OsStr) -> Result<String> {
450        match self._var_os(key) {
451            Some(it) => it.into_string().map_err(VarError::NotUnicode),
452            None => Err(VarError::NotPresent),
453        }
454        .map_err(|err| Error::new_var(err, key.to_os_string()))
455    }
456
457    /// Fetches the environmental variable `key` for this [`Shell`] as
458    /// [`OsString`] Returns [`None`] if the variable is not set.
459    ///
460    /// Environment of the [`Shell`] affects all commands spawned via this
461    /// shell.
462    pub fn var_os<K: AsRef<OsStr>>(&self, key: K) -> Option<OsString> {
463        self._var_os(key.as_ref())
464    }
465    fn _var_os(&self, key: &OsStr) -> Option<OsString> {
466        self.env.borrow().get(key).cloned().or_else(|| env::var_os(key))
467    }
468
469    /// Sets the value of `key` environment variable for this [`Shell`] to
470    /// `val`.
471    ///
472    /// Note that this doesn't affect [`std::env::var`].
473    pub fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) {
474        self._set_var(key.as_ref(), val.as_ref())
475    }
476    fn _set_var(&self, key: &OsStr, val: &OsStr) {
477        self.env.borrow_mut().insert(key.to_os_string(), val.to_os_string());
478    }
479
480    /// Temporary sets the value of `key` environment variable for this
481    /// [`Shell`] to `val`.
482    ///
483    /// Returns a RAII guard which restores the old environment when dropped.
484    ///
485    /// Note that this doesn't affect [`std::env::var`].
486    pub fn push_env<K: AsRef<OsStr>, V: AsRef<OsStr>>(&self, key: K, val: V) -> PushEnv<'_> {
487        self._push_env(key.as_ref(), val.as_ref())
488    }
489    fn _push_env(&self, key: &OsStr, val: &OsStr) -> PushEnv<'_> {
490        PushEnv::new(self, key.to_os_string(), val.to_os_string())
491    }
492    // endregion:env
493
494    // region:fs
495    /// Read the entire contents of a file into a string.
496    #[doc(alias = "cat")]
497    pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<String> {
498        self._read_file(path.as_ref())
499    }
500    fn _read_file(&self, path: &Path) -> Result<String> {
501        let path = self.path(path);
502        fs::read_to_string(&path).map_err(|err| Error::new_read_file(err, path))
503    }
504
505    /// Read the entire contents of a file into a vector of bytes.
506    pub fn read_binary_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
507        self._read_binary_file(path.as_ref())
508    }
509    fn _read_binary_file(&self, path: &Path) -> Result<Vec<u8>> {
510        let path = self.path(path);
511        fs::read(&path).map_err(|err| Error::new_read_file(err, path))
512    }
513
514    /// Returns a sorted list of paths directly contained in the directory at
515    /// `path`.
516    #[doc(alias = "ls")]
517    pub fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Vec<PathBuf>> {
518        self._read_dir(path.as_ref())
519    }
520    fn _read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
521        let path = self.path(path);
522        let mut res = Vec::new();
523        || -> _ {
524            for entry in fs::read_dir(&path)? {
525                let entry = entry?;
526                res.push(entry.path())
527            }
528            Ok(())
529        }()
530        .map_err(|err| Error::new_read_dir(err, path))?;
531        res.sort();
532        Ok(res)
533    }
534
535    /// Write a slice as the entire contents of a file.
536    ///
537    /// This function will create the file and all intermediate directories if
538    /// they don't exist.
539    // TODO: probably want to make this an atomic rename write?
540    pub fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, contents: C) -> Result<()> {
541        self._write_file(path.as_ref(), contents.as_ref())
542    }
543    fn _write_file(&self, path: &Path, contents: &[u8]) -> Result<()> {
544        let path = self.path(path);
545        if let Some(p) = path.parent() {
546            self.create_dir(p)?;
547        }
548        fs::write(&path, contents).map_err(|err| Error::new_write_file(err, path))
549    }
550
551    /// Copies `src` into `dst`.
552    ///
553    /// `src` must be a file, but `dst` need not be. If `dst` is an existing
554    /// directory, `src` will be copied into a file in the `dst` directory whose
555    /// name is same as that of `src`.
556    ///
557    /// Otherwise, `dst` is a file or does not exist, and `src` will be copied into
558    /// it.
559    #[doc(alias = "cp")]
560    pub fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> {
561        self._copy_file(src.as_ref(), dst.as_ref())
562    }
563    fn _copy_file(&self, src: &Path, dst: &Path) -> Result<()> {
564        let src = self.path(src);
565        let dst = self.path(dst);
566        let dst = dst.as_path();
567        let mut _tmp;
568        let mut dst = dst;
569        if dst.is_dir() {
570            if let Some(file_name) = src.file_name() {
571                _tmp = dst.join(file_name);
572                dst = &_tmp;
573            }
574        }
575        std::fs::copy(&src, dst)
576            .map_err(|err| Error::new_copy_file(err, src.to_path_buf(), dst.to_path_buf()))?;
577        Ok(())
578    }
579
580    /// Hardlinks `src` to `dst`.
581    #[doc(alias = "ln")]
582    pub fn hard_link<S: AsRef<Path>, D: AsRef<Path>>(&self, src: S, dst: D) -> Result<()> {
583        self._hard_link(src.as_ref(), dst.as_ref())
584    }
585    fn _hard_link(&self, src: &Path, dst: &Path) -> Result<()> {
586        let src = self.path(src);
587        let dst = self.path(dst);
588        fs::hard_link(&src, &dst).map_err(|err| Error::new_hard_link(err, src, dst))
589    }
590
591    /// Creates the specified directory.
592    ///
593    /// All intermediate directories will also be created.
594    #[doc(alias("mkdir_p", "mkdir"))]
595    pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
596        self._create_dir(path.as_ref())
597    }
598    fn _create_dir(&self, path: &Path) -> Result<PathBuf> {
599        let path = self.path(path);
600        match fs::create_dir_all(&path) {
601            Ok(()) => Ok(path),
602            Err(err) => Err(Error::new_create_dir(err, path)),
603        }
604    }
605
606    /// Creates an empty named world-readable temporary directory.
607    ///
608    /// Returns a [`TempDir`] RAII guard with the path to the directory. When
609    /// dropped, the temporary directory and all of its contents will be
610    /// removed.
611    ///
612    /// Note that this is an **insecure method** -- any other process on the
613    /// system will be able to read the data.
614    #[doc(alias = "mktemp")]
615    pub fn create_temp_dir(&self) -> Result<TempDir> {
616        let base = std::env::temp_dir();
617        self.create_dir(&base)?;
618
619        static CNT: AtomicUsize = AtomicUsize::new(0);
620
621        let mut n_try = 0u32;
622        loop {
623            let cnt = CNT.fetch_add(1, Ordering::Relaxed);
624            let path = base.join(format!("xshell-tmp-dir-{}", cnt));
625            match fs::create_dir(&path) {
626                Ok(()) => return Ok(TempDir { path }),
627                Err(err) if n_try == 1024 => return Err(Error::new_create_dir(err, path)),
628                Err(_) => n_try += 1,
629            }
630        }
631    }
632
633    /// Removes the file or directory at the given path.
634    #[doc(alias("rm_rf", "rm"))]
635    pub fn remove_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
636        self._remove_path(path.as_ref())
637    }
638    fn _remove_path(&self, path: &Path) -> Result<(), Error> {
639        let path = self.path(path);
640        match path.metadata() {
641            Ok(meta) => if meta.is_dir() { remove_dir_all(&path) } else { fs::remove_file(&path) }
642                .map_err(|err| Error::new_remove_path(err, path)),
643            Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
644            Err(err) => Err(Error::new_remove_path(err, path)),
645        }
646    }
647
648    /// Returns whether a file or directory exists at the given path.
649    #[doc(alias("stat"))]
650    pub fn path_exists<P: AsRef<Path>>(&self, path: P) -> bool {
651        self.path(path.as_ref()).exists()
652    }
653    // endregion:fs
654
655    /// Creates a new [`Cmd`] that executes the given `program`.
656    pub fn cmd<P: AsRef<Path>>(&self, program: P) -> Cmd<'_> {
657        // TODO: path lookup?
658        Cmd::new(self, program.as_ref())
659    }
660
661    fn path(&self, p: &Path) -> PathBuf {
662        let cd = self.cwd.borrow();
663        cd.join(p)
664    }
665}
666
667/// RAII guard returned from [`Shell::push_dir`].
668///
669/// Dropping `PushDir` restores the working directory of the [`Shell`] to the
670/// old value.
671#[derive(Debug)]
672#[must_use]
673pub struct PushDir<'a> {
674    old_cwd: PathBuf,
675    shell: &'a Shell,
676}
677
678impl<'a> PushDir<'a> {
679    fn new(shell: &'a Shell, path: PathBuf) -> PushDir<'a> {
680        PushDir { old_cwd: mem::replace(&mut *shell.cwd.borrow_mut(), path), shell }
681    }
682}
683
684impl Drop for PushDir<'_> {
685    fn drop(&mut self) {
686        mem::swap(&mut *self.shell.cwd.borrow_mut(), &mut self.old_cwd)
687    }
688}
689
690/// RAII guard returned from [`Shell::push_env`].
691///
692/// Dropping `PushEnv` restores the old value of the environmental variable.
693#[derive(Debug)]
694#[must_use]
695pub struct PushEnv<'a> {
696    key: OsString,
697    old_value: Option<OsString>,
698    shell: &'a Shell,
699}
700
701impl<'a> PushEnv<'a> {
702    fn new(shell: &'a Shell, key: OsString, val: OsString) -> PushEnv<'a> {
703        let old_value = shell.env.borrow_mut().insert(key.clone(), val);
704        PushEnv { shell, key, old_value }
705    }
706}
707
708impl Drop for PushEnv<'_> {
709    fn drop(&mut self) {
710        let mut env = self.shell.env.borrow_mut();
711        let key = mem::take(&mut self.key);
712        match self.old_value.take() {
713            Some(value) => {
714                env.insert(key, value);
715            }
716            None => {
717                env.remove(&key);
718            }
719        }
720    }
721}
722
723/// A builder object for constructing a subprocess.
724///
725/// A [`Cmd`] is usually created with the [`cmd!`] macro. The command exists
726/// within a context of a [`Shell`] and uses its working directory and
727/// environment.
728///
729/// # Example
730///
731/// ```no_run
732/// use xshell::{Shell, cmd};
733///
734/// let sh = Shell::new()?;
735///
736/// let branch = "main";
737/// let cmd = cmd!(sh, "git switch {branch}").quiet().run()?;
738/// # Ok::<(), xshell::Error>(())
739/// ```
740#[derive(Debug)]
741#[must_use]
742pub struct Cmd<'a> {
743    shell: &'a Shell,
744    data: CmdData,
745}
746
747#[derive(Debug, Default, Clone)]
748struct CmdData {
749    prog: PathBuf,
750    args: Vec<OsString>,
751    env_changes: Vec<EnvChange>,
752    ignore_status: bool,
753    quiet: bool,
754    secret: bool,
755    stdin_contents: Option<Vec<u8>>,
756    ignore_stdout: bool,
757    ignore_stderr: bool,
758}
759
760// We just store a list of functions to call on the `Command` — the alternative
761// would require mirroring the logic that `std::process::Command` (or rather
762// `sys_common::CommandEnvs`) uses, which is moderately complex, involves
763// special-casing `PATH`, and plausibly could change.
764#[derive(Debug, Clone)]
765enum EnvChange {
766    Set(OsString, OsString),
767    Remove(OsString),
768    Clear,
769}
770
771impl fmt::Display for Cmd<'_> {
772    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773        fmt::Display::fmt(&self.data, f)
774    }
775}
776
777impl fmt::Display for CmdData {
778    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
779        if self.secret {
780            return write!(f, "<secret>");
781        }
782
783        write!(f, "{}", self.prog.display())?;
784        for arg in &self.args {
785            // TODO: this is potentially not copy-paste safe.
786            let arg = arg.to_string_lossy();
787            if arg.chars().any(|it| it.is_ascii_whitespace()) {
788                write!(f, " \"{}\"", arg.escape_default())?
789            } else {
790                write!(f, " {}", arg)?
791            };
792        }
793        Ok(())
794    }
795}
796
797impl From<Cmd<'_>> for Command {
798    fn from(cmd: Cmd<'_>) -> Command {
799        cmd.to_command()
800    }
801}
802
803impl<'a> Cmd<'a> {
804    fn new(shell: &'a Shell, prog: &Path) -> Cmd<'a> {
805        let mut data = CmdData::default();
806        data.prog = prog.to_path_buf();
807        Cmd { shell, data }
808    }
809
810    // region:builder
811    /// Adds an argument to this commands.
812    pub fn arg<P: AsRef<OsStr>>(mut self, arg: P) -> Cmd<'a> {
813        self._arg(arg.as_ref());
814        self
815    }
816    fn _arg(&mut self, arg: &OsStr) {
817        self.data.args.push(arg.to_owned())
818    }
819
820    /// Adds all of the arguments to this command.
821    pub fn args<I>(mut self, args: I) -> Cmd<'a>
822    where
823        I: IntoIterator,
824        I::Item: AsRef<OsStr>,
825    {
826        args.into_iter().for_each(|it| self._arg(it.as_ref()));
827        self
828    }
829
830    #[doc(hidden)]
831    pub fn __extend_arg<P: AsRef<OsStr>>(mut self, arg_fragment: P) -> Cmd<'a> {
832        self.___extend_arg(arg_fragment.as_ref());
833        self
834    }
835    fn ___extend_arg(&mut self, arg_fragment: &OsStr) {
836        match self.data.args.last_mut() {
837            Some(last_arg) => last_arg.push(arg_fragment),
838            None => {
839                let mut prog = mem::take(&mut self.data.prog).into_os_string();
840                prog.push(arg_fragment);
841                self.data.prog = prog.into();
842            }
843        }
844    }
845
846    /// Overrides the value of the environmental variable for this command.
847    pub fn env<K: AsRef<OsStr>, V: AsRef<OsStr>>(mut self, key: K, val: V) -> Cmd<'a> {
848        self._env_set(key.as_ref(), val.as_ref());
849        self
850    }
851
852    fn _env_set(&mut self, key: &OsStr, val: &OsStr) {
853        self.data.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned()));
854    }
855
856    /// Overrides the values of specified environmental variables for this
857    /// command.
858    pub fn envs<I, K, V>(mut self, vars: I) -> Cmd<'a>
859    where
860        I: IntoIterator<Item = (K, V)>,
861        K: AsRef<OsStr>,
862        V: AsRef<OsStr>,
863    {
864        vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref()));
865        self
866    }
867
868    /// Removes the environment variable from this command.
869    pub fn env_remove<K: AsRef<OsStr>>(mut self, key: K) -> Cmd<'a> {
870        self._env_remove(key.as_ref());
871        self
872    }
873    fn _env_remove(&mut self, key: &OsStr) {
874        self.data.env_changes.push(EnvChange::Remove(key.to_owned()));
875    }
876
877    /// Removes all of the environment variables from this command.
878    pub fn env_clear(mut self) -> Cmd<'a> {
879        self.data.env_changes.push(EnvChange::Clear);
880        self
881    }
882
883    /// Don't return an error if command the command exits with non-zero status.
884    ///
885    /// By default, non-zero exit status is considered an error.
886    pub fn ignore_status(mut self) -> Cmd<'a> {
887        self.set_ignore_status(true);
888        self
889    }
890    /// Controls whether non-zero exit status is considered an error.
891    pub fn set_ignore_status(&mut self, yes: bool) {
892        self.data.ignore_status = yes;
893    }
894
895    /// Don't echo the command itself to stderr.
896    ///
897    /// By default, the command itself will be printed to stderr when executed via [`Cmd::run`].
898    pub fn quiet(mut self) -> Cmd<'a> {
899        self.set_quiet(true);
900        self
901    }
902    /// Controls whether the command itself is printed to stderr.
903    pub fn set_quiet(&mut self, yes: bool) {
904        self.data.quiet = yes;
905    }
906
907    /// Marks the command as secret.
908    ///
909    /// If a command is secret, it echoes `<secret>` instead of the program and
910    /// its arguments, even in error messages.
911    pub fn secret(mut self) -> Cmd<'a> {
912        self.set_secret(true);
913        self
914    }
915    /// Controls whether the command is secret.
916    pub fn set_secret(&mut self, yes: bool) {
917        self.data.secret = yes;
918    }
919
920    /// Pass the given slice to the standard input of the spawned process.
921    pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd<'a> {
922        self._stdin(stdin.as_ref());
923        self
924    }
925    fn _stdin(&mut self, stdin: &[u8]) {
926        self.data.stdin_contents = Some(stdin.to_vec());
927    }
928
929    /// Ignores the standard output stream of the process.
930    ///
931    /// This is equivalent to redirecting stdout to `/dev/null`. By default, the
932    /// stdout is inherited or captured.
933    pub fn ignore_stdout(mut self) -> Cmd<'a> {
934        self.set_ignore_stdout(true);
935        self
936    }
937    /// Controls whether the standard output is ignored.
938    pub fn set_ignore_stdout(&mut self, yes: bool) {
939        self.data.ignore_stdout = yes;
940    }
941
942    /// Ignores the standard output stream of the process.
943    ///
944    /// This is equivalent redirecting stderr to `/dev/null`. By default, the
945    /// stderr is inherited or captured.
946    pub fn ignore_stderr(mut self) -> Cmd<'a> {
947        self.set_ignore_stderr(true);
948        self
949    }
950    /// Controls whether the standard error is ignored.
951    pub fn set_ignore_stderr(&mut self, yes: bool) {
952        self.data.ignore_stderr = yes;
953    }
954    // endregion:builder
955
956    // region:running
957    /// Runs the command.
958    ///
959    /// By default the command itself is echoed to stderr, its standard streams
960    /// are inherited, and non-zero return code is considered an error. These
961    /// behaviors can be overridden by using various builder methods of the [`Cmd`].
962    pub fn run(&self) -> Result<()> {
963        if !self.data.quiet {
964            eprintln!("$ {}", self);
965        }
966        self.output_impl(false, false).map(|_| ())
967    }
968
969    /// Run the command and return its stdout as a string. Any trailing newline or carriage return will be trimmed.
970    pub fn read(&self) -> Result<String> {
971        self.read_stream(false)
972    }
973
974    /// Run the command and return its stderr as a string. Any trailing newline or carriage return will be trimmed.
975    pub fn read_stderr(&self) -> Result<String> {
976        self.read_stream(true)
977    }
978
979    /// Run the command and return its output.
980    pub fn output(&self) -> Result<Output> {
981        self.output_impl(true, true)
982    }
983    // endregion:running
984
985    fn read_stream(&self, read_stderr: bool) -> Result<String> {
986        let read_stdout = !read_stderr;
987        let output = self.output_impl(read_stdout, read_stderr)?;
988        self.check_status(output.status)?;
989
990        let stream = if read_stderr { output.stderr } else { output.stdout };
991        let mut stream = String::from_utf8(stream).map_err(|err| Error::new_cmd_utf8(self, err))?;
992
993        if stream.ends_with('\n') {
994            stream.pop();
995        }
996        if stream.ends_with('\r') {
997            stream.pop();
998        }
999
1000        Ok(stream)
1001    }
1002
1003    fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> Result<Output> {
1004        let mut child = {
1005            let mut command = self.to_command();
1006
1007            if !self.data.ignore_stdout {
1008                command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
1009            }
1010            if !self.data.ignore_stderr {
1011                command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
1012            }
1013
1014            command.stdin(match &self.data.stdin_contents {
1015                Some(_) => Stdio::piped(),
1016                None => Stdio::null(),
1017            });
1018
1019            command.spawn().map_err(|err| {
1020                // Try to determine whether the command failed because the current
1021                // directory does not exist. Return an appropriate error in such a
1022                // case.
1023                if matches!(err.kind(), io::ErrorKind::NotFound) {
1024                    let cwd = self.shell.cwd.borrow();
1025                    if let Err(err) = cwd.metadata() {
1026                        return Error::new_current_dir(err, Some(cwd.clone()));
1027                    }
1028                }
1029                Error::new_cmd_io(self, err)
1030            })?
1031        };
1032
1033        let mut io_thread = None;
1034        if let Some(stdin_contents) = self.data.stdin_contents.clone() {
1035            let mut stdin = child.stdin.take().unwrap();
1036            io_thread = Some(std::thread::spawn(move || {
1037                stdin.write_all(&stdin_contents)?;
1038                stdin.flush()
1039            }));
1040        }
1041        let out_res = child.wait_with_output();
1042        let err_res = io_thread.map(|it| it.join().unwrap());
1043        let output = out_res.map_err(|err| Error::new_cmd_io(self, err))?;
1044        if let Some(err_res) = err_res {
1045            err_res.map_err(|err| Error::new_cmd_stdin(self, err))?;
1046        }
1047        self.check_status(output.status)?;
1048        Ok(output)
1049    }
1050
1051    fn to_command(&self) -> Command {
1052        let mut res = Command::new(&self.data.prog);
1053        res.current_dir(self.shell.current_dir());
1054        res.args(&self.data.args);
1055
1056        for (key, val) in &*self.shell.env.borrow() {
1057            res.env(key, val);
1058        }
1059        for change in &self.data.env_changes {
1060            match change {
1061                EnvChange::Clear => res.env_clear(),
1062                EnvChange::Remove(key) => res.env_remove(key),
1063                EnvChange::Set(key, val) => res.env(key, val),
1064            };
1065        }
1066
1067        if self.data.ignore_stdout {
1068            res.stdout(Stdio::null());
1069        }
1070
1071        if self.data.ignore_stderr {
1072            res.stderr(Stdio::null());
1073        }
1074
1075        res
1076    }
1077
1078    fn check_status(&self, status: ExitStatus) -> Result<()> {
1079        if status.success() || self.data.ignore_status {
1080            return Ok(());
1081        }
1082        Err(Error::new_cmd_status(self, status))
1083    }
1084}
1085
1086/// A temporary directory.
1087///
1088/// This is a RAII object which will remove the underlying temporary directory
1089/// when dropped.
1090#[derive(Debug)]
1091#[must_use]
1092pub struct TempDir {
1093    path: PathBuf,
1094}
1095
1096impl TempDir {
1097    /// Returns the path to the underlying temporary directory.
1098    pub fn path(&self) -> &Path {
1099        &self.path
1100    }
1101}
1102
1103impl Drop for TempDir {
1104    fn drop(&mut self) {
1105        let _ = remove_dir_all(&self.path);
1106    }
1107}
1108
1109#[cfg(not(windows))]
1110fn remove_dir_all(path: &Path) -> io::Result<()> {
1111    std::fs::remove_dir_all(path)
1112}
1113
1114#[cfg(windows)]
1115fn remove_dir_all(path: &Path) -> io::Result<()> {
1116    for _ in 0..99 {
1117        if fs::remove_dir_all(path).is_ok() {
1118            return Ok(());
1119        }
1120        std::thread::sleep(std::time::Duration::from_millis(10))
1121    }
1122    fs::remove_dir_all(path)
1123}