---
title: How to Add Subcommands to a Rust CLI with clap
summary: Add subcommands to a Rust CLI by defining a Subcommand enum and matching on the parsed result.
lang: en
slug: how-to-add-subcommands-to-a-rust-cli-with-clap
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-add-subcommands-to-a-rust-cli-with-clap/
---

# How to Add Subcommands to a Rust CLI with clap



## When flags get out of hand

You built a CLI tool. It started with a single job: `mytool process input.txt`. Then you added `--output`, `--verbose`, and `--format`. Now you need a completely different mode. You want to list files, remove entries, and configure settings. Adding `--list`, `--remove`, and `--config` flags turns your help text into a wall of text. Users have to memorize which flags go with which mode. The interface is becoming unmanageable.

Subcommands solve this. They partition your tool into distinct verbs. `mytool list`, `mytool remove`, `mytool config`. Each subcommand has its own set of arguments and flags. The user sees a clean menu. `clap` makes subcommands feel like defining a tree structure. You write an enum, derive a trait, and attach it to your parser. The library handles the routing, help generation, and error messages.

## Subcommands as a hierarchy

Think of a Swiss Army knife. The handle is your binary. The blade, the screwdriver, and the file are subcommands. You don't use them all at once. You select one tool to perform a specific action. Each tool has its own shape and purpose. In CLI terms, the main program is the handle. `git commit`, `git push`, and `git log` are the tools. They share the binary but have different arguments. `commit` takes a message. `push` takes a remote. `log` takes a range.

`clap` models this hierarchy using Rust enums. An enum represents a value that can be one of several variants. Each variant maps to a subcommand. The fields inside a variant map to the arguments for that subcommand. This matches the mental model of a CLI perfectly. You pick a command, then you provide the data that command needs. The compiler enforces that you handle every command in your `main` function. You can't forget a case.

## Minimal example

Start with the derive macros. `Parser` generates the argument definition for your CLI struct. `Subcommand` generates the subcommand definition for your enum. You link them with the `#[command(subcommand)]` attribute.

```rust
use clap::{Parser, Subcommand};

/// A simple tool with subcommands
#[derive(Parser)]
#[command(name = "mytool")]
struct Cli {
    /// The subcommand to execute
    #[command(subcommand)]
    command: Commands,
}

/// Available subcommands
#[derive(Subcommand)]
enum Commands {
    /// Add a new item
    Add {
        /// Name of the item to add
        name: String,
    },
    /// Remove an existing item
    Remove {
        /// Name of the item to remove
        name: String,
    },
}

fn main() {
    // Parse arguments from std::env::args()
    let cli = Cli::parse();

    // Route to the appropriate handler
    match cli.command {
        Commands::Add { name } => println!("Adding {name}"),
        Commands::Remove { name } => println!("Removing {name}"),
    }
}
```

The `#[command(subcommand)]` attribute tells `clap` that the `command` field holds the subcommand enum. Without this attribute, `clap` treats the field as a regular argument and fails to parse subcommands. The `#[derive(Subcommand)]` macro inspects the enum variants and generates code to match command-line tokens to variants.

## What happens under the hood

When you call `Cli::parse()`, the generated code reads `std::env::args()`. It skips the first argument, which is the program name. It looks at the second argument. If the second argument is `add`, it matches `Commands::Add`. It then consumes the remaining arguments and assigns them to the `name` field. If the second argument is `remove`, it matches `Commands::Remove`. If the second argument is unknown, `clap` prints an error message and exits. If there is no second argument, `clap` prints the help text and exits.

The derive macros do the heavy work at compile time. `Parser` generates a `clap::Command` builder call that defines the structure. `Subcommand` generates the logic to parse the subcommand variant. The runtime cost is minimal. `clap` uses a fast parser. The generated code is optimized by the compiler. You get the safety of enums and the ergonomics of a builder pattern without writing the boilerplate.

If you forget `#[derive(Subcommand)]` on the enum, the compiler rejects you with `E0277` (trait bound not satisfied). The `Parser` derive expects the field to implement `Subcommand`, and a plain enum doesn't. The error message points to the field definition. Add the derive and the error disappears.

## Realistic example with nested commands

Real tools often have deeper hierarchies. `git remote add` has two levels. `remote` is a subcommand. `add` is a sub-subcommand. `clap` supports nesting by putting `#[command(subcommand)]` inside a variant. You can recurse as deep as needed.

Global flags apply to all subcommands. Use `#[command(global = true)]` for flags like `--verbose` or `--color`. These flags can appear before or after the subcommand. `mytool --verbose add foo` and `mytool add --verbose foo` both work.

```rust
use clap::{Parser, Subcommand};

/// A package manager with nested subcommands
#[derive(Parser)]
#[command(name = "pkg", version, about = "Manage packages")]
struct Cli {
    /// Enable verbose output for all commands
    #[command(global = true)]
    verbose: bool,

    /// The subcommand to execute
    #[command(subcommand)]
    command: Commands,
}

/// Top-level subcommands
#[derive(Subcommand)]
enum Commands {
    /// Install a package
    Install {
        /// Package name
        package: String,
    },
    /// Manage remotes
    Remote {
        /// Remote subcommands
        #[command(subcommand)]
        command: RemoteCommands,
    },
}

/// Subcommands for the 'remote' command
#[derive(Subcommand)]
enum RemoteCommands {
    /// Add a remote
    Add {
        /// Remote name
        name: String,
        /// Remote URL
        url: String,
    },
    /// Remove a remote
    Remove {
        /// Remote name
        name: String,
    },
}

fn main() {
    let cli = Cli::parse();

    // Handle global flags first
    if cli.verbose {
        println!("Verbose mode enabled");
    }

    // Route to top-level commands
    match cli.command {
        Commands::Install { package } => {
            println!("Installing {package}");
        }
        Commands::Remote { command } => {
            // Route to nested commands
            match command {
                RemoteCommands::Add { name, url } => {
                    println!("Adding remote {name} at {url}");
                }
                RemoteCommands::Remove { name } => {
                    println!("Removing remote {name}");
                }
            }
        }
    }
}
```

The `#[command(global = true)]` attribute makes `verbose` available everywhere. `clap` generates help text that shows global flags in the main section and subcommand flags in their respective sections. The help output stays organized. Users see `pkg remote --help` and get documentation for `add` and `remove` without noise from `install`.

Convention aside: use `#[command(...)]` for attributes. Older versions of `clap` used `#[clap(...)]`. The community migrated to `command` to match the derive macro name `Parser`. `#[clap(...)]` still works as an alias, but new code uses `command`. Stick with `command` to match the ecosystem.

## Pitfalls and validation

Subcommands enforce syntax, not semantics. `clap` ensures the user provided a string for `name`. It does not check if the name is valid for your application. You handle semantic validation in the match arms. If `name` must be alphanumeric, check that after parsing. Return an error or exit with a non-zero code.

Optional subcommands require `Option`. If you want `mytool` to do something useful without a subcommand, wrap the enum in `Option`. `#[command(subcommand)]` on an `Option<Commands>` makes the subcommand optional. If the user runs `mytool`, `cli.command` is `None`. You can provide a default action.

If you use `Commands` directly, `mytool` fails when run without arguments. `clap` prints help and exits with error code 1. This is usually the desired behavior. Tools like `git` require a subcommand. Tools like `cargo` have a default action. Choose based on your tool's purpose.

Another common issue is argument conflicts. `clap` detects conflicts automatically. If two subcommands share a flag name, `clap` isolates them. Flags are scoped to their subcommand. You don't need to worry about collisions. If you want a flag to be mutually exclusive with another flag in the same subcommand, use `#[command(args_conflicts_with_subcommands = true)]` or `ArgGroup`. Keep it simple first. Subcommands usually eliminate the need for complex conflict rules.

Treat the enum as your API contract. If you add a new subcommand, the compiler forces you to update the match arm. You can't ship a binary that crashes on a new command. This safety is one of the strongest benefits of the derive approach.

## Decision: when to use subcommands

Use subcommands when the tool performs distinct actions that have their own sets of arguments. `git commit` and `git push` don't share arguments. They share the binary. Subcommands keep the interface clean and the help text readable.

Use flags when the action is a modifier to a single core operation. `ls -la` is one action with modifiers. `ls` doesn't have subcommands like `ls list` or `ls show`. Flags work best when the user always does the same thing but tweaks the behavior.

Use positional arguments when the input is the primary focus and the action is implied or simple. `cat file.txt`. The file is the argument. `cat` has no subcommands. Positional args shine when the verb is obvious from context.

Use `Option<Commands>` when the binary has a default mode that runs without a subcommand. `cargo` runs tests by default but accepts `cargo build`. The option allows the user to omit the subcommand for the common case.

Structure follows semantics. If the user thinks in verbs, give them subcommands. If the user thinks in options, give them flags. Match the CLI structure to the mental model of the task.

## Where to go next

- [How to parse command line arguments with clap](/en/how-to-parse-command-line-arguments-with-clap/)
- [How to Use Derive-Based Argument Parsing with clap](/en/how-to-use-derive-based-argument-parsing-with-clap/)
- [How to use clap crate in Rust CLI parser](/en/how-to-use-clap-crate-in-rust-cli-parser/)