clap / Chapter 2 of 5

Subcommands and shared options

Build clap subcommands with shared options, argument groups, conflicts, and environment defaults. Handle paths and trailing arguments.

5 topicsUpdated
08 / 27

Subcommands#

Use subcommands when your tool performs distinct actions such as add, list, and remove.

RustOne parser, two actions
use clap::{Parser, Subcommand};

/// Keep a small list of tasks.
#[derive(Parser)]
#[command(version, propagate_version = true)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Add one task.
    Add {
        title: String,
        #[arg(short, long)]
        priority: Option<u8>,
    },
    /// Display tasks.
    List {
        #[arg(long)]
        all: bool,
    },
}

fn main() {
    match Cli::parse().command {
        Command::Add { title, priority } => println!("add {title} {priority:?}"),
        Command::List { all } => println!("list all={all}"),
    }
}
ShellEach command gets help
cargo run -- add "Pay rent" -p 2
# add Pay rent Some(2)
cargo run -- list --all
# list all=true
cargo run -- add --help
Command requiredness

A field of type Command requires a subcommand and shows help when one is missing. Option<Command> allows no subcommand. The help subcommand is generated by clap.

09 / 27

Nested commands and shared options#

Compose larger CLIs from smaller structs without copying every field.

Use #[derive(Args)] for reusable fields, then #[command(flatten)] them into a parser or subcommand. Nest another Subcommand enum for a command family. A global argument can be supplied before or after a subcommand.

RustNested users commands and one global flag
use clap::{Args, Parser, Subcommand};

#[derive(Parser)]
struct Cli {
    #[arg(long, global = true)]
    debug: bool,
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    Users {
        #[command(subcommand)]
        action: UserAction,
    },
    Export(ExportArgs),
}

#[derive(Subcommand)]
enum UserAction {
    Show { id: u64 },
    Delete { id: u64 },
}

#[derive(Args)]
struct ExportArgs {
    #[arg(long)]
    output: String,
}

fn main() {
    let cli = Cli::parse();
    match cli.command {
        Command::Users { action: UserAction::Show { id } } => println!("show {id}"),
        Command::Users { action: UserAction::Delete { id } } => println!("delete {id}"),
        Command::Export(args) => println!("export to {}", args.output),
    }
    if cli.debug { eprintln!("debug enabled"); }
}
ShellInvocation
cargo run -- --debug users show 42
cargo run -- users delete 42 --debug
cargo run -- export --output users.csv
Unique argument names

Flattened fields share one argument namespace within a command. Give each option a unique long name and keep reusable Args structs focused.

10 / 27

Conflicts, dependencies, and groups#

Describe invalid combinations in the parser so users see help before work begins.

RustMutually exclusive source and required partner
use clap::{Args, Parser};

#[derive(Parser)]
struct Cli {
    #[command(flatten)]
    source: Source,

    /// Endpoint that needs --token.
    #[arg(long, requires = "token")]
    remote: Option<String>,
    #[arg(long)]
    token: Option<String>,
}

#[derive(Args)]
#[group(required = true, multiple = false)]
struct Source {
    /// Read from a file.
    #[arg(long)]
    file: Option<String>,
    /// Read from standard input.
    #[arg(long)]
    stdin: bool,
}

fn main() {
    let _ = Cli::parse();
}
ShellAllowed and rejected combinations
cargo run -- --file data.txt
cargo run -- --stdin --remote https://example.test --token secret
cargo run -- --file data.txt --stdin  # conflicting source options
cargo run -- --stdin --remote https://example.test  # --token required

A group with required = true, multiple = false asks for exactly one member. The ID in requires = "token" is a Rust field ID, not the spelling of its long option. For a single pair, #[arg(conflicts_with = "stdin")] is often enough.

Credentials

Pass real credentials through environment variables or secure input. Literal command-line tokens can remain in shell history.

11 / 27

Environment variables and precedence#

Let users set stable defaults without typing the same option every time.

Enable clap’s env feature. An explicit command line value wins over an environment value, which wins over a declared default. Clap does not read .env files by itself.

RustRead an environment value
use clap::Parser;

#[derive(Parser, Debug)]
struct Cli {
    #[arg(long, env = "SHELF_ENDPOINT", default_value = "http://localhost:3000")]
    endpoint: String,
    #[arg(long, env = "SHELF_TOKEN", hide_env_values = true)]
    token: Option<String>,
}

fn main() {
    let cli = Cli::parse();
    println!("endpoint={}", cli.endpoint);
    println!("token set={}", cli.token.is_some());
}
ShellPrecedence
SHELF_ENDPOINT=https://example.test cargo run --
# endpoint=https://example.test
SHELF_ENDPOINT=https://example.test cargo run -- --endpoint http://localhost:9000
# endpoint=http://localhost:9000
Help and secrets

hide_env_values avoids showing the current environment value in help output. Avoid printing credential values yourself, including debug-printing the entire parsed struct.

12 / 27

Paths, Unicode, and the -- separator#

Accept file paths without assuming every OS path is valid UTF-8.

RustUse PathBuf for a path
use std::path::PathBuf;
use clap::Parser;

#[derive(Parser)]
struct Cli {
    /// File to read.
    input: PathBuf,
    /// Write results here.
    #[arg(short, long)]
    output: Option<PathBuf>,
}

fn main() {
    let cli = Cli::parse();
    println!("{:?} -> {:?}", cli.input, cli.output);
}
ShellA filename beginning with a dash
cargo run -- -- -notes.txt
# The first -- ends Cargo's options; the second ends shelf's options.
cargo run -- -o result.txt -- -notes.txt

Use OsString for raw OS strings and PathBuf for file paths. With a String, non-UTF-8 input cannot be represented. For a negative number intended as a positional argument, -- -5 avoids confusion with an option.

Values starting with a dash

For an option whose value may begin with -, use --option=-value or configure allow_hyphen_values only when that behavior suits the option.