clap / Chapter 4 of 5

Builder API and advanced arguments

Use the clap builder API for dynamic commands and advanced arguments. Add JSON output, anyhow errors, and verbosity flags to a Rust CLI.

6 topicsUpdated
15 / 27

Builder API for dynamic commands#

Build a parser at runtime when the available options or subcommands depend on data.

RustA small builder-style parser
use clap::{Arg, ArgAction, Command, value_parser};

fn cli() -> Command {
    Command::new("ship")
        .about("Send a package")
        .arg(Arg::new("destination").required(true))
        .arg(Arg::new("retries")
            .long("retries")
            .default_value("3")
            .value_parser(value_parser!(u8)))
        .arg(Arg::new("dry-run")
            .long("dry-run")
            .action(ArgAction::SetTrue))
}

fn main() {
    let matches = cli().get_matches();
    let destination = matches.get_one::<String>("destination").unwrap();
    let retries = matches.get_one::<u8>("retries").unwrap();
    let dry_run = matches.get_flag("dry-run");
    println!("{destination} {retries} {dry_run}");
}
ShellTry it
cargo run -- Berlin --retries 2 --dry-run
# Berlin 2 true

Derive and builder configure the same Command machinery. Builder uses string argument IDs and typed lookups; the type you request from ArgMatches must match the parser configured for that argument. Prefer derive for a fixed CLI and builder for dynamic composition.

16 / 27

Customize a derived command#

Keep a typed struct while adjusting the generated help or parser.

RustUse CommandFactory with the builder API
use clap::{CommandFactory, FromArgMatches, Parser};

#[derive(Debug, Parser)]
struct Cli {
    #[arg(long)]
    color: bool,
}

fn main() {
    let cmd = Cli::command()
        .after_help("Examples:\n  painter --color");
    let matches = cmd.get_matches();
    let cli = Cli::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
    println!("color={}", cli.color);
}
Why parse in two steps

Cli::command() gives you the generated builder. After changing it, parse the resulting matches with FromArgMatches; calling Cli::parse() again would discard the builder customization.

17 / 27

Advanced argument shapes#

Use these parser switches when a simpler positional or option does not fit.

NeedApproachExample
A count flagArgAction::Count-vvv becomes 3.
An optional value after a flagnum_args = 0..=1 plus default_missing_value--color and --color=always.
Several values for one optionnum_args = 2--range 1 10.
A final pass-through argument listtrailing_var_arg or last on a positionalPass arguments to another process.
RustAn option with an optional value
use clap::Parser;

#[derive(Parser, Debug)]
struct Cli {
    #[arg(long, num_args = 0..=1, default_missing_value = "always")]
    color: Option<String>,
}

fn main() {
    println!("{:?}", Cli::parse().color);
}
ShellOutput
cargo run --                # None
cargo run -- --color        # Some("always")
cargo run -- --color=never  # Some("never")
Pass-through commands

The -- separator ends option parsing. When forwarding arbitrary child-process arguments, keep them as OsString and choose trailing_var_arg or last based on whether you require an explicit separator.

18 / 27

serde_json for machine-readable output#

Produce valid JSON from real data without manually escaping strings.

Add serde with derive support and serde_json. Serialize a struct, then write one JSON document to standard output. Put progress messages on standard error.

ShellAdd the packages
cargo add serde --features derive
cargo add serde_json
RustA safe JSON result
use clap::Parser;
use serde::Serialize;
use std::io::{self, Write};

#[derive(Parser)]
struct Cli {
    name: String,
    #[arg(long)]
    json: bool,
}

#[derive(Serialize)]
struct Greeting<'a> {
    greeting: &'a str,
    name: &'a str,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();
    if cli.json {
        let value = Greeting { greeting: "Hello", name: &cli.name };
        serde_json::to_writer(io::stdout().lock(), &value)?;
        println!();
    } else {
        writeln!(io::stdout().lock(), "Hello, {}!", cli.name)?;
    }
    Ok(())
}
ShellSpecial characters remain valid JSON
cargo run -- 'Ada "Lovelace"' --json
# {"greeting":"Hello","name":"Ada \"Lovelace\""}
19 / 27

anyhow for work failures#

Give file and network errors useful context after argument parsing succeeds.

anyhow is a general-purpose error package. Its Context trait adds the operation and path to an underlying error. Clap still handles CLI syntax; anyhow handles what went wrong while doing the work.

ShellAdd the package
cargo add anyhow
RustShow which file could not be read
use std::{fs, path::PathBuf};
use anyhow::{Context, Result};
use clap::Parser;

#[derive(Parser)]
struct Cli {
    input: PathBuf,
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    let contents = fs::read_to_string(&cli.input)
        .with_context(|| format!("could not read {}", cli.input.display()))?;
    println!("{} bytes", contents.len());
    Ok(())
}
ShellFailure
cargo run -- missing.txt
# Error: could not read missing.txt
# Caused by: No such file or directory (os error 2)
Quiet machine output

Rust prints a returned main() -> Result error to stderr and exits unsuccessfully. If you need a strict error format, handle the error in main, write to stderr yourself, and choose your exit code.

20 / 27

clap_verbosity_flag for -v and -q#

Add familiar verbosity and quiet controls without rewriting level logic.

clap-verbosity-flag supplies -v/--verbose and -q/--quiet. Flatten its Verbosity into your CLI. With its default log integration, you can configure env_logger.

ShellAdd the packages
cargo add clap-verbosity-flag env_logger log
RustVerbose logging that stays off stdout
use clap::Parser;
use clap_verbosity_flag::Verbosity;

#[derive(Parser)]
struct Cli {
    #[command(flatten)]
    verbosity: Verbosity,
    input: String,
}

fn main() {
    let cli = Cli::parse();
    env_logger::Builder::new()
        .filter_level(cli.verbosity.log_level_filter())
        .init();
    log::info!("processing {}", cli.input);
    println!("done: {}", cli.input);
}
ShellControl log volume
cargo run -- data.txt       # errors only by default
cargo run -- -v data.txt    # warnings as well
cargo run -- -vv data.txt   # info as well
cargo run -- -q data.txt    # disable logs
Logging is separate from output

-q controls logs from the logger, not a println! that writes the requested result. Decide separately whether quiet mode should suppress your own progress messages.