CLIs are where Rust really shines: fast startup, single binary, no runtime. Here’s how to build one properly.
Setup
[dependencies]
clap = { version = "4", features = ["derive"] }
anyhow = "1"
The derive API
clap 4’s derive API is the way to go. You define your CLI as structs, clap handles parsing:
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "mytool", version, about = "Does useful things")]
struct Cli {
/// Enable verbose output
#[arg(short, long)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Initialize a new project
Init {
#[arg(help = "Project name")]
name: String,
},
/// Run the project
Run {
#[arg(short, long, default_value = "8080")]
port: u16,
},
}
Main entry point
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Init { name } => init(&name, cli.verbose),
Commands::Run { port } => run(port, cli.verbose),
}
}
One tip: separate your logic from CLI parsing
The main function should only parse args and dispatch. Keep your actual logic in functions that take plain types — not clap types. This makes your logic testable without invoking the CLI layer.
fn init(name: &str, verbose: bool) -> anyhow::Result<()> {
// real logic here, no clap dependency
}
You get a clean binary, auto-generated --help, shell completions via clap_complete, and a structure that stays maintainable as the CLI grows.
Comments