xshell 0.1.13

Utilities for quick shell scripting in Rust
Documentation
//! xshell makes it easy to write cross-platform "bash" scripts in Rust.
//!
//! It provides a `cmd!` macro for running subprocesses, as well as a number of
//! basic file manipulation utilities.
//!
//! ```
//! # if cfg!(windows) { return Ok(()); }
//! use xshell::{cmd, read_file};
//!
//! let name = "Julia";
//! let output = cmd!("echo hello {name}!").read()?;
//! assert_eq!(output, "hello Julia!");
//!
//! let err = read_file("feeling-lucky.txt").unwrap_err();
//! assert_eq!(
//!     err.to_string(),
//!     "`feeling-lucky.txt`: no such file or directory (os error 2)",
//! );
//! # Ok::<(), xshell::Error>(())
//! ```
//!
//! The intended use-case is various bits of glue code, which could be written
//! in bash or python. The original motivation is
//! [`xtask`](https://github.com/matklad/cargo-xtask) development.
//!
//! **Goals**: fast compile times, ergonomics, clear error messages.<br>
//! **Non goals**: completeness, robustness / misuse resistance.
//!
//! For "heavy-duty" code, consider using [`duct`] or [`std::process::Command`]
//! instead.
//!
//! # API Overview
//!
//! For a real-world example, see this crate's own CI script:
//!
//! [https://github.com/matklad/xshell/blob/master/examples/ci.rs](https://github.com/matklad/xshell/blob/master/examples/ci.rs)
//!
//! ## `cmd!` Macro
//!
//! Read output of the process into `String`. The final newline will be
//! stripped.
//!
//! ```
//! # use xshell::cmd;
//! let output = cmd!("date +%Y-%m-%d").read()?;
//! assert!(output.chars().all(|c| "01234567890-".contains(c)));
//! # Ok::<(), xshell::Error>(())
//! ```
//!
//! If the exist status is non-zero, an error is returned.
//!
//! ```
//! # use xshell::cmd;
//! let err = cmd!("false").read().unwrap_err();
//! assert_eq!(
//!     err.to_string(),
//!     "command `false` failed, exit code: 1",
//! );
//! ```
//!
//! <hr>
//!
//! Run the process, inheriting stdout and stderr. The command is echoed to
//! stdout.
//!
//! ```
//! # use xshell::cmd;
//! cmd!("echo hello!").run()?;
//! # Ok::<(), xshell::Error>(())
//! ```
//!
//! Output
//!
//! ```text
//! $ echo hello!
//! hello!
//! ```
//!
//! <hr>
//!
//! Interpolation is supported via `{name}` syntax. Use `{name...}` to
//! interpolate sequence of values.
//!
//! ```
//! # use xshell::cmd;
//! let greeting = "Guten Tag";
//! let people = &["Spica", "Boarst", "Georgina"];
//! assert_eq!(
//!     cmd!("echo {greeting} {people...}").to_string(),
//!     r#"echo "Guten Tag" Spica Boarst Georgina"#
//! );
//! ```
//!
//! Note that the argument with a space is handled correctly. This is because
//! `cmd!` macro parses the string template at compile time. The macro hands the
//! interpolated values to the underlying `std::process::Command` as is and is
//! not vulnerable to [shell
//! injection](https://en.wikipedia.org/wiki/Code_injection#Shell_injection).
//!
//! Single quotes in literal arguments are supported:
//!
//! ```
//! # use xshell::cmd;
//! assert_eq!(
//!     cmd!("echo 'hello world'").to_string(),
//!     r#"echo "hello world""#,
//! )
//! ```
//! Splat syntax is used for optional arguments idiom.
//!
//! ```
//! # use xshell::cmd;
//! let check = if true { &["--", "--check"] } else { &[][..] };
//! assert_eq!(
//!     cmd!("cargo fmt {check...}").to_string(),
//!     "cargo fmt -- --check"
//! );
//!
//! let dry_run = if true { Some("--dry-run") } else { None };
//! assert_eq!(
//!     cmd!("git push {dry_run...}").to_string(),
//!     "git push --dry-run"
//! );
//! ```
//!
//! <hr>
//!
//! xshell does not provide API for creating command pipelines. If you need
//! pipelines, consider using [`duct`] instead. Alternatively, you can convert
//! `xshell::Cmd` into [`std::process::Command`]:
//!
//! ```
//! # use xshell::cmd;
//! let command: std::process::Command = cmd!("echo 'hello world'").into();
//! ```
//!
//! ## Manipulating the Environment
//!
//! Instead of `cd` and `export`, xshell uses RAII based `pushd` and `pushenv`
//!
//! ```
//! use xshell::{cwd, pushd, pushenv};
//!
//! let initial_dir = cwd()?;
//! {
//!     let _p = pushd("src")?;
//!     assert_eq!(
//!         cwd()?,
//!         initial_dir.join("src"),
//!     );
//! }
//! assert_eq!(cwd()?, initial_dir);
//!
//! assert!(std::env::var("MY_VAR").is_err());
//! let _e = pushenv("MY_VAR", "92");
//! assert_eq!(
//!     std::env::var("MY_VAR").as_deref(),
//!     Ok("92")
//! );
//! # Ok::<(), xshell::Error>(())
//! ```
//!
//! ## Working with Files
//!
//! xshell provides the following utilities, which are mostly re-exports from
//! `std::fs` module with paths added to error messages: `rm_rf`, `read_file`,
//! `write_file`, `mkdir_p`, `cp`, `read_dir`, `cwd`.
//!
//! # Maintenance
//!
//! Minimum Supported Rust Version: 1.47.0. MSRV bump is not considered semver
//! breaking. MSRV is updated conservatively.
//!
//! The crate isn't comprehensive. Additional functionality is added on
//! as-needed bases, as long as it doesn't compromise compile times.
//! Function-level docs are an especially welcome addition :-)
//!
//! # Implementation details
//!
//! The design is heavily inspired by the Julia language:
//!
//! * [Shelling Out Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/)
//! * [Put This In Your Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/)
//! * [Running External Programs](https://docs.julialang.org/en/v1/manual/running-external-programs/)
//! * [Filesystem](https://docs.julialang.org/en/v1/base/file/)
//!
//! Smaller influences are the [`duct`] crate and Ruby's
//! [`FileUtils`](https://ruby-doc.org/stdlib-2.4.1/libdoc/fileutils/rdoc/FileUtils.html)
//! module.
//!
//! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on
//! helper libraries, so the fixed-cost impact on compile times is moderate.
//! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second.
//! Equivalent program using only `std::process::Command` compiles in 0.25
//! seconds.
//!
//! To make IDEs infer correct types without expanding proc-macro, it is wrapped
//! into a declarative macro which supplies type hints.
//!
//! Environment manipulation mutates global state and might have surprising
//! interactions with threads. Internally, everything is protected by a global
//! shell lock, so all functions in this crate are thread safe. However,
//! functions outside of xshell's control might experience race conditions:
//!
//! ```
//! use std::{thread, fs};
//!
//! use xshell::{pushd, read_file};
//!
//! let t1 = thread::spawn(|| {
//!     let _p = pushd("./src");
//! });
//!
//! // This is guaranteed to work: t2 will block while t1 is in `pushd`.
//! let t2 = thread::spawn(|| {
//!     let res = read_file("./src/lib.rs");
//!     assert!(res.is_ok());
//! });
//!
//! // This is a race: t3 might observe difference cwds depending on timing.
//! let t3 = thread::spawn(|| {
//!     let res = fs::read_to_string("./src/lib.rs");
//!     assert!(res.is_ok() || res.is_err());
//! });
//! # t1.join().unwrap(); t2.join().unwrap(); t3.join().unwrap();
//! ```
//!
//! # Naming
//!
//! xshell is an ex-shell, for those who grew tired of bash.<br>
//! xshell is an x-platform shell, for those who don't want to run `build.sh` on windows.<br>
//! xshell is built for [`xtask`](https://github.com/matklad/cargo-xtask).<br>
//! xshell uses x-traordinary level of [trickery](https://github.com/matklad/xshell/blob/843df7cd5b7d69fc9d2b884dc0852598335718fe/src/lib.rs#L233-L234),
//! just like `xtask` [does](https://matklad.github.io/2018/01/03/make-your-own-make.html).
//!
//! [`duct`]: https://github.com/oconnor663/duct.rs
//! [`std::process::Command`]: https://doc.rust-lang.org/stable/std/process/struct.Command.html

#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![deny(rust_2018_idioms)]

mod env;
mod gsl;
mod error;
mod fs;

use std::{
    ffi::{OsStr, OsString},
    fmt, io,
    io::Write,
    path::Path,
    process::Output,
    process::Stdio,
};

use error::CmdErrorKind;
#[doc(hidden)]
pub use xshell_macros::__cmd;

pub use crate::{
    env::{pushd, pushenv, Pushd, Pushenv},
    error::{Error, Result},
    fs::{cp, cwd, mkdir_p, mktemp_d, read_dir, read_file, rm_rf, write_file, TempDir},
};

/// Constructs a [`Cmd`] from the given string.
#[macro_export]
macro_rules! cmd {
    ($cmd:tt) => {{
        #[cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)]
        format_args!($cmd);
        use $crate::Cmd as __CMD;
        let cmd: $crate::Cmd = $crate::__cmd!(__CMD $cmd);
        cmd
    }};
}

/// A command.
#[must_use]
#[derive(Debug)]
pub struct Cmd {
    args: Vec<OsString>,
    stdin_contents: Option<Vec<u8>>,
    ignore_status: bool,
    echo_cmd: bool,
    secret: bool,
    env_changes: Vec<EnvChange>,
    ignore_stdout: bool,
    ignore_stderr: bool,
}

impl fmt::Display for Cmd {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if !self.secret {
            let mut space = "";
            for arg in &self.args {
                write!(f, "{}", space)?;
                space = " ";

                let arg = arg.to_string_lossy();
                if arg.chars().any(|it| it.is_ascii_whitespace()) {
                    write!(f, "\"{}\"", arg.escape_default())?
                } else {
                    write!(f, "{}", arg)?
                };
            }
        } else {
            write!(f, "<secret>")?;
        }
        Ok(())
    }
}

impl From<Cmd> for std::process::Command {
    fn from(cmd: Cmd) -> Self {
        cmd.command()
    }
}

impl Cmd {
    /// Creates a new `Cmd` that executes the given `program`.
    pub fn new(program: impl AsRef<Path>) -> Cmd {
        Cmd::_new(program.as_ref())
    }
    fn _new(program: &Path) -> Cmd {
        Cmd {
            args: vec![program.as_os_str().to_owned()],
            stdin_contents: None,
            ignore_status: false,
            echo_cmd: true,
            secret: false,
            env_changes: vec![],
            ignore_stdout: false,
            ignore_stderr: false,
        }
    }

    /// Pushes an argument onto this `Cmd`.
    pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Cmd {
        self._arg(arg.as_ref());
        self
    }

    /// Pushes the arguments onto this `Cmd`.
    pub fn args<I>(mut self, args: I) -> Cmd
    where
        I: IntoIterator,
        I::Item: AsRef<OsStr>,
    {
        args.into_iter().for_each(|it| self._arg(it.as_ref()));
        self
    }

    fn _arg(&mut self, arg: &OsStr) {
        self.args.push(arg.to_owned())
    }

    /// Equivalent to [`std::process::Command::env`].
    pub fn env<K, V>(mut self, key: K, val: V) -> Cmd
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self._env_set(key.as_ref(), val.as_ref());
        self
    }

    fn _env_set(&mut self, key: &OsStr, val: &OsStr) {
        self.env_changes.push(EnvChange::Set(key.to_owned(), val.to_owned()));
    }

    /// Equivalent to [`std::process::Command::envs`].
    ///
    /// Note: This does not replace the child process's environment, unless you
    /// call [`Cmd::env_clear`] first.
    pub fn envs<I, K, V>(mut self, vars: I) -> Cmd
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        vars.into_iter().for_each(|(k, v)| self._env_set(k.as_ref(), v.as_ref()));
        self
    }

    /// Equivalent to [`std::process::Command::env_remove`].
    pub fn env_remove<K>(mut self, key: K) -> Cmd
    where
        K: AsRef<OsStr>,
    {
        self._env_remove(key.as_ref());
        self
    }

    fn _env_remove(&mut self, key: &OsStr) {
        self.env_changes.push(EnvChange::Remove(key.to_owned()));
    }

    /// Equivalent to [`std::process::Command::env_clear`].
    ///
    /// Note that on Windows some environmental variables are required for
    /// process spawning. See https://github.com/rust-lang/rust/issues/31259.
    pub fn env_clear(mut self) -> Cmd {
        self.env_changes.push(EnvChange::Clear);
        self
    }

    /// Returns a `Cmd` that will ignore the stdout stream. This is equivalent of
    /// attaching stdout to `/dev/null`.
    pub fn ignore_stdout(mut self) -> Cmd {
        self.ignore_stdout = true;
        self
    }

    /// Returns a `Cmd` that will ignore the stderr stream. This is equivalent of
    /// attaching stderr to `/dev/null`.
    pub fn ignore_stderr(mut self) -> Cmd {
        self.ignore_stderr = true;
        self
    }

    #[doc(hidden)]
    pub fn __extend_arg(mut self, arg: impl AsRef<OsStr>) -> Cmd {
        self.___extend_arg(arg.as_ref());
        self
    }
    fn ___extend_arg(&mut self, arg: &OsStr) {
        self.args.last_mut().unwrap().push(arg)
    }

    /// Returns a `Cmd` that ignores its exit status.
    pub fn ignore_status(mut self) -> Cmd {
        self.ignore_status = true;
        self
    }

    /// Returns a `Cmd` with the given stdin.
    pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Cmd {
        self._stdin(stdin.as_ref());
        self
    }
    fn _stdin(&mut self, stdin: &[u8]) {
        self.stdin_contents = Some(stdin.to_vec());
    }

    /// Returns a `Cmd` that echoes itself (or not) as specified.
    pub fn echo_cmd(mut self, echo: bool) -> Cmd {
        self.echo_cmd = echo;
        self
    }

    /// Returns a `Cmd` that is secret (or not) as specified.
    ///
    /// If a command is secret, it echoes `<secret>` instead of the program and
    /// its arguments.
    pub fn secret(mut self, secret: bool) -> Cmd {
        self.secret = secret;
        self
    }

    /// Returns the stdout from running the command.
    pub fn read(self) -> Result<String> {
        self.read_stream(false)
    }

    /// Returns the stderr from running the command.
    pub fn read_stderr(self) -> Result<String> {
        self.read_stream(true)
    }

    /// Returns a [`std::process::Output`] from running the command.
    pub fn output(self) -> Result<Output> {
        match self.output_impl(true, true) {
            Ok(output) if output.status.success() || self.ignore_status => Ok(output),
            Ok(output) => Err(CmdErrorKind::NonZeroStatus(output.status).err(self)),
            Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
        }
    }

    /// Runs the command.
    pub fn run(self) -> Result<()> {
        let _guard = gsl::read();
        if self.echo_cmd {
            eprintln!("$ {}", self);
        }
        match self.command().status() {
            Ok(status) if status.success() || self.ignore_status => Ok(()),
            Ok(status) => Err(CmdErrorKind::NonZeroStatus(status).err(self)),
            Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
        }
    }

    fn read_stream(self, read_stderr: bool) -> Result<String> {
        let read_stdout = !read_stderr;
        match self.output_impl(read_stdout, read_stderr) {
            Ok(output) if output.status.success() || self.ignore_status => {
                let stream = if read_stderr { output.stderr } else { output.stdout };
                let mut stream = String::from_utf8(stream)
                    .map_err(|utf8_err| CmdErrorKind::NonUtf8Output(utf8_err).err(self))?;

                if stream.ends_with('\n') {
                    stream.pop();
                }
                if stream.ends_with('\r') {
                    stream.pop();
                }

                Ok(stream)
            }
            Ok(output) => Err(CmdErrorKind::NonZeroStatus(output.status).err(self)),
            Err(io_err) => Err(CmdErrorKind::Io(io_err).err(self)),
        }
    }

    fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> io::Result<Output> {
        let mut child = {
            let _guard = gsl::read();
            let mut command = self.command();

            command.stdin(match &self.stdin_contents {
                Some(_) => Stdio::piped(),
                None => Stdio::null(),
            });

            if !self.ignore_stdout {
                command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
            }

            if !self.ignore_stderr {
                command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
            }

            command.spawn()?
        };

        if let Some(stdin_contents) = &self.stdin_contents {
            let mut stdin = child.stdin.take().unwrap();
            stdin.write_all(stdin_contents)?;
            stdin.flush()?;
        }
        child.wait_with_output()
    }

    fn command(&self) -> std::process::Command {
        let mut res = std::process::Command::new(&self.args[0]);
        res.args(&self.args[1..]);
        self.apply_env(&mut res);
        if self.ignore_stdout {
            res.stdout(Stdio::null());
        }
        if self.ignore_stderr {
            res.stderr(Stdio::null());
        }
        res
    }

    fn apply_env(&self, cmd: &mut std::process::Command) {
        for change in &self.env_changes {
            match change {
                EnvChange::Clear => cmd.env_clear(),
                EnvChange::Remove(key) => cmd.env_remove(key),
                EnvChange::Set(key, val) => cmd.env(key, val),
            };
        }
    }
}

// We just store a list of functions to call on the `Command` — the alternative
// would require mirroring the logic that `std::process::Command` (or rather
// `sys_common::CommandEnvs`) uses, which is moderately complex, involves
// special-casing `PATH`, and plausbly could change.
#[derive(Debug)]
enum EnvChange {
    Set(OsString, OsString),
    Remove(OsString),
    Clear,
}