Build CLI Tools in Rust: From Zero to Working Binary in 10 Minutes
Why Rust is quietly eating the CLI world — and the 4 crates you need to ship your first tool today
Half the developer tools you love ripgrep, bat, fd, starship, zoxide, uv — are written in Rust. There’s a reason: Rust gives you a single static binary, near-C performance, and a type system that catches bugs before users do. No runtime, no node_modules, no pip install hell.
Here’s everything you need to build your own.
The toolkit
Four crates do 95% of the work:
Crate What it does clap Argument parsing with auto-generated --help anyhow Ergonomic error handling colored Terminal colors indicatif Progress bars and spinners
A working example in 40 lines
Let’s build wc-rs — a tiny replacement for Unix wc that counts lines, words, and characters in a file.
cargo new wc-rs
cd wc-rs
cargo add clap --features derive
cargo add anyhow coloredThen drop this into src/main.rs:
use anyhow::{Context, Result};
use clap::Parser;
use colored::Colorize;
use std::fs;
use std::path::PathBuf;
/// A tiny word counter, written in Rust.
#[derive(Parser)]
#[command(version, about)]
struct Args {
/// Path to the file to count
file: PathBuf,
/// Count lines
#[arg(short, long)]
lines: bool,
/// Count words
#[arg(short, long)]
words: bool,
/// Count characters
#[arg(short, long)]
chars: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let content = fs::read_to_string(&args.file)
.with_context(|| format!("failed to read {}", args.file.display()))?;
// If no flags passed, show everything.
let show_all = !(args.lines || args.words || args.chars);
if args.lines || show_all {
println!("{} {}", "lines:".green(), content.lines().count());
}
if args.words || show_all {
println!("{} {}", "words:".cyan(), content.split_whitespace().count());
}
if args.chars || show_all {
println!("{} {}", "chars:".yellow(), content.chars().count());
}
Ok(())
}Run it:
cargo run -- src/main.rs --words
# words: 87That’s it. You have a real CLI with colored output, --help, --version, proper error messages, and exit codes. All in 40 lines.
What’s actually happening here
#[derive(Parser)]turns a plain struct into a full argument parser.clapreads your doc comments and generates--helptext automatically.#[arg(short, long)]gives you both-land--linesfor free.anyhow::Result+.with_context()means errors carry useful messages instead of cryptic panics..green(),.cyan()fromcoloredmake output scannable without ANSI escape codes.
Shipping it
When you’re ready to share:
cargo build --releaseThe binary lands in target/release/wc-rs. It’s a single file. Copy it anywhere. No runtime required. Works on Linux, macOS, and Windows.
For broader distribution, publish to crates.io with cargo publish, or use cargo-dist to auto-build binaries for every platform via GitHub Actions.
Three patterns that level you up next
Subcommands —
clapsupportsgit-style subcommands (mytool add,mytool remove) with#[derive(Subcommand)].Interactive prompts —
dialoguergives you confirmations, selects, and password inputs in two lines.Progress bars —
indicatifadds beautiful spinners and bars for long-running tasks.
Master these and you can build anything from a deploy tool to a database migrator.
Building real things in Rust and sharing every step. Subscribe and build with me.

