Rust Clap in Depth
Clap's advanced features handle more argument shapes. A setting may come from the environment.
A path stays a path
A PathBuf field tells clap the value is a file path. Your code gets a real path. It does not get plain text.
/// Root folder to serve
root: PathBuf,Reject a bad port early
A range on the value parser rejects bad numbers. Run with --port 0 and clap stops the program. The first error line is:
#[arg(long, value_parser = clap::value_parser!(u16).range(1..=65535), default_value_t = 8080)]
port: u16,Plug in your own parser
The value_parser part can name a function of yours. The field then holds whatever your function returns.
/// Delay before answering, like 30s or 2m
#[arg(long, value_parser = parse_delay)]
delay: Option<u64>,The parser is a plain function
It takes text. It returns a Result. The Err text becomes part of clap's error line.
fn parse_delay(text: &str) -> Result<u64, String> {
let (num, mult) = match text.chars().last() {
Some('s') => (&text[..text.len() - 1], 1),
Some('m') => (&text[..text.len() - 1], 60),
_ => return Err(format!("bad delay: {text}")),
};
num.parse::<u64>().map(|n| n * mult).map_err(|_| format!("bad delay: {text}"))
}A setting from the environment
The env part names a variable. Run with APP_NAME=docs and no flag. The plan prints the variable's value:
/// Name for the banner (or set APP_NAME)
#[arg(long, env = "APP_NAME", default_value = "files")]
name: String,Repeat an option
ArgAction::Append collects every use into a Vec. Run with --tag a --tag b. The plan counts both tags:
/// A tag to attach; repeat the flag for more tags
#[arg(long, action = clap::ArgAction::Append)]
tag: Vec<String>,Example
// Cargo.toml: clap = { version = "4", features = ["derive", "env"] }
use clap::Parser;
use std::path::PathBuf;
/// Print a server plan built from flags, the environment, and defaults
#[derive(Parser)]
struct Cli {
/// Root folder to serve
root: PathBuf,
/// Port to listen on
#[arg(long, value_parser = clap::value_parser!(u16).range(1..=65535), default_value_t = 8080)]
port: u16,
/// Delay before answering, like 30s or 2m
#[arg(long, value_parser = parse_delay)]
delay: Option<u64>,
/// Name for the banner (or set APP_NAME)
#[arg(long, env = "APP_NAME", default_value = "files")]
name: String,
/// A tag to attach; repeat the flag for more tags
#[arg(long, action = clap::ArgAction::Append)]
tag: Vec<String>,
}
fn parse_delay(text: &str) -> Result<u64, String> {
let (num, mult) = match text.chars().last() {
Some('s') => (&text[..text.len() - 1], 1),
Some('m') => (&text[..text.len() - 1], 60),
_ => return Err(format!("bad delay: {text}")),
};
num.parse::<u64>().map(|n| n * mult).map_err(|_| format!("bad delay: {text}"))
}
fn main() {
let cli = Cli::parse();
println!("root: {}", cli.root.display());
println!("port: {}", cli.port);
println!("name: {}", cli.name);
match cli.delay {
Some(secs) => println!("delay: {secs}s"),
None => println!("delay: none"),
}
println!("tags: {}", cli.tag.len());
}Key points
- Add clap with two features for this lesson: clap = { version = "4", features = ["derive", "env"] }.
- Without the env feature, the env attribute does not compile.
- Precedence is fixed.
- A typed flag beats the environment.
- The environment beats the default.
- default_value_t takes a typed value, like 8080.
- default_value takes plain text, like "files".
- Add global = true to an argument.
- Every subcommand then accepts it too, even typed after the subcommand name.
- Your parser function runs inside Cli::parse().
- Bad input never reaches your main code.
- Clap's advanced features cover paths, environment settings, and repeated options.
In this example, the run is cargo run -- www --port 9000 --delay 2m --tag a --tag b. The word www fills the PathBuf root. The port passes the range check and replaces the 8080 default. parse_delay reads 2m and returns 120, so the plan prints delay: 120s. No --name flag is typed, and APP_NAME is not set, so the default files fills the name. The two --tag flags collect into one Vec, and the plan prints its length. Every value was checked before main ran a single line.
Note
A rejected value exits with code 2.
Clap prints the error and a hint to try --help.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
APP_NAME=docs is set in the terminal. The user types --name slides. Which name does the program use?