Expand description
§XScript
A library for writing robust shell-script-like programs and running commands with ease.
The initial motivation for this crate was xtask
development.
Taken from the xtask
of this crate, here is an example for building the documentation you are currently reading with the nightly toolchain and proper --cfg
flags:
use xscript::{read_str, run, vars, Run, LocalEnv};
let mut env = LocalEnv::current_dir()?.with_vars(vars! {
RUSTDOCFLAGS = "--cfg docsrs --cfg xscript_unstable",
RUSTFLAGS = "--cfg xscript_unstable",
});
let project_root = read_str!(env, ["git", "rev-parse", "--show-toplevel"])?;
env.change_dir(project_root)?;
let cargo_args = ["+nightly"];
let doc_args = ["--lib", "--all-features"];
run!(env, ["cargo", ...cargo_args, "doc", ...doc_args])?;
🙏 Acknowledgements: The design of this crate has been heavily inspired by xshell
.
See Related Crates for details.
🤔 Rationale: Executing commands via std::process::Command
can be quite cumbersome.
You have to be careful to check the exit status of the child process as otherwise errors may go unnoticed, you have to take care of the input and output manually, and you have to prepare the environment and set the working directory for each command individually.1
While this fine-grained control is the correct choice for the standard library – and, in fact, an explicit goal – this crate aims to provide more convenient APIs for common shell-script-like use cases where multiple commands are executed in succession.
🎯 Goals and Non-Goals: This crate aims to provide convenient and hard-to-misuse APIs for writing robust shell-script-like programs where multiple commands are executed in succession and in the same environment. For this purpose, an environment may also be a Docker container or a remote machine. This crate does not aim to be a general-purpose solution for all command launching needs, though, and instead favors ease of use for common use cases.
§User Guide
Let’s first establish some base terminology and corresponding types:
- A command consists of a program and arguments.
Commands are represented by the
Cmd
type. In addition to the program and its arguments, theCmd
type may also hold further information, e.g., thestdin
input to provide. - An environment provides a context in which commands can be executed.
Usually, it consists of a working directory and values for environment variables.
LocalEnv
is an environment for executing commands locally.
This separation of commands and environments is central to shell-script-like programs. It enables the convenient execution of multiple commands in the same environment without rebuilding it all the time.
§Commands
For convenient command construction, the cmd!
macro is provided.
The first argument of the cmd!
macro becomes the program of the command.
The remaining arguments become the arguments of the command.
Here is a simple example:
cmd!("git", "rev-parse", "--show-toplevel");
When using string literals, the program and arguments both support string interpolation with format!
:
let prefix = "/usr/bin";
let user = "silitics";
let repo = "xscript-rs";
cmd!("{prefix}/git", "clone", "https://github.com/{user}/{repo}.git");
Instead of string literals, any expression implementing AsRef<str>
can be used for the program and its arguments.
Further, to extend the arguments of a command with an iterable, any iterable expression can be prefixed with ...
:
const CARGO: &str = "cargo";
let cargo_command = "doc";
let cargo_args = ["+nightly"];
let extra_args = ["--lib", "--all-features"];
cmd!(CARGO, ...cargo_args, cargo_command, ...extra_args);
After constructing a command with cmd!
, builder methods can be called to further configure it (see Cmd
for details).
For instance, environment variables can be set per command and the stdin
input can be specified:
cmd!("cargo", "+nightly", "doc").with_var("RUSTDOCFLAGS", "--cfg docsrs");
cmd!("gzip", "--best").with_stdin("Compress this string!");
Note that cmd!
merely constructs the command but does not yet execute it.
§Environments
Commands are executed in environments. Note that we expressly intend for third-party crates to implement environments, e.g., for running commands on a remote machine via SSH. Behind an unstable feature flag (see Optional Features), this crate also provides an experimental environment for running commands in a Docker container.
Environments should implement the Run
and/or the RunAsync
trait.
Sane I/O Defaults: Unless otherwise specified as part of a command, environments should set stdin
to In::Null
by default.
Inheriting stdin
is dangerous for two reasons, (a) a command may wait for input and, thus, block the main program, and (b) a command may interfere with the main program.
Likewise, by default, all outputs should be captured, i.e., stdout
and stderr
should be set to Out::Capture
by default.
This means, that they are both available as part of RunError
or RunOutput
independently of whether the program succeeds or fails.
The stderr
output should also be replayed by default, i.e., written to the parent process’ stderr
stream (after the command terminated).
§Running Commands
After constructing a command, the methods of the Run
(RunAsync
) trait can be used to run it.
For convenience, the macros run!
, read_str!
, and read_bytes!
are provided combining the construction and execution.
For instance
read_str!(env, ["git", "rev-parse", "--show-toplevel"])?;
runs git rev-parse --show-toplevel
(which outputs a Git repository’s root directory on stdout
) and reads its stdout
output as a string.
Here, everything inside the brackets [...]
is forwarded to the cmd!
macro (see Commands).
The syntax of run!
and read_bytes!
is analogous.
In addition, calls to Cmd
builder methods can simply be appended:
read_bytes!(env, ["gzip", "--best"].with_stdin("Compress this string!"))?;
If the environment is omitted, then ParentEnv
is used.
This allows running commands in the environment of the parent process, e.g., in cases where the environment does not matter or inheriting the environment is intended.
§Optional Features
This crate offers the following optional features:
serde
: Support for serialization and deserialization of various data structures via Serde.async
: Support for asynchronous environments (in particular, theRunAsync
trait).tokio
: Support for running commands asynchronously using Tokio.
§Related Crates
This crate is by far not the first aiming to simplify command launching.
The crates xshell
, devx-cmd
, and duct
start with the same motivation.
In terms of both, its design and its goals, xshell
comes closest to xscript
.
In contrast to devx-cmd
and duct
, xshell
and xscript
aim to separate the execution environment from the commands being launched.
This separation is central to shell-script-like use cases because it enables launching multiple commands in the same environment.
In case of xshell
, the Shell
type provides a common environment for launching multiple commands.
Just like a shell, it has a current working directory in which commands are launched and defines environment variables.
Note, however, that xshell
still couples every command to a Shell
.
Now, xscript
follows the same idea but its command type Cmd
is completely decoupled from the execution environment.
Instead of calling a method on the command to launch it, the idea is to provide the command as an argument to a method of an environment.
An environment then defines the exact way in which commands are launched.
For instance, they may launch the command in a Docker container or via SSH on a remote machine, and they may offer asynchronous launch methods.
Crucially, environments can also be defined by third-party crates.
Another technical difference between xshell
and xscript
is that xshell
uses a proc macro and parses a command provided as a string while xscript
does not do any string parsing but requires the arguments to be given individually.
In contrast to xscript
, xshell
’s Shell
environment also offers methods to work with the filesystem.
We may also add such functionality to xscript
later.
Note that xscript
does not support building pipelines where the output of one command is continuously fed as an input to another command running at the same time.
In case you need pipelines, consider using duct
instead.
In contrast to all other crates mentioned above, xscript
is generic over the command’s string type.
It supports both OsString
- and String
-based commands for local and portable commands, respectively.
Most macros rely on Rust’s type inference for the correct string type.
For the cmd!
macro, there also exist specialized variants.
Or, modify the global environment of the parent process. ↩
Modules§
Macros§
- cmd
- Constructs a command.
- cmd_os
- Constructs a command using
OsString
as string type. - cmd_str
- Constructs a command using
String
as string type. - read_
bytes - Runs a command in a given environment reading
stdout
as bytes (seeRun::read_bytes
)). - read_
str - Runs a command in a given environment reading
stdout
as a string (seeRun::read_str
). - run
- Runs a command in a given environment (see
Run::run
). - vars
- Convenience macro for constructing sets of variables.
- vars_os
- Constructs environment variables using
OsString
as string type. - vars_
str - Constructs environment variables using
String
as string type.
Structs§
- Cmd
- A command.
- Local
Env - A local execution environment.
- Parent
Env - Execution environment of the parent process.
- RunError
- Error running a command.
- RunOutput
- Output produced when running a command.
- Vars
- A set of environment variables.
Enums§
- In
- An input provided to a command.
- Out
- Indicates what to do with the output of a command.
- RunError
Kind - Error while running a command.
Traits§
- CmdString
- A string type that can be used to construct commands.
- Run
- Trait for running commands in an execution environment.
- RunAsync
async
- Trait for running commands asynchronously in an execution environment.
- ToString
Lossy - Lossy string conversion.
Type Aliases§
- RunResult
- The result of running a command.