rust 123 lines · 3 tabs

Building a clap Subcommand Dispatcher With a Command Trait in Rust

Shared by codesnips Aug 2026
3 tabs
use clap::{Args, Parser, Subcommand, ValueEnum};

#[derive(Parser, Debug)]
#[command(name = "tasks", version, about = "A tiny task manager")]
pub struct Cli {
    #[arg(short, long, global = true)]
    pub verbose: bool,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    Add(AddArgs),
    Remove(RemoveArgs),
    List(ListArgs),
}

#[derive(Args, Debug)]
pub struct AddArgs {
    pub title: String,

    #[arg(short, long, default_value_t = 3)]
    pub priority: u8,

    #[arg(short, long)]
    pub tags: Vec<String>,
}

#[derive(Args, Debug)]
pub struct RemoveArgs {
    pub id: u64,

    #[arg(short, long)]
    pub force: bool,
}

#[derive(Args, Debug)]
pub struct ListArgs {
    #[arg(short, long, value_enum, default_value_t = SortOrder::Created)]
    pub sort: SortOrder,

    #[arg(short, long, default_value_t = 20)]
    pub limit: usize,
}

#[derive(ValueEnum, Clone, Copy, Debug)]
pub enum SortOrder {
    Created,
    Priority,
    Title,
}
3 files · rust Explain with highlit

This snippet shows a common Rust CLI pattern: parse arguments with clap's derive API into a strongly-typed enum of subcommands, then dispatch each variant to its own handler through a shared trait. The goal is to keep argument definitions declarative while keeping the execution logic modular, so adding a new subcommand touches one enum variant and one struct rather than a growing match full of inline logic.

In cli.rs, the top-level Cli struct derives Parser and carries a global --verbose flag plus a command field of type Commands. The Commands enum derives Subcommand, and each variant wraps a dedicated args struct — AddArgs, RemoveArgs, ListArgs. Attributes like #[arg(short, long)], default_value_t, and value_enum show how clap maps flags and positional arguments onto typed fields, including an enum-valued --sort option backed by SortOrder deriving ValueEnum. Keeping each variant's data in its own struct means the parsing surface for one command stays isolated and self-documenting.

In command.rs, the Command trait defines a single run method returning anyhow::Result<()>, and a Context struct threads shared state (here just the verbose flag) into every handler. Each args struct implements Command, so the behavior lives next to the data it operates on. This is the trait-object-free version of a plugin system: dispatch is static, the compiler verifies every variant has a handler, and there is no runtime downcasting.

In main.rs, Cli::parse() does the whole parse-or-exit dance — on bad input clap prints usage and exits with a non-zero code automatically. The parsed cli.command is matched once, and each arm simply calls args.run(&ctx), forwarding the shared Context. The match is exhaustive, so a forgotten handler is a compile error rather than a silent no-op.

The trade-off is a little boilerplate per command, but it scales cleanly: the enum stays a flat routing table while logic grows in separate impls. A developer reaches for this when a tool has several verbs that each need distinct options, and wants clap's generated help, validation, and error messages without letting main balloon into a monolith. Returning anyhow::Result from main lets errors propagate with context and a tidy exit code.


Related snips

Share this code

Here's the card — post it anywhere.

Building a clap Subcommand Dispatcher With a Command Trait in Rust — share card
Link copied