Command Parsing: Shell to Rust
Bashrs converts shell command invocations, pipe chains, and environment variable
access into typed Rust equivalents using std::process::Command and iterator
chains.
Simple Commands
Bash
docker build -t myapp:latest .
Rust
#![allow(unused)]
fn main() {
use std::process::Command;
let status = Command::new("docker")
.args(["build", "-t", "myapp:latest", "."])
.status()?;
}
Each shell command becomes a Command::new call. Arguments are passed as a
slice, avoiding shell injection vulnerabilities that arise from string
interpolation in Bash.
Pipe Chains
Bash
cat access.log | grep "ERROR" | awk '{print $4}' | sort | uniq -c | sort -rn
Rust (process pipes)
#![allow(unused)]
fn main() {
use std::process::{Command, Stdio};
let grep = Command::new("grep")
.arg("ERROR")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let awk = Command::new("awk")
.arg("{print $4}")
.stdin(grep.stdout.unwrap())
.stdout(Stdio::piped())
.spawn()?;
}
For pipelines that process text, bashrs can also convert to pure Rust iterator chains, eliminating external process overhead:
Rust (iterator chain)
#![allow(unused)]
fn main() {
use std::fs;
let content = fs::read_to_string("access.log")?;
let mut counts: HashMap<String, usize> = HashMap::new();
for line in content.lines().filter(|l| l.contains("ERROR")) {
if let Some(field) = line.split_whitespace().nth(3) {
*counts.entry(field.to_string()).or_default() += 1;
}
}
let mut sorted: Vec<_> = counts.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1));
}
The iterator version is typically faster because it avoids spawning four separate processes and piping data through the kernel.
Environment Variables
Bash
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
CONNECTION="postgresql://${DB_HOST}:${DB_PORT}/mydb"
Rust
#![allow(unused)]
fn main() {
use std::env;
let db_host = env::var("DB_HOST").unwrap_or_else(|_| "localhost".into());
let db_port = env::var("DB_PORT").unwrap_or_else(|_| "5432".into());
let connection = format!("postgresql://{db_host}:{db_port}/mydb");
}
For CLI tools, bashrs promotes environment variables to typed clap arguments
with env attributes, providing both flag and env-var access:
#![allow(unused)]
fn main() {
#[derive(clap::Parser)]
struct Config {
#[arg(long, env = "DB_HOST", default_value = "localhost")]
db_host: String,
#[arg(long, env = "DB_PORT", default_value_t = 5432)]
db_port: u16, // Typed as integer, not string
}
}
Command Substitution
Bash
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "On branch: ${CURRENT_BRANCH}"
Rust
#![allow(unused)]
fn main() {
let output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.output()?;
let current_branch = String::from_utf8(output.stdout)?
.trim()
.to_string();
println!("On branch: {current_branch}");
}
Command::output() captures both stdout and stderr. The output is explicit
bytes that must be decoded, catching encoding issues that Bash would silently
pass through.
Conditional Execution
Bash
command -v docker >/dev/null 2>&1 || { echo "docker not found"; exit 1; }
Rust
#![allow(unused)]
fn main() {
use which::which;
if which("docker").is_err() {
eprintln!("docker not found");
std::process::exit(1);
}
}
The which crate provides cross-platform command detection, replacing the
Bash-specific command -v builtin.
Key Takeaways
- Shell commands become
Command::newwith typed argument slices, eliminating injection risks. - Pipe chains can remain as process pipes or convert to iterator chains for better performance.
- Environment variables with defaults map to
claparguments withenvattributes and typed parsing. - Command substitution uses
Command::output()with explicit encoding.
Navigate: Table of Contents