🎓 A1Learner
🔍

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?