Skip to content
Select theme

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:

Cargo.toml
[dependencies]
clap = { version = "4.6.7", features = ["derive"] }

Replace src/main.rs with the greeter, split into two commands:

src/main.rs
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 Ada
hello, Ada
$ cargo run -q -- greet Ada --count 3
hello, Ada
hello, Ada
hello, Ada
$ cargo run -q -- bye Bob
bye, Bob

The command list in --help comes from the enum as well, with each variant’s doc comment as its description:

$ cargo run -q -- --help
A 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 help

Shaping that text further is a page of its own.

--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 Ada
hello, Ada
verbose: Greet { name: "Ada", count: 1 }
$ cargo run -q -- greet Ada --verbose
hello, Ada
verbose: 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.

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.txt
hello, Ada
$ cargo run -q -- gret Ada
error: unrecognized subcommand 'gret'
tip: a similar subcommand exists: 'greet'
Usage: args1 [OPTIONS] <COMMAND>
For more information, try '--help'.
$ echo $?
2

clap 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.