How to Use Derive-Based Argument Parsing with clap

Use the #[derive(Parser)] attribute on a struct to automatically handle command-line argument parsing with clap.

When manual parsing becomes a trap

You are building a command-line tool. You start with std::env::args(). It returns a vector of strings. You write a loop to check for --query. You add support for -q. You handle --help. Then you need --output json and --output text. You add an enum. You add validation for port numbers. You realize you need to support --query=pattern and --query pattern. The parser code grows larger than the tool itself. You spend more time handling arguments than doing the work. The code turns into a tangle of if let and match arms. Debugging a typo in a flag name requires running the tool and reading the error output.

The clap crate offers a derive macro that eliminates this boilerplate. You define a struct that represents your configuration. You add #[derive(Parser)]. You sprinkle attributes on fields to describe how they map to flags, short options, or positional arguments. The macro generates the parsing code at compile time. You get a type-safe struct back. The compiler checks your types. You get automatic help text, version flags, and rich error messages without writing extra code.

The mental model of derive macros

The #[derive(Parser)] attribute is a procedural macro. It runs during compilation. It inspects your struct and generates Rust code that implements the clap::Parser trait. The generated code builds a clap::Command using the builder API. It wires up arguments, subcommands, and validation rules based on your attributes. You can see the generated code by running cargo expand. This is a useful debugging step when the macro behaves unexpectedly.

The macro does not change runtime performance. The generated code is identical to what you would write using the builder API. The benefit is maintainability. You define the shape of your data once. The parser stays in sync with the struct. If you add a field, the parser updates automatically. If you change a type, the compiler catches mismatches.

Minimal example

This example shows a basic CLI with two required arguments. The struct derives Parser and Debug. The #[command] attribute sets metadata. Field attributes describe argument mapping.

use clap::Parser;

/// A simple search tool configuration.
#[derive(Parser, Debug)]
#[command(name = "minigrep", version, about = "Search for patterns in files")]
struct Config {
    /// The pattern to search for.
    #[arg(short, long)]
    query: String,

    /// The file path to search.
    #[arg(short, long)]
    file_path: String,
}

fn main() {
    // Parse command-line arguments into Config.
    // Clap generates --help and --version automatically.
    // If parsing fails, process::exit(1) is called.
    let config = Config::parse();

    println!("Searching for '{}' in '{}'", config.query, config.file_path);
}

Run this with --help to see the generated output. The doc comments on the struct and fields appear in the help text. This is a convention: write documentation on fields, and clap includes it in --help. Always derive Debug on your CLI struct. You will need to print the configuration for debugging.

Walk through what happens

At compile time, the macro generates a parse method. The method reads std::env::args(). It matches tokens against the generated definition. If the user provides --query hello, the parser puts "hello" into the query field. If the user provides -f file.txt, it puts "file.txt" into file_path.

The #[arg(short, long)] attribute tells clap to accept both -q and --query. The macro derives the long name from the field name. Underscores in field names become hyphens in flags. A field named file_path accepts --file-path. This matches CLI conventions.

If a required argument is missing, parse() prints an error message and calls process::exit(1). This is the standard behavior for CLIs. The process stops, and the shell receives a non-zero exit code. The error message includes usage instructions and suggests missing flags. If the user provides an unknown flag, clap suggests similar flags if available.

Realistic example with subcommands

Real tools often have subcommands. git commit, git push, cargo build, cargo run. The clap derive macro supports subcommands via enums. You derive Subcommand on the enum. You add a field to the root struct with #[command(subcommand)].

use clap::{Parser, Subcommand};

/// A server management CLI.
#[derive(Parser)]
#[command(name = "server-cli", version, about = "Manage the local server")]
struct Cli {
    /// The command to execute.
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Start the server.
    Start {
        /// Port to bind to.
        #[arg(short, long, default_value_t = 8080)]
        port: u16,
    },
    /// Stop the server.
    Stop {
        /// Force stop without saving state.
        #[arg(short, long)]
        force: bool,
    },
}

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

    match cli.command {
        Commands::Start { port } => {
            println!("Starting server on port {}", port);
        }
        Commands::Stop { force } => {
            if force {
                println!("Force stopping server...");
            } else {
                println!("Stopping server gracefully...");
            }
        }
    }
}

The Start variant has a port field with a default value. The default_value_t attribute sets the default to 8080. The _t suffix indicates that the value is parsed as the target type. This is safer than default_value, which takes a string. Use default_value_t when the default is a constant expression.

The Stop variant has a force flag. The type is bool. clap treats bool fields as flags. If the flag is present, the value is true. If absent, it is false. This matches standard CLI behavior.

Advanced attributes and conventions

The #[arg(...)] attribute supports many options. num_args sets the number of values expected. value_parser specifies how to parse the value. requires enforces dependencies between arguments. conflicts_with prevents arguments from being used together.

use clap::Parser;

#[derive(Parser, Debug)]
#[command(name = "tool")]
struct Config {
    /// Enable verbose output.
    #[arg(short, long)]
    verbose: bool,

    /// Output format. Requires --verbose.
    #[arg(short, long, requires = "verbose")]
    output_format: Option<String>,

    /// Log level. Conflicts with --verbose.
    #[arg(short, long, conflicts_with = "verbose")]
    log_level: Option<String>,
}

The requires attribute ensures --output-format is only valid if --verbose is present. The conflicts_with attribute prevents --log-level and --verbose from being used together. clap validates these rules and reports clear errors.

Value parsers control type conversion. clap infers parsers for common types like String, u32, and bool. For custom types, you need to implement FromStr. The value_parser attribute can also use the clap::value_parser! macro for explicit parsing.

use std::str::FromStr;
use clap::Parser;

#[derive(Debug, Clone)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

impl FromStr for Color {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<&str> = s.split(',').collect();
        if parts.len() != 3 {
            return Err("Expected r,g,b".into());
        }
        Ok(Color {
            r: parts[0].parse().map_err(|e| e.to_string())?,
            g: parts[1].parse().map_err(|e| e.to_string())?,
            b: parts[2].parse().map_err(|e| e.to_string())?,
        })
    }
}

#[derive(Parser, Debug)]
struct Config {
    /// Background color in r,g,b format.
    #[arg(short, long)]
    color: Color,
}

The FromStr implementation parses the string into a Color struct. clap uses this to convert the argument value. If parsing fails, the error message includes the custom error text. This pattern allows complex types in CLIs.

Convention aside: keep #[arg(...)] attributes concise. Combine related options. #[arg(short, long, default_value_t = 8080)] is better than separate attributes. Use explicit names for short flags when the derived letter is ambiguous. #[arg(short = 'p', long = "port")] clarifies intent.

Pitfalls and compiler errors

The parse() method calls process::exit(1) on error. This is fine for main. If you are writing a library function that parses arguments, parse() will kill the entire process. Use try_parse() instead. It returns Result<T, Error>. You can handle the error gracefully.

fn parse_config() -> Result<Config, clap::Error> {
    Config::try_parse()
}

Custom types require FromStr. If you use a custom struct without implementing FromStr, the compiler rejects the code. You may see E0277 (trait bound not satisfied) if FromStr is missing. The error message points to the field. Implement FromStr or use a value parser.

Field names with underscores become hyphens in flags. This is usually correct. If you need a different flag name, use #[arg(long = "custom-name")]. Do not rely on field names for flag names if they differ from CLI conventions.

The #[command(subcommand)] attribute is required on the field that holds the enum. Forgetting it causes the enum to be ignored. The parser will not recognize subcommands. Add the attribute to enable subcommand parsing.

Treat try_parse() as the escape hatch. If your code needs to recover from bad arguments, parse() is the wrong tool.

Decision: when to use derive vs builder vs manual

Use #[derive(Parser)] when your CLI has a stable configuration shape and you want type safety. Use #[derive(Parser)] when you need subcommands, optional flags, and automatic help generation without writing boilerplate. Use the builder API (Command::new()) when argument logic depends on runtime conditions that a static struct cannot express. Use std::env::args() when you are writing a micro-tool with zero dependencies and a single positional argument.

Where to go next