-
Notifications
You must be signed in to change notification settings - Fork 39
Home
cmd_lib lets you write shell-script-like code in Rust with compile-time safety and proper error handling. By default, it behaves like bash with set -euo pipefail - commands fail fast and errors propagate. You can check some real project examples, where rust is being used for xtask and DevOps.
| Bash | Rust (cmd_lib) |
|---|---|
| Error Handling | |
set -euo pipefail |
Built-in default |
echo "Error at $LINENO" |
Automatic (includes cmd + file:line) |
set -x / set +x
|
ScopedDebug::set(true) / drops |
set -x (env var) |
RUST_LOG=debug RUST_CMD_LIB_DEBUG=1 ./program |
set -o pipefail |
ScopedPipefail::set(true) |
cmd || true |
run_cmd!(ignore cmd)? |
| Commands | |
cmd arg1 arg2 |
run_cmd!(cmd arg1 arg2)? |
output=$(cmd) |
let output = run_fun!(cmd)? |
cmd & |
spawn!(cmd)? |
| Pipes | |
cmd1 | cmd2 |
run_cmd!(cmd1 | cmd2)? |
cmd1 | cmd2 | cmd3 |
run_cmd!(cmd1 | cmd2 | cmd3)? |
cmd1 |& cmd2 |
run_cmd!(cmd1 |& cmd2)? |
| Redirections | |
cmd > file |
run_cmd!(cmd > file)? |
cmd >> file |
run_cmd!(cmd >> file)? |
cmd < file |
run_cmd!(cmd < file)? |
cmd 2> file |
run_cmd!(cmd 2> file)? |
cmd 2>&1 |
run_cmd!(cmd 2>&1)? |
cmd 1>&2 |
run_cmd!(cmd 1>&2)? |
cmd 1>&2 |
run_cmd!(cmd 1>&2)? |
| Variables | |
echo $var |
run_cmd!(echo $var)? |
echo ${var} |
run_cmd!(echo ${var})? |
echo '$var' (literal) |
run_cmd!(echo r"$var")? |
VAR=val cmd |
run_cmd!(VAR=val cmd)? |
export VAR=val |
std::env::set_var("VAR", "val") |
| Directory | |
cd dir; cmd; cd - |
run_cmd!{ cd dir; cmd; }? (auto-restores) |
| Global Variables | |
VAR=value (global) |
tls_init!(VAR, Type, default) |
echo $VAR |
tls_get!(VAR) |
VAR=new_value |
tls_set!(VAR, |v| *v = new_value) |
Bash:
ls -la
echo "hello world"Rust:
run_cmd!(ls -la)?;
run_cmd!(echo "hello world")?;Bash:
version=$(rustc --version)
count=$(wc -l < file.txt)Rust:
let version = run_fun!(rustc --version)?;
let count = run_fun!(wc -l < file.txt)?;Bash:
name="world"
echo "hello $name"
echo "path is ${HOME}/bin"Rust:
let name = "world";
run_cmd!(echo "hello $name")?;
let home = std::env::var("HOME")?;
run_cmd!(echo "path is ${home}/bin")?;Vector expansion (no bash equivalent):
let args = vec!["-l", "-a", "/tmp"];
run_cmd!(ls $[args])?; // expands to: ls -l -a /tmpRaw strings (disable interpolation):
run_cmd!(echo r"$HOME")?; // prints literal $HOMEBash:
cat file.txt | grep pattern | wc -l
ps aux | grep nginx | awk '{print $2}'Rust:
run_cmd!(cat file.txt | grep pattern | wc -l)?;
let pids = run_fun!(ps aux | grep nginx | awk r"{print $2}")?;Bash:
echo "text" > output.txt
echo "more" >> output.txt
cmd 2>/dev/null
cmd 2>&1
cmd &> all.log
cmd < input.txtRust:
run_cmd!(echo "text" > output.txt)?;
run_cmd!(echo "more" >> output.txt)?;
run_cmd!(cmd 2>/dev/null)?;
run_cmd!(cmd 2>&1)?;
run_cmd!(cmd &> all.log)?;
run_cmd!(cmd < input.txt)?;Bash (set -e is default in cmd_lib):
set -e
cmd1
cmd2 # won't run if cmd1 failsRust (default behavior):
run_cmd! {
cmd1;
cmd2; // won't run if cmd1 fails
}?;Automatic error reporting:
Unlike bash, cmd_lib automatically includes the failed command and source location in error messages:
Error: Running ["ls" "/nonexistent"] exited with error; status code: 2
at src/main.rs:15
No need for manual echo "Error at line $LINENO" - it's built-in.
Bash (ignore errors):
rm -f file.txt || true
set +e
risky_command
set -eRust:
run_cmd!(ignore rm -f file.txt)?;
// or in a block:
run_cmd! {
ignore risky_command;
next_command;
}?;Bash (pipefail):
set -o pipefail
false | true # fails
set +o pipefail
false | true # succeedsRust:
// pipefail is ON by default
run_cmd!(false | true)?; // fails
// disable temporarily:
let _pf = ScopedPipefail::set(false);
run_cmd!(false | true)?; // succeeds
// pipefail restored when _pf dropsBash:
cd /tmp
ls
cd -Rust (cd is scoped - auto restores):
run_cmd! {
cd /tmp;
ls;
}?;
// automatically back to original dirBash:
export FOO=bar
FOO=bar ./script.shRust:
std::env::set_var("FOO", "bar");
run_cmd!(FOO=bar ./script.sh)?; // per-command envBash:
{
echo "step 1"
echo "step 2"
ls /tmp
}Rust:
run_cmd! {
echo "step 1";
echo "step 2";
ls /tmp;
}?;Bash:
long_running_cmd &
pid=$!
# do other work
wait $pidRust:
let mut proc = spawn!(long_running_cmd)?;
// do other work
proc.wait()?;
// or with output capture:
let mut proc = spawn_with_output!(cmd)?;
let output = proc.wait_with_output()?;Bash:
set -x
ls /tmp
set +xRust (in code):
// Global (affects all threads)
cmd_lib::set_debug(true);
run_cmd!(ls /tmp)?;
cmd_lib::set_debug(false);
// Scoped (thread-local, auto-restores)
{
let _debug = ScopedDebug::set(true);
run_cmd!(ls /tmp)?; // prints: [DEBUG] Running ["ls" "/tmp"] ...
}
// debug mode restored when _debug dropsRust (via environment variables):
# Enable debug output at runtime without code changes
RUST_CMD_LIB_DEBUG=1 ./my_program
# With full log output (requires env_logger init)
RUST_LOG=debug RUST_CMD_LIB_DEBUG=1 ./my_programBash:
COUNT=0
increment() {
COUNT=$((COUNT + 1))
}
increment
echo $COUNTRust (thread-local variables):
use cmd_lib::*;
tls_init!(COUNT, i32, 0);
fn increment() {
tls_set!(COUNT, |c| *c += 1);
}
fn main() {
increment();
println!("{}", tls_get!(COUNT)); // prints: 1
}Bash:
echo "INFO: Starting process"
echo "ERROR: Failed" >&2Rust:
run_cmd!(info "Starting process")?;
run_cmd!(error "Failed")?;
// Also: warn, debug, traceBash script:
#!/bin/bash
set -euo pipefail
DIR="/tmp/test"
FILES=$(find . -name "*.txt")
mkdir -p "$DIR"
cd "$DIR"
for f in $FILES; do
cp "$f" .
done
tar czf archive.tar.gz *.txt
echo "Done: $(ls *.txt | wc -l) files archived"Rust equivalent:
use cmd_lib::*;
use glob::glob;
#[cmd_lib::main]
fn main() -> CmdResult {
let dir = "/tmp/test";
// Use glob crate instead of find with wildcard
let files: Vec<String> = glob("./**/*.txt")
.expect("Failed to read glob pattern")
.filter_map(|e| e.ok())
.map(|p| p.display().to_string())
.collect();
run_cmd!(mkdir -p $dir)?;
for f in &files {
run_cmd!(cd $dir; cp $f .)?;
}
// Get txt files in target dir for tar
let txt_files: Vec<String> = glob(&format!("{}/*.txt", dir))
.expect("Failed to read glob pattern")
.filter_map(|e| e.ok())
.map(|p| p.display().to_string())
.collect();
if !txt_files.is_empty() {
run_cmd!(cd $dir; tar czf archive.tar.gz $[txt_files])?;
}
let count = txt_files.len();
run_cmd!(info "Done: $count files archived")?;
Ok(())
}- No shell injection - Variables are parsed before substitution
- Compile-time checking - Syntax errors caught at compile time
- Scoped cd - Directory changes are automatically restored
- Type-safe - Rust's type system prevents many errors
- No glob expansion - Use external commands or Rust's glob crate
-
Explicit error handling - Use
?orignorekeyword - Automatic error context - Failed commands report command + source location