Clap Subcommands Flags and Positional Arguments Explained
Model a tool with verbs, like git add and git commit, using a clap Subcommand enum. Shows global flags, per-command arguments and dispatch with match.

A tool that does one thing takes flags. A tool that does several things takes verbs, and clap models verbs as an enum. The compiler then forces you to handle every command, which is a better guarantee than a string comparison. This post builds a small task tool with add, list and done, and shows where the options belong. It follows clap derive step by step in the command line tool series.
clap 4, Rust 1.90.
The structure
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "tasks", version, about = "A tiny task list")]
struct Cli {
/// Show extra detail
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Add a task
Add {
/// Task text
text: String,
/// Priority from 1 to 5
#[arg(short, long, default_value_t = 3)]
priority: u8,
},
/// List tasks
List {
/// Include finished tasks
#[arg(long)]
all: bool,
},
/// Mark a task finished
#[command(alias = "finish")]
Done {
/// Task number
id: u32,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Add { text, priority } => {
println!("added {text:?} at priority {priority}");
}
Commands::List { all } => println!("listing (all: {all})"),
Commands::Done { id } => println!("finished {id}"),
}
}tasks add "write post" -p 2 -v prints added "write post" at priority 2. The -v works after the subcommand because it is global.
Help
tasks --help lists:
Usage: tasks [OPTIONS] <COMMAND>
Commands:
add Add a task
list List tasks
done Mark a task finished [aliases: finish]
help Print this message or the help of the given subcommand(s)tasks add --help shows only the arguments of add. A missing subcommand prints this help and exits with status 2.
Sharing arguments between commands
When two commands take the same options, extract a struct:
#[derive(clap::Args)]
struct Target {
#[arg(long)]
file: std::path::PathBuf,
}and use it in a variant as Export(Target). The fields appear as if written inline.
Keep dispatch thin
main should parse and dispatch, and each run_add, run_list function should live in the library crate and return a Result. That is what makes the commands testable without a process, as in testing with assert_cmd.
Limits
Deeply nested trees get hard to discover. Keep the first level small and use consistent verbs. For flexible plugin style commands like git foo running git-foo, use external_subcommand. The reference is the clap docs for Subcommand.


