| name | rust-impl-clap-cli |
| description | Use when the user builds a CLI with clap 4.x, uses derive Parser API or builder API, defines subcommands, custom value parsers, env-var fallback, or shell completions. Prevents picking builder when derive suffices, missing `--features = ["derive"]`, omitting `value_parser` for non-string types, and confusing `Args` group with `Subcommand`. Covers: derive Parser API (`#[derive(Parser, Args, Subcommand, ValueEnum)]`), builder API (when derive is insufficient), nested Subcommand enums, `value_parser` (custom + range), env var fallback `env = "VAR"`, defaults `default_value` / `default_value_t`, required vs optional args, multiple values, shell completions via `clap_complete`, `Args` for shared argument groups. Keywords: clap, "#[derive(Parser)]", Parser, Subcommand, Args, ValueEnum, "clap derive", "clap builder", "value_parser", env, "default_value", "default_value_t", "shell completion", "clap_complete", "command line", CLI, argparse, "parse_from", "Parser::parse", subcommand, "nested command", required, "long short flag".
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
rust-impl-clap-cli
The clap 4.x CLI parser skill: how to scaffold a command-line interface with the derive macro API, when to drop to the builder API for dynamic CLIs, how to model subcommands and reusable argument groups, how to wire custom value parsers and environment-variable fallbacks, and how to ship shell completions through clap_complete.
Cross-references: [[rust-impl-cargo-project]] (declaring the derive feature in Cargo.toml), [[rust-syntax-traits]] (derive macros, FromStr for custom parsers), [[rust-impl-error-handling]] (returning anyhow::Result from main), [[rust-impl-testing]] (try_parse_from for unit-testable CLI logic).
When to use this skill
- User runs
cargo add clap and asks which features to enable.
- User writes
#[derive(Parser)] on a struct and gets a "trait Parser is not implemented" error.
- User wants nested subcommands (e.g.
git remote add ...) and is unsure whether to use Subcommand, Args, or both.
- User has a non-
String argument type and the parser refuses to compile.
- User wants
--config to fall back to an environment variable, or to a typed default value.
- User wants to generate Bash, Zsh, Fish, PowerShell, or Elvish completions.
- User builds a CLI whose argument set is decided at runtime and asks how to skip the derive API.
- User asks how to unit-test argument parsing without invoking the binary.
For the Cargo.toml mechanics of activating optional crate features see [[rust-impl-cargo-project]]. For mapping argument values into your domain types via FromStr see [[rust-syntax-traits]].
Quick reference: Cargo.toml entry
[dependencies]
clap = { version = "4.6", features = ["derive"] }
clap_complete = "4.6"
ALWAYS include features = ["derive"] when using #[derive(Parser)]. Without it the macros are absent and the compiler emits "cannot find derive macro Parser in this scope". NEVER write clap = "4.6" alone and expect derive to work.
Decision tree: derive vs builder
Is your CLI structure known at compile time?
YES -> use the derive API (#[derive(Parser)]).
NO -> use the builder API (Command::new(...).arg(...)).
Do you need dynamic arguments based on a config file or plugin discovery?
YES -> builder API.
NO -> derive API.
Do you need maximum compile-time validation and IDE autocomplete on the parsed struct?
YES -> derive API.
Are you generating bash/zsh completions for a plugin host?
The Command tree is the same in both APIs;
prefer derive and call `Cli::command()` to get the underlying Command.
ALWAYS start with the derive API. Drop to the builder API only when the argument set is genuinely unknown until runtime. NEVER mix the two for one command: choose one per Command tree.
Minimal derive CLI
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "myapp", version, about = "Does the thing", long_about = None)]
struct Cli {
#[arg(short, long)]
file: std::path::PathBuf,
#[arg(short, long)]
verbose: bool,
}
fn main() {
let cli = Cli::parse();
println!("file={:?} verbose={}", cli.file, cli.verbose);
}
Cli::parse() reads from std::env::args_os() and exits the process on parse error or on --help / --version. For test-friendly variants see the testing section below.
ALWAYS derive Debug together with Parser during development so the parsed struct can be printed with {:#?}. NEVER call Cli::parse() inside library code: it terminates the process; parse once in main and pass the struct downward.
Subcommands
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "git-lite", version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Add {
paths: Vec<std::path::PathBuf>,
},
Commit {
#[arg(short, long)]
message: String,
},
Remote {
#[command(subcommand)]
action: RemoteAction,
},
}
#[derive(Subcommand, Debug)]
enum RemoteAction {
Add { name: String, url: String },
Remove { name: String },
List,
}
ALWAYS attach #[command(subcommand)] to the field carrying the Subcommand-deriving enum on the parent struct or variant. NEVER decorate the enum itself with #[command(subcommand)]: the attribute belongs on the field that holds the enum.
Use nested #[command(subcommand)] (as in RemoteAction above) for git remote add style hierarchies. The depth is unlimited but more than two levels usually warrants a different top-level structure.
Reusable argument groups with Args
use clap::{Args, Parser, Subcommand};
#[derive(Args, Debug)]
struct GlobalOpts {
#[arg(long, env = "MYAPP_CONFIG", default_value = "~/.myapp.toml")]
config: std::path::PathBuf,
#[arg(short, long, global = true)]
quiet: bool,
}
#[derive(Parser, Debug)]
struct Cli {
#[command(flatten)]
globals: GlobalOpts,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Build,
Test,
}
ALWAYS use Args plus #[command(flatten)] for argument sets that belong to several commands (e.g. --config, --verbose, --quiet). NEVER use Subcommand to share arguments: Subcommand models alternative commands, Args models shared argument lists.
#[arg(global = true)] makes an argument visible to the parent command and every subcommand. Without it, myapp --quiet build parses but myapp build --quiet does not.
Choice values with ValueEnum
use clap::{Parser, ValueEnum};
#[derive(Copy, Clone, Debug, ValueEnum)]
enum Format {
Json,
Yaml,
Toml,
}
#[derive(Parser, Debug)]
struct Cli {
#[arg(short, long, value_enum, default_value_t = Format::Json)]
format: Format,
}
ALWAYS prefer ValueEnum over value_parser plus FromStr for closed sets of accepted values: it auto-generates possible_values in --help and gives the shell completion generator the value list for free. NEVER hand-roll a custom parser for an enum when ValueEnum would compile.
Custom value_parser
use clap::{Parser, value_parser};
fn port_in_range(s: &str) -> Result<u16, String> {
let port: u16 = s.parse().map_err(|_| format!("`{s}` is not a port"))?;
if (1024..=65535).contains(&port) {
Ok(port)
} else {
Err(format!("port `{port}` not in 1024-65535"))
}
}
#[derive(Parser, Debug)]
struct Cli {
#[arg(short, long, value_parser = port_in_range, default_value_t = 8080)]
port: u16,
#[arg(long, value_parser = value_parser!(u32).range(1..=64))]
workers: u32,
#[arg(long, value_parser = value_parser!(std::path::PathBuf))]
input: std::path::PathBuf,
}
ALWAYS provide value_parser for any non-String type unless the type implements clap::builder::ValueParserFactory (most primitives do, PathBuf does, OsString does). NEVER assume value_parser!(MyType) works for a custom struct: implement FromStr and pass the function directly, or call value_parser!(MyType) only if MyType: ValueParserFactory.
Numeric ranges use the inline form value_parser!(u32).range(1..=64). The full list of supported types and factory methods is in references/methods.md.
Environment-variable fallback
#[derive(Parser, Debug)]
struct Cli {
#[arg(long, env = "MYAPP_TOKEN", required = true)]
token: String,
#[arg(long, env = "RUST_LOG", default_value = "info")]
log_level: String,
}
Precedence is CLI argument > environment variable > default_value. ALWAYS document the env var in the doc comment that becomes --help text. NEVER store secrets in default_value (it appears in --help); the env var pattern keeps secrets out of help output and shell history.
Defaults: default_value vs default_value_t
| Attribute | Operand type | Use when |
|---|
default_value = "8080" | String literal | The field type implements FromStr and you want clap to parse the string at runtime. |
default_value_t = 8080u16 | Typed expression | The field type implements Default or Display, and you want the default to appear typed in --help. |
default_value_t (no =) | none | The field type implements Default; equivalent to default_value_t = Default::default(). |
ALWAYS use default_value_t for non-string types so that the help output prints the typed representation. NEVER mix default_value = "..." with value_parser returning a non-String type unless the string is guaranteed to parse: the parse failure surfaces at runtime, not compile time.
Required, optional, and multiple
#[derive(Parser, Debug)]
struct Cli {
name: String,
#[arg(short, long, required = true)]
output: std::path::PathBuf,
#[arg(short, long)]
label: Option<String>,
inputs: Vec<std::path::PathBuf>,
#[arg(short = 'I', long = "include")]
includes: Vec<String>,
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
}
Field-type semantics: T is required, Option<T> is optional, Vec<T> is repeated zero-or-more, bool is a flag with ArgAction::SetTrue. ALWAYS choose the field type that already encodes the cardinality; NEVER set required = true on an Option<T> field (it contradicts the type).
For incrementing counters use action = ArgAction::Count on a u8 / u16 / u32 field.
Builder API (dynamic CLIs only)
use clap::{Arg, ArgAction, Command, value_parser};
fn build_cli(plugins: &[String]) -> Command {
let mut cmd = Command::new("myapp")
.version("1.0")
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue)
.help("Verbose output"),
)
.arg(
Arg::new("port")
.long("port")
.value_parser(value_parser!(u16).range(1024..=65535))
.default_value("8080"),
);
for plugin in plugins {
cmd = cmd.subcommand(Command::new(plugin.clone()).about("Dynamic plugin"));
}
cmd
}
fn main() {
let plugins = discover_plugins();
let matches = build_cli(&plugins).get_matches();
let verbose = matches.get_flag("verbose");
let port: u16 = *matches.get_one("port").expect("default");
let _ = (verbose, port);
}
fn discover_plugins() -> Vec<String> { vec![] }
ALWAYS use the builder API when the set of subcommands or arguments is computed from runtime state (plugin discovery, configuration file, etc.). NEVER bend the derive API with #[command(skip)] plus runtime hacks: the builder API exists for exactly this case.
Access parsed values via ArgMatches::get_one::<T>("id"), get_many::<T>("id"), and get_flag("id"). The id is the string passed to Arg::new("...").
Shell completions
use clap::{CommandFactory, Parser};
use clap_complete::{generate, Shell};
use std::io;
#[derive(Parser, Debug)]
#[command(name = "myapp", version)]
struct Cli {
#[arg(long, value_enum)]
completions: Option<Shell>,
}
fn main() {
let cli = Cli::parse();
if let Some(shell) = cli.completions {
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
generate(shell, &mut cmd, name, &mut io::stdout());
return;
}
}
ALWAYS expose completions through a dedicated flag or subcommand that writes to stdout (the convention is myapp completions <shell> or myapp --completions <shell>). NEVER hand-write _myapp Zsh files or Bash complete -F snippets: clap_complete::generate produces correct, version-aware output.
Install instructions for users: myapp --completions bash > /etc/bash_completion.d/myapp, or write to $ZDOTDIR/_myapp for Zsh. Shell-specific install paths are in references/examples.md.
Testing argument parsing
use clap::Parser;
#[derive(Parser, Debug, PartialEq)]
struct Cli {
#[arg(short, long)]
name: String,
}
#[test]
fn parses_flag() {
let cli = Cli::try_parse_from(["myapp", "--name", "alice"]).unwrap();
assert_eq!(cli.name, "alice");
}
#[test]
fn missing_required_is_error() {
let err = Cli::try_parse_from(["myapp"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn cli_definition_is_consistent() {
use clap::CommandFactory;
Cli::command().debug_assert();
}
ALWAYS use try_parse_from(...) in tests: it returns Result<Self, clap::Error> and never exits the process. NEVER call Cli::parse() from a #[test]: a parse failure will terminate the test process, hiding the real assertion failure.
ALWAYS add one debug_assert()-style test per Parser struct: it catches misconfigured Command trees (duplicate ids, conflicting short flags) before they reach users.
Common cargo entries
[dependencies]
clap = { version = "4.6", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.6"
| Feature | What it enables |
|---|
derive | The #[derive(Parser, Args, Subcommand, ValueEnum)] macros. |
env | The env = "VAR" attribute on #[arg]. Required if you use env-var fallback. |
wrap_help | Wraps --help output to terminal width via terminal_size. |
unicode | Unicode-aware help formatting. |
string | Allow String (in addition to &'static str) for help and about text. |
NEVER omit env from the feature list if any #[arg(env = "...")] exists in the code: omitting it produces a confusing "unknown attribute" error rather than a "feature gate" error.
Anti-patterns
See references/anti-patterns.md for the full enumeration with fixes. Highlights:
- Forgetting
features = ["derive"] in the clap Cargo entry.
- Picking the builder API when derive would be ten times shorter.
- Omitting
value_parser for non-String types and getting an &String at runtime.
- Setting
#[command(name = "foo")] while Cargo.toml's [[bin]] name is bar.
- Hand-rolling shell completion files instead of using
clap_complete::generate.
- Deeply nesting
Subcommand enums without setting about on each variant.
- Calling
Cli::parse() from a test (process exits, test framework fails to report).
- Using
Subcommand to model shared arguments (use Args plus flatten).
- Storing secrets in
default_value (they print in --help).
Reference files
references/methods.md: full attribute and method catalogue for #[command(...)], #[arg(...)], #[group(...)], #[value(...)], value_parser!, ArgAction, Command, Arg, ArgMatches, clap_complete::Shell.
references/examples.md: end-to-end CLIs (single binary, subcommand tree, dynamic plugin host, shell-completion install snippets, env-var-driven configuration).
references/anti-patterns.md: numbered anti-patterns with the failure mode and the deterministic fix.
Official sources
Verified 2026-05-19.