CLI Basics
10 examples to get you started with Rust CLIs: 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with Rust CLIs: 7 basic and 3 intermediate.
cargo add clap --features derive anyhow for most examplesEvery CLI starts with a main that returns a result.
fn main() -> anyhow::Result<()> {
println!("Hello from mytool");
Ok(())
}Result so errors propagate cleanlyanyhow for application-level error handlingmain thin; delegate logic to library functions? operator for early exit with contextRelated: Error Reporting - friendly CLI errors
std::env::args works for the simplest tools.
use std::env;
fn main() -> anyhow::Result<()> {
let name = env::args().nth(1)
.ok_or_else(|| anyhow::anyhow!("usage: greet NAME"))?;
println!("Hello, {name}!");
Ok(())
}args().nth(0) is the binary nameanyhow does this automatically)clap for real toolsRelated: clap - proper argument parsing
Shell scripts and CI check $? after your command.
use std::process::ExitCode;
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) if e.is::<std::io::Error>() => ExitCode::from(2),
Err(_) => ExitCode::FAILURE,
}
}0 means success; distinct codes for distinct failures--helpResult; binaries map to exit codesCLIs compose via pipes when stdout stays clean.
use std::io::{self, BufRead};
fn main() -> anyhow::Result<()> {
let stdin = io::stdin();
for line in stdin.lock().lines() {
println!("{}", line?.to_uppercase());
}
Ok(())
}stdout for data output (pipeable)stderr for logs and diagnosticscat file.txt | mytool | otherOrganize verbs like git commit or cargo build.
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "items")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Add { name: String },
Remove { id: u64 },
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Add { name } => println!("added {name}"),
Command::Remove { id } => println!("removed {id}"),
}
Ok(())
}clap generates --help per subcommandRelated: clap - derive-based parsing
12-factor apps read config from the environment.
use std::env;
fn api_key() -> anyhow::Result<String> {
env::var("API_KEY").map_err(|_| anyhow::anyhow!("set API_KEY"))
}RUST_LOG for tracing in production CLIs--helpRelated: Configuration & Env - layering config
Put logic in src/lib.rs, keep src/main.rs as a thin wrapper.
# Cargo.toml
[lib]
name = "mytool"
path = "src/lib.rs"
[[bin]]
name = "mytool"
path = "src/main.rs"// src/lib.rs
pub fn process(input: &str) -> String {
input.to_uppercase()
}main only parses args and calls library functionsUse colored or owo-colors for human-facing messages.
use colored::Colorize;
fn main() {
eprintln!("{}", "error: file not found".red().bold());
println!("{}", "done".green());
}NO_COLOR and --no-colorRelated: Terminal Output - indicatif and styling
Show feedback when work takes more than a second.
use indicatif::{ProgressBar, ProgressStyle};
fn main() {
let bar = ProgressBar::new(100);
bar.set_style(ProgressStyle::with_template(
"[{bar:40}] {pos}/{len} {msg}",
).unwrap());
for i in 0..100 {
bar.set_message(format!("item {i}"));
bar.inc(1);
}
bar.finish_with_message("complete");
}--quiet or non-TTYRelated: Terminal Output - full guide
Ship a global binary with cargo install or release artifacts.
[package]
name = "mytool"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "mytool"
path = "src/main.rs"cargo install --path .
mytool --helprust-toolchain.toml for reproducible buildscargo-dist for cross-platform release archivesRelated: Packaging & Distribution - cargo-dist
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 19, 2026