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!(err.to_string().starts_with("command `false` failed"));
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
Structs
A command.
An error returned by an xshell
operation.
The result of calling a successful pushd
.
The result of calling a successful pushenv
.
A temporary directory.
Functions
Copies src
into dst
.
Returns the current working directory.
Hardlinks src
to dst
.
Creates the path
directory and all of its parents.
Creates an empty, world-readable, temporary directory.
Changes the current directory to dir
.
Sets the environment variable key
to have value val
.
Returns a sorted list of paths directly contained in the directory at path
that were able to be accessed without error.
Reads the file at path
into a String
.
Removes the given path
and all of its contents (if it is a directory).
Writes the contents
into the file at path
, creating the file (and the
path to it) if it didn’t exist already.