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.
Builder API for dynamic commands#
Build a parser at runtime when the available options or subcommands depend on data.
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}");
}cargo run -- Berlin --retries 2 --dry-run
# Berlin 2 trueDerive 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.
Customize a derived command#
Keep a typed struct while adjusting the generated help or parser.
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);
}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.
Advanced argument shapes#
Use these parser switches when a simpler positional or option does not fit.
| Need | Approach | Example |
|---|---|---|
| A count flag | ArgAction::Count | -vvv becomes 3. |
| An optional value after a flag | num_args = 0..=1 plus default_missing_value | --color and --color=always. |
| Several values for one option | num_args = 2 | --range 1 10. |
| A final pass-through argument list | trailing_var_arg or last on a positional | Pass arguments to another process. |
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);
}cargo run -- # None
cargo run -- --color # Some("always")
cargo run -- --color=never # Some("never")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.
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.
cargo add serde --features derive
cargo add serde_jsonuse 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(())
}cargo run -- 'Ada "Lovelace"' --json
# {"greeting":"Hello","name":"Ada \"Lovelace\""}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.
cargo add anyhowuse 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(())
}cargo run -- missing.txt
# Error: could not read missing.txt
# Caused by: No such file or directory (os error 2)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.
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.
cargo add clap-verbosity-flag env_logger loguse 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);
}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-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.
Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice