Building Command-Line Apps
Prerequisites: Writing tests
Command-line tools are one of the most common things people build in Rust: the binaries start instantly, have no runtime to install, and are easy to ship as a single file. A CLI program needs three things: read its arguments, report errors on stderr, and exit with a meaningful exit code (0 for success, non-zero for failure) so scripts can react to it.
Syntax
std::env::args() returns an iterator over the arguments as Strings. The
first item is the program's own path, so you usually skip it.
use std::env;
use std::process;
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
if args.is_empty() {
eprintln!("usage: greet <name> [times]"); // eprintln! writes to stderr
process::exit(2); // exit immediately with code 2
}
println!("got {} argument(s)", args.len());
}
process::exit(code) ends the program right away, without running
destructors, so call it only once there is nothing left to clean up — usually
at the top of main. When main returns Err, Rust exits with code 1.
Example
Parsing arguments by hand, in src/main.rs of a package named greet:
use std::env;
use std::process;
struct Config {
name: String,
times: u32,
}
fn parse_args(args: &[String]) -> Result<Config, String> {
match args {
[name] => Ok(Config { name: name.clone(), times: 1 }),
[name, times] => {
let times = times
.parse()
.map_err(|_| format!("'{times}' is not a valid number"))?;
Ok(Config { name: name.clone(), times })
}
_ => Err(String::from("usage: greet <name> [times]")),
}
}
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
let config = parse_args(&args).unwrap_or_else(|err| {
eprintln!("error: {err}");
process::exit(2);
});
for i in 1..=config.times {
println!("{i}: Hello, {}!", config.name);
}
}
Arguments after -- are passed to your program rather than to Cargo:
cargo run -- Ada 2
Expected output
1: Hello, Ada!
2: Hello, Ada!
cargo run -- Ada two prints error: 'two' is not a valid number to stderr
and exits with status 2.
Using clap
Hand-written parsing gets tedious once you want flags, defaults and a
--help page. clap is the most widely used argument parser. With its
derive API you describe the arguments as a struct, and doc comments become
the help text.
cargo add clap --features derive
use clap::Parser;
/// Print a friendly greeting.
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
/// Who to greet
name: String,
/// How many times to print the greeting
#[arg(short = 'n', long, default_value_t = 1)]
times: u8,
/// Print in upper case
#[arg(short, long)]
shout: bool,
}
fn main() {
let args = Args::parse(); // on bad input: prints an error and exits with code 2
let mut line = format!("Hello, {}!", args.name);
if args.shout {
line = line.to_uppercase();
}
for _ in 0..args.times {
println!("{line}");
}
}
- A plain field (
name) is a required positional argument. #[arg(short, long)]creates-s/--shout; aboolfield is a flag that istruewhen present.default_value_tgives a default;Option<T>makes an argument optional.- clap converts each value to the field's type and reports a clear error if that fails.
Running cargo run -- Ada -n 2 --shout prints:
HELLO, ADA!
HELLO, ADA!
And cargo run -- --help prints the generated help page (the program name in
the Usage line comes from how the binary was invoked, so on Windows it may
show as greet.exe):
Print a friendly greeting
Usage: greet [OPTIONS] <NAME>
Arguments:
<NAME> Who to greet
Options:
-n, --times <TIMES> How many times to print the greeting [default: 1]
-s, --shout Print in upper case
-h, --help Print help
-V, --version Print version
Common mistakes
- Forgetting that
args[0]is the program path, then treating it as the first real argument. Use.skip(1). - Indexing
args[1]without checking the length. It panics withindex out of boundswhen the user passes nothing; match on the slice or checklen()first. - Printing errors with
println!. Errors belong on stderr (eprintln!), so they are still visible when normal output is redirected to a file. - Exiting with code 0 after an error. Scripts and CI rely on a non-zero code to notice failures.
- Using clap's derive attributes without the
derivefeature:error: cannot find derive macro `Parser` in this scope. - Running
cargo run --shoutand wondering why Cargo complains about an unexpected argument. Put your program's arguments after--.
Practice
Using clap, write a tool sum that takes one or more integers as positional
arguments (Vec<i64>, marked #[arg(required = true)]) and a --mean flag.
It prints the sum, or the mean with two decimals when --mean is given.
cargo run -- 3 4 8 --mean should print mean = 5.00.
Show solution
use clap::Parser;
/// Add up numbers.
#[derive(Parser)]
struct Args {
/// Numbers to add
#[arg(required = true)]
numbers: Vec<i64>,
/// Print the mean instead of the sum
#[arg(long)]
mean: bool,
}
fn main() {
let args = Args::parse();
let total: i64 = args.numbers.iter().sum();
if args.mean {
let mean = total as f64 / args.numbers.len() as f64;
println!("mean = {mean:.2}");
} else {
println!("sum = {total}");
}
}
mean = 5.00