Crate xshell[][src]

Expand description

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.

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)",
);

The intended use-case is various bits of glue code, which could be written in bash or python. The original motivation is xtask development.

Goals: fast compile times, ergonomics, clear error messages.
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

cmd! Macro

Read output of the process into String. The final newline will be stripped.

let output = cmd!("date +%Y-%m-%d").read()?;
assert!(output.chars().all(|c| "01234567890-".contains(c)));

If the exist status is non-zero, an error is returned.

let err = cmd!("false").read().unwrap_err();
assert_eq!(
    err.to_string(),
    "command `false` failed, exit code: 1",
);

Run the process, inheriting stdout and stderr. The command is echoed to stdout.

cmd!("echo hello!").run()?;

Output

$ echo hello!
hello!

Interpolation is supported via {name} syntax. Use {name...} to interpolate sequence of values.

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.

Single quotes in literal arguments are supported:

assert_eq!(
    cmd!("echo 'hello world'").to_string(),
    r#"echo "hello world""#,
)

Splat syntax is used for optional arguments idiom.

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"
);

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:

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")
);

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:

Smaller influences are the duct crate and Ruby’s FileUtils 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());
});

Naming

xshell is an ex-shell, for those who grew tired of bash.
xshell is an x-platform shell, for those who don’t want to run build.sh on windows.
xshell is built for xtask.
xshell uses x-traordinary level of trickery, just like xtask does.

Macros

cmd

Constructs a Cmd from the given string.

Structs

Cmd

A command.

Error

An error returned by an xshell operation.

Pushd

The result of calling a successful pushd.

Pushenv

The result of calling a successful pushenv.

TempDir

A temporary directory.

Functions

cp

Copies src into dst.

cwd

Returns the current working directory.

mkdir_p

Creates the path directory and all of its parents.

mktemp_d

Creates an empty, world-readable, temporary directory.

pushd

Changes the current directory to dir.

pushenv

Sets the environment variable key to have value val.

read_dir

Returns a sorted list of paths directly contained in the directory at path that were able to be accessed without error.

read_file

Reads the file at path into a String.

rm_rf

Removes the given path and all of its contents (if it is a directory).

write_file

Writes the contents into the file at path, creating the file (and the path to it) if it didn’t exist already.

Type Definitions

Result

Result from std, with the error type defaulting to xshell’s Error.