How do I parse command line arguments in Rust?

Walkthrough

Command-line argument parsing is essential for CLI tools. The clap crate (Command Line Argument Parser) provides a robust, ergonomic API for building feature-rich CLIs with automatic help generation, shell completions, and validation.

Clap offers multiple API styles:

  1. Derive API — uses macros for clean, declarative argument definitions (recommended for most uses)
  2. Builder API — programmatic construction with full flexibility
  3. Parser API — lower-level parsing for custom needs

We'll focus on the Derive API, which is the most idiomatic for typical applications.

Code Example

# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
 
/// A fictional versioning CLI tool
#[derive(Parser, Debug)]
#[command(name = "mycli")]
#[command(author = "Your Name <you@example.com>")]
#[command(version = "1.0")]
#[command(about = "A CLI tool demonstrating clap features", long_about = None)]
struct Cli {
    /// Turn debugging information on
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
 
    /// Configuration file path
    #[arg(short, long, value_name = "FILE")]
    config: Option<PathBuf>,
 
    #[command(subcommand)]
    command: Commands,
}
 
#[derive(Subcommand, Debug)]
enum Commands {
    /// Add a file to the index
    Add {
        /// Files to add
        #[arg(required = true)]
        files: Vec<PathBuf>,
 
        /// Force add ignored files
        #[arg(short, long)]
        force: bool,
    },
    /// Commit changes
    Commit {
        /// Commit message
        #[arg(short, long)]
        message: Option<String>,
 
        /// Open editor for commit message
        #[arg(long)]
        edit: bool,
    },
    /// Push changes to remote
    Push {
        /// Remote name
        #[arg(default_value = "origin")]
        remote: String,
 
        /// Branch name
        branch: Option<String>,
    },
    /// Pull changes from remote
    Pull {
        /// Remote name
        remote: Option<String>,
    },
}
 
#[derive(Parser, Debug)]
struct SearchArgs {
    /// Search pattern
    pattern: String,
 
    /// Search path
    #[arg(default_value = ".")]
    path: PathBuf,
 
    /// Case insensitive search
    #[arg(short, long)]
    ignore_case: bool,
 
    /// Number of results to show
    #[arg(short = 'n', long = "limit", default_value = "10")]
    max_results: usize,
}
 
fn main() {
    let cli = Cli::parse();
 
    // Handle verbosity
    match cli.verbose {
        0 => println!("Normal output"),
        1 => println!("Verbose output"),
        2 => println!("More verbose output"),
        _ => println!("Maximum verbosity!"),
    }
 
    // Handle config file
    if let Some(config_path) = cli.config {
        println!("Using config file: {:?}", config_path);
    }
 
    // Handle subcommands
    match cli.command {
        Commands::Add { files, force } => {
            println!("Adding files: {:?}, force: {}", files, force);
        }
        Commands::Commit { message, edit } => {
            if edit {
                println!("Opening editor...");
            }
            println!("Committing with message: {:?}", message);
        }
        Commands::Push { remote, branch } => {
            println!("Pushing to {} (branch: {:?})", remote, branch);
        }
        Commands::Pull { remote } => {
            println!("Pulling from {:?}", remote);
        }
    }
}

Enum Values and Validation

use clap::{Parser, ValueEnum};
 
#[derive(Parser, Debug)]
struct FormatArgs {
    /// Output format
    #[arg(value_enum)]
    format: OutputFormat,
 
    /// Sort order
    #[arg(short, long, value_enum, default_value = "asc")]
    sort: SortOrder,
}
 
#[derive(Clone, Debug, ValueEnum)]
enum OutputFormat {
    Json,
    Yaml,
    Toml,
    Text,
}
 
#[derive(Clone, Debug, ValueEnum)]
enum SortOrder {
    Asc,
    Desc,
}
 
fn main() {
    let args = FormatArgs::parse();
    println!("Format: {:?}, Sort: {:?}", args.format, args.sort);
}

Arguments with Custom Validation

use clap::Parser;
 
fn validate_port(port: &str) -> Result<u16, String> {
    let port: u16 = port.parse().map_err(|_| "Invalid port number")?;
    if port < 1024 {
        return Err("Port must be >= 1024".to_string());
    }
    Ok(port)
}
 
#[derive(Parser, Debug)]
struct ServerArgs {
    /// Port to listen on (must be >= 1024)
    #[arg(short, long, value_parser = validate_port)]
    port: u16,
 
    /// Host address
    #[arg(long, default_value = "127.0.0.1")]
    host: String,
}
 
fn main() {
    let args = ServerArgs::parse();
    println!("Server listening on {}:{{}}", args.host, args.port);
}

Running the CLI

# Show help
mycli --help
mycli add --help
 
# Basic usage
mycli add src/main.rs src/lib.rs
mycli add --force src/hidden.rs
mycli commit -m "Initial commit"
mycli push origin main
 
# With flags
mycli -v add src/main.rs
mycli -vv commit -m "Commit message"
mycli --config ./mycli.toml push
 
# With enum values
mycli search "pattern" --format json --sort desc
mycli search "pattern" -i -n 20

Summary

  • Use #[derive(Parser)] on a struct to define arguments; Parser::parse() reads from std::env::args()
  • #[arg(short, long)] creates both short (-f) and long (--flag) versions
  • #[command(subcommand)] enables subcommands with an enum marked with #[derive(Subcommand)]
  • Option<T> for optional arguments; Vec<T> for multiple values; defaults with default_value
  • Use #[derive(ValueEnum)] for arguments that accept a fixed set of values
  • Custom validation with value_parser function that returns Result<T, String>
  • Clap auto-generates -h (short help) and --help (long help) from your doc comments
  • The action = clap::ArgAction::Count pattern enables -v, -vv, -vvv style verbosity flags