14 releases

0.1.14 May 29, 2021
0.1.13 May 21, 2021
0.1.10 Apr 26, 2021
0.1.9 Mar 1, 2021
0.1.6 Oct 19, 2020

#11 in Build Utils

Download history 3682/week @ 2021-04-08 3109/week @ 2021-04-15 2792/week @ 2021-04-22 3837/week @ 2021-04-29 3628/week @ 2021-05-06 3605/week @ 2021-05-13 4074/week @ 2021-05-20 4027/week @ 2021-05-27 3933/week @ 2021-06-03 3839/week @ 2021-06-10 3545/week @ 2021-06-17 3913/week @ 2021-06-24 4346/week @ 2021-07-01 3263/week @ 2021-07-08 3974/week @ 2021-07-15 4278/week @ 2021-07-22

14,588 downloads per month
Used in less than 8 crates

MIT/Apache

34KB
648 lines

xshell: Making Rust a Better Bash

xshell provides a set of cross-platform utilities for writing ergonomic "bash" scripts.

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

See the docs for more.


lib.rs:

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 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.

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

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

# use xshell::cmd;
cmd!("echo hello!").run()?;
# Ok::<(), xshell::Error>(())

Output

$ echo hello!
hello!

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.

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

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:

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());
});
# t1.join().unwrap(); t2.join().unwrap(); t3.join().unwrap();

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.

Dependencies