Rust CLI Patterns
clap with Derive API
[dependencies]
clap = { version = "4", features = ["derive", "env"] }
use clap::{Parser, Subcommand, Args};
#[derive(Parser, Debug)]
#[command(name = "mytool", version, about = "A powerful CLI tool")]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short, long, env = "MYTOOL_CONFIG", default_value = "~/.config/mytool.toml")]
config: std::path::PathBuf,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Process files
Process(ProcessArgs),
/// Show status
Status {
#[arg(short, long)]
detailed: bool,
},
}
#[derive(Args, Debug)]
struct ProcessArgs {
#[arg(required = true)]
files: Vec<std::path::PathBuf>,
#[arg(short, long, default_value = ".")]
output: std::path::PathBuf,
#[arg(short = 'j', long, default_value_t = 4)]
jobs: usize,
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Process(args) => process(args),
Commands::Status { detailed } => status(detailed),
}
}
Progress Output with indicatif
[dependencies]
indicatif = "0.17"
use indicatif::{ProgressBar, ProgressStyle};
fn process_with_progress(items: Vec<String>) {
let pb = ProgressBar::new(items.len() as u64);
pb.set_style(
ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}"
)
.unwrap()
.progress_chars("=>-"),
);
for item in &items {
pb.set_message(format!("processing {item}"));
do_work(item);
pb.inc(1);
}
pb.finish_with_message("done");
}
Colored Terminal Output
[dependencies]
colored = "2"
use colored::Colorize;
fn print_status(ok: bool, message: &str) {
if ok {
println!("{} {}", "✓".green().bold(), message);
} else {
eprintln!("{} {}", "✗".red().bold(), message);
}
}
Config File Handling
[dependencies]
config = "0.14"
serde = { version = "1", features = ["derive"] }
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Settings {
database_url: String,
port: u16,
workers: usize,
}
fn load_settings() -> anyhow::Result<Settings> {
let settings = config::Config::builder()
.set_default("port", 8080)?
.set_default("workers", 4)?
.add_source(config::File::with_name("config").required(false))
.add_source(config::Environment::with_prefix("APP").separator("__"))
.build()?;
Ok(settings.try_deserialize::<Settings>()?)
}
Interactive Prompts
[dependencies]
dialoguer = "0.11"
use dialoguer::{Confirm, Input, Select};
fn interactive_setup() -> anyhow::Result<()> {
let name: String = Input::new()
.with_prompt("Project name")
.default("my-project".into())
.interact_text()?;
let framework = Select::new()
.with_prompt("Select framework")
.items(&["axum", "actix-web", "warp"])
.default(0)
.interact()?;
let confirmed = Confirm::new()
.with_prompt(format!("Create project '{name}'?"))
.default(true)
.interact()?;
if confirmed { create_project(name, framework)?; }
Ok(())
}
Exit Codes and Error Reporting
fn main() {
if let Err(e) = run() {
eprintln!("error: {e:#}");
std::process::exit(1);
}
}
Common Anti-Patterns
- Parsing args manually — use
clap; it handles edge cases and generates help
- Writing errors to stdout — errors go to
stderr; use eprintln! or a logger
- Hard-coding config values — support env vars and config files from the start
- No progress feedback for long ops — users need to know the tool is working
- Panicking on invalid input — validate early with
clap validators or early returns