Why Rust for CLI Tools?
Rust is an excellent choice for CLI tools: fast startup, single binary distribution, cross-platform support, and excellent libraries. Tools like ripgrep, fd, bat, exa, and delta are all written in Rust and are faster than their traditional counterparts.
Argument Parsing with clap
// Cargo.toml:
// clap = { version = "4", features = ["derive"] }
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "mytool")]
#[command(version, about = "A powerful CLI tool built with Rust")]
struct Cli {
/// Input file to process
#[arg(short, long)]
input: String,
/// Output file (default: stdout)
#[arg(short, long)]
output: Option,
/// Enable verbose logging
#[arg(short, long, default_value_t = false)]
verbose: bool,
/// Number of threads to use
#[arg(short = 'j', long, default_value_t = 4)]
threads: usize,
#[command(subcommand)]
command: Option,
}
#[derive(Subcommand)]
enum Commands {
/// Process a file
Process {
/// Processing mode
#[arg(short, long, default_value = "fast")]
mode: String,
},
/// Validate input
Validate,
/// Show statistics
Stats {
/// Show detailed statistics
#[arg(short, long)]
detailed: bool,
},
}
fn main() {
let cli = Cli::parse();
if cli.verbose {
println!("Input: {}", cli.input);
println!("Threads: {}", cli.threads);
}
match &cli.command {
Some(Commands::Process { mode }) => {
println!("Processing in {mode} mode...");
}
Some(Commands::Validate) => {
println!("Validating...");
}
Some(Commands::Stats { detailed }) => {
println!("Stats (detailed={})", detailed);
}
None => {
println!("No subcommand — running default action");
}
}
}
// Usage:
// mytool --input data.txt --verbose process --mode fast
// mytool -i data.txt -j 8 validate
// mytool --help
Colored Output and Progress Bars
// Cargo.toml:
// colored = "2"
// indicatif = "0.17"
// console = "0.15"
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;
fn main() {
// Colored output
println!("{}", "Success!".green().bold());
println!("{}", "Warning: check config".yellow());
println!("{}", "Error: file not found".red().bold());
println!("{}", "Info: processing...".blue());
println!("{}", "Debug: value=42".dimmed());
// Progress bar
let pb = ProgressBar::new(100);
pb.set_style(ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})"
).unwrap().progress_chars("#>-"));
for _ in 0..100 {
pb.inc(1);
std::thread::sleep(Duration::from_millis(30));
}
pb.finish_with_message("Done!");
// Spinner for indeterminate tasks
let spinner = ProgressBar::new_spinner();
spinner.set_message("Connecting to server...");
for _ in 0..50 {
spinner.tick();
std::thread::sleep(Duration::from_millis(50));
}
spinner.finish_with_message("Connected!");
}
File I/O and stdin/stdout
use std::fs;
use std::io::{self, BufRead, Write, BufWriter, BufReader};
use std::path::PathBuf;
fn process_file(input: &str, output: Option<&str>) -> io::Result<()> {
// Read entire file
let content = fs::read_to_string(input)?;
println!("Read {} bytes", content.len());
// Read line by line (memory efficient for large files)
let file = fs::File::open(input)?;
let reader = BufReader::new(file);
let mut line_count = 0;
for line in reader.lines() {
let line = line?;
line_count += 1;
// Process each line
if line.contains("ERROR") {
eprintln!("Found error on line {line_count}: {line}");
}
}
// Write output
let mut writer: Box = match output {
Some(path) => Box::new(BufWriter::new(fs::File::create(path)?)),
None => Box::new(io::stdout().lock()),
};
writeln!(writer, "Processed {line_count} lines")?;
// Read from stdin
let stdin = io::stdin();
for line in stdin.lock().lines().take(5) {
let line = line?;
println!("Got: {line}");
}
Ok(())
}
fn main() {
if let Err(e) = process_file("input.txt", Some("output.txt")) {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Cross-Platform Distribution
# Build for release (optimized)
cargo build --release
# Cross-compile (with cross)
cargo install cross
cross build --release --target x86_64-unknown-linux-musl # Static Linux binary
cross build --release --target x86_64-pc-windows-gnu # Windows
cross build --release --target aarch64-apple-darwin # macOS ARM
# Install locally
cargo install --path .
# Publish to crates.io
cargo publish
# Reduce binary size
# In Cargo.toml:
# [profile.release]
# opt-level = "z" # Optimize for size
# lto = true # Link-time optimization
# strip = true # Strip debug symbols
# codegen-units = 1 # Single codegen unit
Key Takeaways
- ✅ Rust produces fast, single-binary CLI tools with no runtime dependencies
- ✅ clap provides powerful argument parsing with derive macros and subcommands
- ✅ colored and indicatif create beautiful terminal UIs with progress bars
- ✅ Use BufReader/BufWriter for efficient large-file processing
- ✅ Cross-compile to multiple platforms with the
crosstool