How to add subcommands with clap
Short answer: Mark a field on the Parser struct with #[command(subcommand)] and give it an enum that derives Subcommand. Each variant is one command with its own arguments, and global = true on a top-level argument is one definition shared by every command.
The examples stay in the args1 project from the previous article. Run cargo new args1 if you don’t have it, then cargo add clap --features derive; clap is already in Cargo.toml:
[dependencies]clap = { version = "4.6.7", features = ["derive"] }Describe the verbs as an enum
Section titled “Describe the verbs as an enum”Replace src/main.rs with the greeter, split into two commands:
use clap::{Parser, Subcommand};
#[derive(Parser)]#[command(about = "A small tool with two commands")]struct Cli { #[command(subcommand)] command: Command,
/// Print the parsed command as well #[arg(short, long, global = true)] verbose: bool,}
#[derive(Debug, Subcommand)]enum Command { /// Greet someone Greet { /// Who to greet name: String,
/// How many times to greet them #[arg(short, long, default_value_t = 1)] count: u8, }, /// Say goodbye Bye { /// Who to say goodbye to name: String, },}
fn main() { let cli = Cli::parse();
if cli.verbose { eprintln!("verbose: {:?}", cli.command); }
match cli.command { Command::Greet { name, count } => { for _ in 0..count { println!("hello, {name}"); } } Command::Bye { name } => { println!("bye, {name}"); } }}The command field holds the subcommand, and the variants of Command are the verbs: clap turns Greet into greet and Bye into bye. Inside a variant the fields work as they did on the flat struct in the previous article: a field with no #[arg] is a required positional, #[arg(short, long)] gives both spellings, and default_value_t supplies the value when the flag is absent. main matches on the enum and handles each command.
$ cargo run -q -- greet Adahello, Ada
$ cargo run -q -- greet Ada --count 3hello, Adahello, Adahello, Ada
$ cargo run -q -- bye Bobbye, BobThe command list in --help comes from the enum as well, with each variant’s doc comment as its description:
$ cargo run -q -- --helpA small tool with two commands
Usage: args1 [OPTIONS] <COMMAND>
Commands: greet Greet someone bye Say goodbye help Print this message or the help of the given subcommand(s)
Options: -v, --verbose Print the parsed command as well -h, --help Print helpShaping that text further is a page of its own.
An option every command shares
Section titled “An option every command shares”--verbose belongs to no single command, so it lives on Cli with global = true. That attribute is what lets it appear after the verb as well as before it:
$ cargo run -q -- --verbose greet Adahello, Adaverbose: Greet { name: "Ada", count: 1 }
$ cargo run -q -- greet Ada --verbosehello, Adaverbose: Greet { name: "Ada", count: 1 }Drop global = true and the second form fails with error: unexpected argument '--verbose' found, exit code 2, and a usage line of args1 greet <NAME>: after the verb, clap is parsing the subcommand’s arguments, not Cli’s. The verbose line goes to stderr, so args1 greet Ada --verbose > out.txt keeps it out of the file.
Shared options that take a value
Section titled “Shared options that take a value”global covers flags. When commands share an option with a value, put the shared fields in a struct that derives Args and flatten it into each variant:
use clap::{Args, Subcommand};
#[derive(Args)]struct Output { /// Write the result here instead of stdout #[arg(short, long)] file: Option<String>,}
#[derive(Subcommand)]enum Command { /// Greet someone Greet { #[command(flatten)] output: Output, /// Who to greet name: String, }, /// Say goodbye Bye { #[command(flatten)] output: Output, /// Who to say goodbye to name: String, },}Both commands accept -f then, and the value lands in the same Output struct:
$ cargo run -q -- greet Ada -f card.txt$ cat card.txthello, AdaA command line clap cannot parse
Section titled “A command line clap cannot parse”$ cargo run -q -- gret Adaerror: unrecognized subcommand 'gret'
tip: a similar subcommand exists: 'greet'
Usage: args1 [OPTIONS] <COMMAND>
For more information, try '--help'.$ echo $?2clap prints that and exits before main runs, so there is no error value to match on and no _ => arm to write. Run args1 with no subcommand at all and the same help text goes to stderr with exit code 2; --help prints it to stdout with exit code 0.
- How to parse arguments with clap — the flat struct this page grows
- How to pass arguments with cargo run — the hand-written parser
- Parsing arguments with clap — the same program in the CLI track
- Back to Building a command-line tool