Building a CLI Tool in Rust
A deep dive into building command-line tools with Rust — from argument parsing with clap to colored output and error handling.
Building a CLI Tool in Rust#
Rust is arguably the best language for building CLI tools today. The combination of zero-cost abstractions, great error handling, and an ecosystem of libraries like clap and colored makes it a joy to work with.
Why Rust for CLIs?#
- Single binary — no runtime dependencies, just ship the executable
- Fast startup — no JIT warmup, no garbage collector pause
- Cross-platform — compile for Linux, macOS, and Windows from one codebase
- Memory safe — no segfaults in production
Setting up the project#
cargo init my-cli
cd my-cli
cargo add clap --features derive
cargo add anyhow coloredArgument parsing with clap#
The derive macro approach makes argument parsing declarative:
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "my-cli", about = "A demo CLI tool")]
struct Args {
/// Input file to process
#[arg(short, long)]
input: String,
/// Enable verbose output
#[arg(short, long, default_value_t = false)]
verbose: bool,
/// Output format
#[arg(short, long, default_value = "json")]
format: String,
}Processing input#
Here's where the real logic lives. We read the input file, process it, and write the output:
use anyhow::{Context, Result};
use std::fs;
fn process_file(path: &str, verbose: bool) -> Result<String> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path))?;
if verbose {
eprintln!("Read {} bytes from {}", content.len(), path);
}
// Your processing logic here
Ok(content.to_uppercase())
}Notice how we use anyhow::Context to add meaningful error messages. This pattern is covered in more detail in my post on Rust error handling.
Colored output#
Making your CLI output readable with colored:
use colored::Colorize;
fn print_result(result: &str, format: &str) {
println!("{} Processing complete", "✓".green().bold());
println!("{}: {}", "Format".cyan(), format);
println!("{}: {} bytes", "Output size".cyan(), result.len());
}Putting it all together#
fn main() -> Result<()> {
let args = Args::parse();
let result = process_file(&args.input, args.verbose)?;
print_result(&result, &args.format);
Ok(())
}What's next#
This is just the foundation. From here you can add:
- Subcommands for different operations
- Progress bars with
indicatif - Configuration files with
serdeandtoml - Shell completions generated by clap
The Rust CLI ecosystem is rich and well-documented. Start small, ship fast, iterate.