Clap Rust
Use these conventions for Rust CLIs built with clap-rs/clap.
Source Baseline
- Prefer released docs from
docs.rs/clap, crates.io, and the matching GitHub release over older snippets. - Current docs.rs baseline checked for this skill:
clap 4.6.1. - Clap builds polished command-line parsers declaratively with derive or procedurally with the builder API.
- Prefer derive for static application CLIs. Use builder API when arguments are generated dynamically, when augmenting another parser, or when the CLI shape cannot be represented cleanly with derive.
Cargo
Use derive for most application CLIs:
[dependencies]
clap = { version = "4.6", features = ["derive", "env", "wrap_help"] }
Feature choices:
- Default features include
std,color,help,usage,error-context, andsuggestions. deriveenables#[derive(Parser)],Args,Subcommand, andValueEnum.cargoenables macros such ascommand!()and Cargo metadata helpers.envenables reading argument values from environment variables.unicodesupports Unicode in arguments and help.wrap_helpwraps help text based on terminal width.stringallows runtime-generated strings in builder configuration.- Avoid
unstable-v5unless the project explicitly opts into preview APIs.
Derive First
Use Parser for the top-level CLI, Args for reusable argument groups, Subcommand for command enums, and ValueEnum for finite sets of allowed values.
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[command(flatten)]
global: GlobalArgs,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Args)]
struct GlobalArgs {
/// Increase logging verbosity
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
/// Optional config file
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,
}
#[derive(Debug, Subcommand)]
enum Command {
/// Run the service
Serve {
#[arg(long, default_value_t = 8080)]
port: u16,
#[arg(long, value_enum, default_value = "fast")]
mode: Mode,
},
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
enum Mode {
Fast,
Slow,
}
fn main() {
let cli = Cli::parse();
println!("{cli:#?}");
}
- Use doc comments for help text. Use
long_about = Nonewhen you want concise command-level help fromabout. - Use
Option<T>for optional values and plainTfor required positionals/options. - Use
default_value_tfor typed defaults; it needsDisplayfor the value type. - Use
value_enumfor enums that should produce possible-values help and validation. - Use
#[command(flatten)]for shared flags, not copy-pasted fields.
Bool and Count Flags
Be explicit with flag actions when behavior matters.
boolfields with#[arg(long)]commonly map to set-true flags.- Use
ArgAction::SetTruefor explicit true flags. - Use
ArgAction::SetFalsefor disable flags such as--no-color. - Use
ArgAction::Countfor-v,-vv,-vvvstyle verbosity. - Use
Vec<T>orArgAction::Appendfor repeated values. - Use
Option<T>when absence is semantically different from a default.
#[derive(Debug, clap::Parser)]
struct Cli {
#[arg(long, action = clap::ArgAction::SetTrue)]
dry_run: bool,
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(long = "include", value_name = "GLOB")]
includes: Vec<String>,
}
Typed Parsers and Validation
Let field types drive parsing when possible. Add value_parser for ranges, custom parsers, and stricter validation.
#[derive(Debug, clap::Parser)]
struct Cli {
#[arg(long, value_parser = clap::value_parser!(u16).range(1..=65535))]
port: u16,
#[arg(long, value_parser = clap::builder::NonEmptyStringValueParser::new())]
name: String,
}
- Prefer
PathBuffor paths,SocketAddrfor socket addresses, numeric types for numeric inputs, andValueEnumfor finite modes. - Validate at parse time when the rule is local to one argument.
- Validate after parse when the rule depends on multiple fields, filesystem state, network state, or business logic.
- Do not parse strings manually after Clap if a typed parser can do it clearly.
Env and Defaults
Use environment variables for deployment/config integration, but keep precedence obvious.
#[derive(Debug, clap::Parser)]
struct Cli {
#[arg(long, env = "APP_RPC_URL")]
rpc_url: String,
#[arg(long, env = "APP_TIMEOUT_MS", default_value_t = 5_000)]
timeout_ms: u64,
}
- Enable the
envfeature before using#[arg(env = "...")]. - Document env variable names in help by using explicit names.
- Use
default_value_tfor typed defaults anddefault_valuefor string defaults. - Be careful with
Option<T>plus defaults: a default means the value is effectively always present.
Subcommands
Use required subcommands for multi-command tools unless the bare command has useful behavior.
#[derive(Debug, clap::Parser)]
#[command(version, about, subcommand_required = true, arg_required_else_help = true)]
struct Cli {
#[command(subcommand)]
command: Command,
}
Option<Command>makes the subcommand optional.subcommand_required = trueandarg_required_else_help = truemake operational tools friendlier.- Use
propagate_version = truewhen subcommands should also respond to--version. - Keep subcommand structs small; delegate business logic to normal functions or services after parsing.
Builder API
Use builder API for dynamic CLIs, generated arguments, plugins, or when combining with derived args.
use clap::{Arg, ArgAction, Command, value_parser};
fn command() -> Command {
Command::new("demo")
.version(env!("CARGO_PKG_VERSION"))
.about("Demo CLI")
.arg(
Arg::new("port")
.long("port")
.value_parser(value_parser!(u16).range(1..=65535))
.default_value("8080"),
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.action(ArgAction::Count),
)
}
- Use stable argument IDs and retrieve with matching types:
get_one::<T>(),get_many::<T>(),get_flag(),get_count(). - In builder mode, make a
fn command() -> Commandso tests can calldebug_assert()andtry_get_matches_from. - Use derive augmentation only when mixing generated and typed sections is clearer than one style alone.
Testing
Clap reports many development errors through debug assertions. Every non-trivial CLI should test the command shape.
use clap::{CommandFactory, Parser};
#[test]
fn cli_shape_is_valid() {
Cli::command().debug_assert();
}
#[test]
fn parses_serve_port() {
let cli = Cli::try_parse_from(["app", "serve", "--port", "9000"]).unwrap();
match cli.command {
Command::Serve { port, .. } => assert_eq!(port, 9000),
}
}
- Use
CommandFactoryfor derive-baseddebug_assert(). - Use
try_parse_fromortry_get_matches_fromin tests; do not callparse()orget_matches()in tests because they exit on error. - Add tests for required args, invalid enum values, env/default behavior, and important conflicts.
- Snapshot help output only when the project already uses snapshot tooling or CLI UX is a stable contract.
Help and UX
- Prefer precise
about, field doc comments,value_name, and examples over long prose. - Use consistent flag names: kebab-case on the command line, snake_case in Rust fields.
- Avoid surprising aliases unless compatibility requires them.
- Use
hide = trueonly for compatibility shims or intentionally hidden advanced flags. - Keep parse errors user-facing. After parsing, convert domain validation failures to clear messages and non-zero exit codes.
Helper Script
Generate starter snippets without loading extra context:
bash /mnt/skills/user/clap-rust/scripts/clap-rust-bootstrap.sh derive
bash /mnt/skills/user/clap-rust/scripts/clap-rust-bootstrap.sh builder
bash /mnt/skills/user/clap-rust/scripts/clap-rust-bootstrap.sh subcommand
bash /mnt/skills/user/clap-rust/scripts/clap-rust-bootstrap.sh value-enum
bash /mnt/skills/user/clap-rust/scripts/clap-rust-bootstrap.sh test
The script prints JSON with a scenario, cargo, and snippet field.
Review Checklist
- Is derive used for static CLIs and builder only where dynamic configuration is needed?
- Are feature flags (
derive,env,wrap_help,cargo) intentional? - Are values parsed into typed fields instead of manually parsed strings?
- Are bool, count, and repeated flags using the right
ArgActionor field type? - Are defaults typed and compatible with
Option<T>semantics? - Are subcommands required or optional intentionally?
- Does the CLI have
debug_assert()and parse-error tests? - Is business logic kept out of parser definitions?
Sources
https://github.com/clap-rs/claphttps://docs.rs/clap/latest/clap/https://docs.rs/clap/latest/clap/_derive/index.htmlhttps://docs.rs/clap/latest/clap/_derive/_tutorial/index.htmlhttps://docs.rs/clap/latest/clap/_tutorial/index.htmlhttps://docs.rs/clap/latest/clap/_features/index.htmlhttps://docs.rs/clap/latest/clap/struct.Command.htmlhttps://docs.rs/clap/latest/clap/struct.Arg.htmlhttps://docs.rs/clap/latest/clap/enum.ArgAction.htmlhttps://docs.rs/clap/latest/clap/trait.ValueEnum.html