5 releases
Uses new Rust 2024
new 0.1.6 | Apr 18, 2025 |
---|---|
0.1.5 | Apr 18, 2025 |
0.1.4 | Mar 16, 2025 |
0.1.3 | Mar 15, 2025 |
0.1.2 | Mar 15, 2025 |
#212 in Build Utils
349 downloads per month
22KB
258 lines
shell-exec
This Rust crate simplifies the execution of CLI programs by a Rust programs. It exports two macros: s!
and exec!
. In general, you want to use macro s!
to execute shell commands. We export exec!
for backward compatibility with some existing code.
Macro exec!
Macro exec!
takes three agruments: exec!(error_id, verbose, cmd)
. Argument error_id
is just a unique string literal that is printed in case of an error. I typically generate this string literal as follows:
echo "\"$RANDOM-$RANDOM-$RANDOM\", "
and paste the output as the first argument of macro exec!
(or, even better the first argument of s!
). This simplifies finding the code that issued a certain error message.
Argument verbose
must be of type bool
. If it is true
, the command that will be executed is first printed to stderr
.
On success, exec!
returns the stdout
of the executed command. If the execution fails, the macro returns an Err
of type ShellError
. One can handle the errors using the question mark operator:
exec!("10874-26631-30577", false, "ls")?`
The cmd
argument is a format
sting, i.e., one can use positional and named arguments as well as variable names:
let path="/tmp";
exec!("17068-22053-696", "ls {path}")`
As for macro format!
, macro exec!
supports positional arguments:
// example: with position argument "/"
println!("ls of {path} is {}", exec!("15911-12192-19189", false, "ls {}", "/")?);
exec!
also supports named arguments:
// example: with named argument p="/tmp"
println!("ls of {path} is {}", exec!("15911-12192-19189", false, "ls {p}", p="/tmp")?);
Macro s!
Macro s!
is similar to macro exec!
: s!
uses crate log
to issue log output instead of eprintln!
.
Hence, it does not have a flag verbose
. Moreover, it
- logs the executed command with all arguments at
info
level, - logs the output, i.e., the
stdout
of the command, atdebug
level, and - logs errors, i.e., the
stderr
of the command, aterror
level.
On success of the executed command, sh!
returns the stdout
of the executed command wrapped in Ok(stdout)
.
Example
You can use macro s!
as follows:
// s! the command is executed and the output is returned
// s! uses the logger to print the command if the log level is set to info
// s! uses the logger to print the output of the command if the log level is set to debug
s!("14526-30026-17058", "echo Hello World")?;
On error, s!
, logs an error that includes:
- the command line that failed,
- the error ID,
- the stdout,
- the stderr,
You can combine macro s!
with crate anyhow
to add more context to error messages.
use sh_exec::*;
// show how to use together with anyhow
use anyhow::*;
fn main() -> Result<()> {
env_logger::init();
// example: ls of /tmp
let path="/etc";
println!("- ls of {path} is {}", exec!("17068-22053-696", true, "ls {path}").with_context(|| format!("Very unexpected - ls failed on {path}"))?);
// example: with position argument "/"
println!("ls of {path} is {}", s!("15911-12192-19189", "ls {}", "/").with_context(|| "Very unexpected - ls failed on /".to_string())?);
// Explicit error handling
match s!("28328-2323-3278", "nonexistent_command").with_context(|| "Failed to execute command 'nonexistent_command'".to_string()) {
std::result::Result::Ok(output) => println!("Unexpected success: {}", output),
Err(e) => println!("Expected error: {}", e),
}
Ok(())
}
Macro a!
Macro a!
is similar to macro s!
but it has a timeout argument. If the command does not
finish in time, a timeout is returned.
Example:
// macro a! provides timeouts and it will return with a Timeout error
// if the command does not finish in time
let ten_secs = time::Duration::from_secs(3);
println!("sleep = {:?}", a!("14526-30888026-777", ten_secs, "sleep 2; echo Hello World"));
rust-script
Example
Here is a simple program that uses this crate. Note that you need to define dependency sh-exec
to import this crate and additionally dependencies colored
, and log
in your Cargo.toml:
[dependencies]
sh-exec = "*"
colored = "*"
log = "*"
and in your Rust program, you import the macros as follows:
use sh_exec::*;
You can use this crate also from within Rust scripts:
#!/usr/bin/env rust-script
//! ```cargo
//! [package]
//! name = "example"
//! edition = "2024"
//!
//! [dependencies]
//! sh-exec = "*"
//! colored = "*"
//! log = "*"
//! ```
use sh_exec::*;
fn main() {
trap_panics_and_errors!("18428-30925-25863", || {
// example: ls of /tmp
let path="/etc";
exec!("17068-22053-696", true, "ls -d {path}")?;
// example: with position argument "/"
println!("ls -d of / is {}", exec!("15911-12192-19189", false, "ls -d {}", "/")?);
// example: with named argument p="/tmp"
println!("ls of /etc/hosts is {}", s!("15911-12192-19189", "ls {p}", p="/etc/hosts")?);
// Test successful command
let output = exec!("28328-2323-44343", true, "bash -c 'echo Hello World'")?;
println!("Output: {}", output);
// Test failing command
match exec!("28328-2323-3278", true, "nonexistent_command") {
Ok(output) => println!("Unexpected success: {}", output),
Err(e) => println!("Expected error: {}", e),
}
// expecting to fail:
exec!( "28328-2323-333", true, "nonexistent_command arg1 arg2")?;
// We need to help Rust regarding the error type
Ok::<(), Box<dyn Error>>(())
});
}
Executing the code as a rust-script
(see file example.rs
), we get the following output:
$ ./example.rs
exec!(17068-22053-696,ls -d /etc)
ls -d of / is /
ls of /etc/hosts is /etc/hosts
exec!(28328-2323-44343,bash -c 'echo Hello World')
Output: Hello World
exec!(28328-2323-3278,nonexistent_command)
Expected error: Command failed: 'nonexistent_command'
sh_exec Exit code: 127
sh_exec Error ID: 28328-2323-3278
Standard error:
sh: 1: nonexistent_command: not found
exec!(28328-2323-333,nonexistent_command arg1 arg2)
Dependencies
~5–14MB
~193K SLoC