Building CLI Tools with Rust
Rust is quietly becoming the go-to language for command-line tools. Here's why, and how to get started building your own.
If you’ve used any modern developer tools lately, there’s a good chance you’ve been running Rust without knowing it. Ripgrep, fd, bat, delta, zoxide — the list keeps growing. There’s a reason for this trend, and it’s not just hype.
Why Rust for CLIs?
Three things make Rust uniquely suited for command-line tools:
Instant startup. Rust compiles to native code with no runtime. Your tool starts in milliseconds, not seconds. This matters when a CLI is invoked hundreds of times per day.
Single binary distribution. cargo build --release gives you one file. No dependency hell, no “make sure you have Python 3.11 installed.” Copy the binary to a server and it runs.
Correctness by default. The type system and ownership model catch entire classes of bugs at compile time. For tools that process files, parse data, or manage system resources, this is a genuine advantage.
Getting started with clap
The clap crate is the standard for argument parsing. Here’s a minimal example:
use clap::Parser;
#[derive(Parser)]
#[command(name = "greet", about = "A friendly greeter")]
struct Cli {
/// Name of the person to greet
name: String,
/// Number of times to greet
#[arg(short, long, default_value_t = 1)]
count: u8,
}
fn main() {
let cli = Cli::parse();
for _ in 0..cli.count {
println!("Hello, {}!", cli.name);
}
}
That’s it. You get --help, error messages, and tab completion for free. The derive macro generates all the parsing logic at compile time.
Error handling with anyhow
CLI tools interact with the messy real world — files that don’t exist, network requests that fail, malformed input. The anyhow crate gives you ergonomic error handling:
use anyhow::{Context, Result};
use std::fs;
fn read_config(path: &str) -> Result<String> {
fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path))
}
The .with_context() method attaches human-readable messages to errors, so your users see “Failed to read config file: config.toml” instead of “No such file or directory.”
Colored output with owo-colors
A small touch that makes a big difference:
use owo_colors::OwoColorize;
println!("{} Configuration loaded", "✓".green());
println!("{} Missing API key", "✗".red());
The ecosystem is mature
Between clap, anyhow, serde for serialization, indicatif for progress bars, and dialoguer for interactive prompts, you can build a polished CLI tool in an afternoon. The compile times are the main downside — but for a tool you’ll ship as a single binary, that’s a trade-off worth making.
If you’re building something that needs to be fast, reliable, and easy to distribute, give Rust a serious look. The learning curve pays for itself quickly.