Skip to content
rusthacks.dev
CLI

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.

2 min read
Summarize in AI
Flat illustration for the post “Clap Subcommands Flags and Positional Arguments Explained”, composed around enums.

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

rust
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:

Text
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:

rust
#[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.

Taggedclap
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.