How to use clap crate in Rust CLI parser

Define CLI arguments using the Parser derive macro and Subcommand enum, then parse them with Cli::parse().

You need a bridge between code and user

You wrote a Rust script that compresses images. It works perfectly when the filename is hardcoded in main. You send the binary to a friend. They run it and stare at a blank terminal. The program waits for input that never comes, or it crashes because it is looking for a file that does not exist. You need a way to tell the program what to do without recompiling it every time. Command-line arguments are the bridge between your code and the user's intent.

Hardcoding inputs works for experiments. It fails the moment you share the tool. You need a structured way to accept flags, options, and subcommands. You need help text that updates automatically. You need validation that rejects bad input before your logic runs. The clap crate provides all of this. It is the standard library for CLI parsing in Rust.

Define the shape of your interface once. clap handles the rest.

clap defines the contract

The clap crate stands for "Command Line Argument Parser." It replaces manual parsing of std::env::args() with a declarative approach. You define a struct that represents your arguments. You add a #[derive(Parser)] attribute. clap reads the struct, generates the help text, validates the input, and hands you a populated struct.

Think of clap as a contract between your code and the user. You define the fields and their types. clap enforces the contract. If the user passes a string where a number is expected, clap rejects it. If the user forgets a required flag, clap stops them and explains what is missing. Your code never sees invalid data.

The derive macro is the key feature. It uses Rust's type system to generate the parsing logic. You write Rust code. clap translates it into a CLI interface. The result is type-safe, documented, and maintainable.

Define the shape, let clap fill the gaps.

Minimal example

Start with a simple struct. Add the Parser derive. Add attributes to configure flags. Call parse() in main.

[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;

/// A simple tool to demonstrate clap
#[derive(Parser, Debug)]
#[command(name = "demo")]
struct Cli {
    /// The name of the file to process
    #[arg(short, long)]
    filename: String,

    /// Number of threads to use
    #[arg(short, long, default_value_t = 1)]
    threads: u32,
}

fn main() {
    // Parse arguments from std::env::args().
    // Panics with a helpful error if parsing fails.
    let cli = Cli::parse();

    println!("Processing {} with {} threads", cli.filename, cli.threads);
}

The #[command(name = "demo")] attribute sets the program name in help text. The #[arg(short, long)] attribute creates -f and --filename flags. The default_value_t attribute provides a fallback if the user omits the flag. Doc comments become the help text.

Run the binary with --help. clap generates the output from your struct and comments. If the help text looks right, your struct is right.

How the derive macro works

When you call Cli::parse(), clap reads the arguments passed to the process. It matches flags to struct fields based on the attributes. It converts string arguments into Rust types. It returns a Cli instance.

The macro generates a builder configuration behind the scenes. It creates a Command object with all the rules. It iterates over std::env::args(). It matches each argument against the rules. It accumulates values into the struct. If everything matches, it returns the struct. If something fails, it prints an error and exits.

Type conversion happens automatically. clap looks for a FromStr implementation. String, u32, bool, PathBuf, and Vec<T> all work out of the box. If you define a field as u32, clap tries to parse the input as an unsigned integer. If the input is "abc", clap catches the error and reports it.

The compiler guarantees the struct exists. clap guarantees the data matches the command line. You get both compile-time safety and runtime validation.

Realistic example with subcommands

Real tools have subcommands. git commit, git push. cargo build, cargo run. Subcommands keep the interface clean. They group related actions. clap supports subcommands via enums.

use clap::{Parser, Subcommand};

/// A tool to manage a project
#[derive(Parser, Debug)]
#[command(name = "my-cli")]
struct Cli {
    /// The subcommand to execute
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Subcommand, Debug)]
enum Command {
    /// Initialize a new project
    Init {
        /// Project name
        #[arg(short, long)]
        name: String,
    },
    /// Build the project
    Build {
        /// Enable release optimizations
        #[arg(long)]
        release: bool,
    },
}

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

    match cli.command {
        Some(Command::Init { name }) => println!("Initializing {}", name),
        Some(Command::Build { release }) => println!("Building with release={}", release),
        None => println!("No command provided. Use --help."),
    }
}

The Command enum derives Subcommand. Each variant becomes a subcommand. Fields inside a variant become arguments for that subcommand. The Cli struct holds an Option<Command>. This makes the subcommand optional. If you want a subcommand to be required, change the field type to Command instead of Option<Command>.

Match on the command to handle logic. The compiler forces you to handle all variants. You cannot forget a subcommand.

Subcommands keep your interface clean. Flat arguments get messy fast.

Attributes and conventions

The derive macro relies on attributes to configure behavior. #[arg(...)] configures fields. #[command(...)] configures the struct or enum.

Common #[arg] attributes:

  • short = 'c' sets the short flag.
  • long = "config" sets the long flag.
  • default_value_t = 10 sets a default value.
  • num_args = 1.. allows multiple values.
  • value_enum restricts input to a fixed set of choices.

Common #[command] attributes:

  • name = "tool" sets the program name.
  • version = "1.0" adds a --version flag.
  • about = "Description" sets the help summary.

The convention is to use kebab-case for flags and snake_case for struct fields. clap handles the conversion automatically. --my-flag maps to my_flag. This keeps your Rust code idiomatic while producing standard CLI output.

Doc comments on the struct become the help text. The first line is the short description. The rest is the long description. This keeps your documentation close to the code. If you change the argument, you update the doc comment. The help text stays in sync.

When you use Rc::clone(&data) in other parts of Rust, the convention is explicit to avoid confusion. With clap, the convention is to put #[command(...)] on the top-level struct and #[arg(...)] on fields. Keep the attributes close to the definition. Do not scatter configuration across the file.

Use ValueEnum for restricted choices. It generates help text with allowed values and rejects invalid input.

use clap::ValueEnum;

#[derive(Debug, Clone, ValueEnum)]
enum Color {
    Red,
    Green,
    Blue,
}

#[derive(Parser)]
struct Cli {
    #[arg(value_enum)]
    color: Color,
}

This pattern is preferred over manual validation. clap handles the parsing and error messages. You get type safety for free.

Pitfalls and error handling

clap panics on error by default. Cli::parse() calls std::process::exit if the arguments are invalid. This is fine for simple scripts. The user sees a clear error message. The program stops.

If you need to handle the error gracefully, use Cli::try_parse(). It returns a Result<Cli, Error>. You can log the error, show a custom message, or retry.

fn main() {
    match Cli::try_parse() {
        Ok(cli) => run(cli),
        Err(err) => {
            eprintln!("Failed to parse arguments: {}", err);
            std::process::exit(1);
        }
    }
}

Common pitfalls include forgetting the derive feature in Cargo.toml. If you omit features = ["derive"], the Parser derive macro does not exist. The compiler rejects the code with a missing derive error. Add the feature to fix it.

Another pitfall is using #[arg] on a field that does not support it. The macro fails at compile time. Check the attribute documentation for valid options.

If you define a field as String, clap requires the argument. If you want it optional, use Option<String>. If you want a default, use default_value_t. Mismatched expectations cause confusion. Align the type with the requirement.

clap generates --help and --version automatically if you configure them. Do not implement these manually. clap handles the logic and output. Trust the library.

Catch the error early. try_parse gives you control.

Decision matrix

Use clap with the derive feature when you want type-safe parsing with minimal boilerplate. Use clap with the builder API when you need dynamic argument generation or complex conditional logic that the derive macro cannot express. Use std::env::args() only when you are writing a tiny script with no dependencies and do not care about help text or validation. Use abscissa or a framework when you are building a large application with plugins and configuration layers.

Pick the tool that matches your complexity. Derive covers 90% of cases.

Where to go next