Sub (sub) Commands in Clap

May 15, 2026

I found it confusing at first to think about how to use clap to do a “sub- sub- command”. We opted to use clap’s “derive” method to define our interface.

Part of that looks like: mcap encrypt and mcap decrypt. This sort of “single depth” subcommand is a popular pattern.

I wanted to add a “debug” subcommand which itself had a series of subcommands – not really intended for “normal” use.

The Source Code

Lets make a minimal example. Starting with cargo init and cargo add clap --features derive gives us a repository producing an executable.

In our src/main.rs we set up an example:

use clap::{Parser, Subcommand};
use clap::Error;
#[derive(Parser)]
#[command(version = "25.12.1")]
#[command(about = "Example sub-sub-commands in clap")]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}
#[derive(Subcommand)]
#[command(arg_required_else_help(true))]
#[command(about = "I am a subcommand with subcommands")]
enum DebugCommands {
    #[command(about = "A sub-sub-command to greet you")]
    Hello {
        name: String,
    }
}
// the "first layer" or "top level" subcommands
#[derive(Subcommand)]
#[command(arg_required_else_help(true))]
enum Commands {
    #[command(about = "frobnicate the appropriate quux")]
    Frob {
        quux: String,
    },
    #[command(about = "Debugging tools.")]
    Debug {
        #[command(subcommand)]
        command: Option<DebugCommands>,
    },
}
fn main_debug_hello(whom: &String) -> Result<(), Error> {
    println!("Hello, {}", whom);
    Ok(())
}
fn main_frob(quux: &String) -> Result<(), Error>  {
    println!("Frobnicate: {}", quux);
    Ok(())
}
fn main() {
    let cli = Cli::parse();
    let result = match &cli.command {
        Some(Commands::Frob {
            quux,
        }) => main_frob(quux),
        Some(Commands::Debug { command }) => match command {
            Some(DebugCommands::Hello{ name }) => main_debug_hello(name),
            None => Ok(()),
        }
        None => Ok(()),
    };
    if let Err(e) = result {
        println!("Error: {}", e);
        std::process::exit(2);
    }
}

That’s kind of a lot but gives us a simple binary with one “normal” sub-command frob and a second sub-command debug that itself has sub-commands. In this case there’s only one sub-sub command (debug hello <name>).

If all is aligned, we can run the above with cargo run. This should ultimately produce output like:

Example sub-sub-commands in clap
Usage: clap-subsub [COMMAND]
Commands:
  frob   frobnicate the appropriate quux
  debug  Debugging tools.
  help   Print this message or the help of the given subcommand(s)
Options:
  -h, --help     Print help
  -V, --version  Print version

The Sub-Sub Command

Basically “just” a matter of aligning the macros correctly. Hopefully at least future-meejah can gain insight.

Running the debug subcommand produces some useful help text, as desired:

$ cargo run -- debug
Debugging tools.
Usage: clap-subsub debug [COMMAND]
Commands:
  hello  A sub-sub-command to greet you
  help   Print this message or the help of the given subcommand(s)
Options:
  -h, --help  Print help