How do I parse command line arguments in Rust?

Walkthrough

Clap (Command Line Argument Parser) is the most popular crate for building command-line interfaces in Rust. It provides a derive-based API for declarative argument definition and generates help text, error messages, and shell completions automatically. Clap handles parsing, validation, subcommands, and provides a polished CLI experience.

Key features:

  1. Derive macro — define arguments with struct attributes
  2. Subcommands — nest commands for complex CLIs
  3. Type conversion — automatic parsing to target types
  4. Validation — built-in validation for values, counts, and ranges
  5. Help generation — automatic --help and usage strings
  6. Shell completions — generate completions for bash, zsh, fish, etc.

Clap excels at building professional CLI tools with minimal boilerplate.

Code Example

# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;
 
/// A simple file search tool
#[derive(Parser, Debug)]
#[command(name = "search")]
#[command(about = "Search for files and text", long_about = None)]
struct Args {
    /// Search pattern
    #[arg(short, long)]
    pattern: String,
    
    /// Directory to search in
    #[arg(short, long, default_value = ".")]
    directory: String,
    
    /// Maximum depth to search
    #[arg(short, long, default_value = "10")]
    depth: u32,
    
    /// Case insensitive search
    #[arg(short, long)]
    ignore_case: bool,
    
    /// Verbose output (can be used multiple times)
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
}
 
fn main() {
    let args = Args::parse();
    
    println!("Pattern: {}", args.pattern);
    println!("Directory: {}", args.directory);
    println!("Depth: {}", args.depth);
    println!("Ignore case: {}", args.ignore_case);
    println!("Verbose level: {}", args.verbose);
}

Subcommands

use clap::{Parser, Subcommand};
 
#[derive(Parser, Debug)]
#[command(name = "git")]
#[command(about = "A version control system", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
    
    /// Global verbose flag
    #[arg(short, long, global = true)]
    verbose: bool,
}
 
#[derive(Subcommand, Debug)]
enum Commands {
    /// Clone a repository
    Clone {
        /// Repository URL
        url: String,
        /// Target directory
        #[arg(short, long)]
        directory: Option<String>,
        /// Clone depth
        #[arg(long, default_value = "0")]
        depth: u32,
    },
    /// Show commit logs
    Log {
        /// Number of commits to show
        #[arg(short, long, default_value = "10")]
        count: usize,
        /// Show one-line format
        #[arg(short, long)]
        oneline: bool,
        /// Specific branch
        #[arg(short, long)]
        branch: Option<String>,
    },
    /// Add files to staging
    Add {
        /// Files to add
        #[arg(required = true)]
        files: Vec<String>,
        /// Add all files
        #[arg(short, long, conflicts_with = "files")]
        all: bool,
    },
    /// Push to remote
    Push {
        /// Remote name
        #[arg(default_value = "origin")]
        remote: String,
        /// Branch name
        branch: Option<String>,
        /// Force push
        #[arg(short, long)]
        force: bool,
    },
}
 
fn main() {
    let cli = Cli::parse();
    
    if cli.verbose {
        println!("Verbose mode enabled");
    }
    
    match cli.command {
        Commands::Clone { url, directory, depth } => {
            println!("Cloning {} (depth: {})", url, depth);
            if let Some(dir) = directory {
                println!("Into directory: {}", dir);
            }
        }
        Commands::Log { count, oneline, branch } => {
            println!("Showing {} commits", count);
            if oneline { println!("Format: oneline"); }
            if let Some(b) = branch { println!("Branch: {}", b); }
        }
        Commands::Add { files, all } => {
            if all {
                println!("Adding all files");
            } else {
                println!("Adding files: {:?}", files);
            }
        }
        Commands::Push { remote, branch, force } => {
            println!("Pushing to {}", remote);
            if let Some(b) = branch { println!("Branch: {}", b); }
            if force { println!("Force push!"); }
        }
    }
}

Positional Arguments and Variadic Args

use clap::Parser;
 
#[derive(Parser, Debug)]
#[command(name = "cp")]
struct Args {
    /// Source file(s)
    #[arg(required = true)]
    source: Vec<String>,
    
    /// Destination
    #[arg(required = true)]
    destination: String,
    
    /// Recursive copy (for directories)
    #[arg(short = 'R', long)]
    recursive: bool,
    
    /// Force overwrite
    #[arg(short, long)]
    force: bool,
    
    /// Preserve permissions and timestamps
    #[arg(short = 'p', long)]
    preserve: bool,
}
 
fn main() {
    let args = Args::parse();
    
    println!("Sources: {:?}", args.source);
    println!("Destination: {}", args.destination);
    println!("Recursive: {}", args.recursive);
    println!("Force: {}", args.force);
    println!("Preserve: {}", args.preserve);
}

Validation and Custom Types

use clap::Parser;
use std::path::PathBuf;
 
#[derive(Parser, Debug)]
struct Args {
    /// Input file (must exist)
    #[arg(short, long, value_name = "FILE")]
    input: PathBuf,
    
    /// Output file
    #[arg(short, long, value_name = "FILE")]
    output: PathBuf,
    
    /// Port number (1-65535)
    #[arg(short, long, value_parser = clap::value_parser!(u16).range(1..))]
    port: u16,
    
    /// Compression level (1-9)
    #[arg(short, long, default_value = "6", value_parser = ["1", "2", "3", "4", "5", "6", "7", "8", "9"])]
    compression: String,
    
    /// Number of threads (1-16)
    #[arg(short, long, default_value = "4", value_parser = clap::value_parser!(u8).range(1..=16))]
    threads: u8,
}
 
fn main() {
    let args = Args::parse();
    
    println!("Input: {:?}", args.input);
    println!("Output: {:?}", args.output);
    println!("Port: {}", args.port);
    println!("Compression: {}", args.compression);
    println!("Threads: {}", args.threads);
}

Arguments from Environment and Files

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// API key (can be set via API_KEY env var)
    #[arg(short, long, env = "API_KEY")]
    api_key: String,
    
    /// Server URL
    #[arg(long, env = "SERVER_URL", default_value = "http://localhost:8080")]
    server: String,
    
    /// Configuration file
    #[arg(short, long, value_name = "FILE")]
    config: Option<String>,
    
    /// Debug mode
    #[arg(short, long, env = "DEBUG", action = clap::ArgAction::SetTrue)]
    debug: bool,
}
 
fn main() {
    let args = Args::parse();
    
    println!("API Key: {}", args.api_key);
    println!("Server: {}", args.server);
    println!("Config: {:?}", args.config);
    println!("Debug: {}", args.debug);
}

Groups and Mutual Exclusion

use clap::{Parser, ArgGroup};
 
#[derive(Parser, Debug)]
#[command(group(
    ArgGroup::new("mode")
        .args(["encode", "decode"])
        .required(true)
))]
struct Args {
    /// Encode mode
    #[arg(short, long)]
    encode: bool,
    
    /// Decode mode
    #[arg(short, long, conflicts_with = "encode")]
    decode: bool,
    
    /// Input file
    #[arg(short, long)]
    input: String,
    
    /// Output file
    #[arg(short, long)]
    output: Option<String>,
    
    /// Compression algorithm
    #[arg(long, value_enum, default_value = "gzip")]
    algorithm: Algorithm,
}
 
#[derive(clap::ValueEnum, Clone, Debug)]
enum Algorithm {
    Gzip,
    Zstd,
    Lz4,
}
 
fn main() {
    let args = Args::parse();
    
    let mode = if args.encode { "encode" } else { "decode" };
    println!("Mode: {}", mode);
    println!("Input: {}", args.input);
    println!("Algorithm: {:?}", args.algorithm);
}

Building Arguments Programmatically (Builder API)

use clap::{Arg, Command};
 
fn main() {
    let matches = Command::new("myapp")
        .version("1.0")
        .about("Does awesome things")
        .arg(
            Arg::new("input")
                .short('i')
                .long("input")
                .value_name("FILE")
                .help("Input file")
                .required(true),
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .value_name("FILE")
                .help("Output file"),
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .action(clap::ArgAction::Count)
                .help("Increase verbosity"),
        )
        .subcommand(
            Command::new("init")
                .about("Initialize the project")
                .arg(
                    Arg::new("name")
                        .help("Project name")
                        .required(true),
                ),
        )
        .subcommand(
            Command::new("build")
                .about("Build the project")
                .arg(
                    Arg::new("release")
                        .short('r')
                        .long("release")
                        .action(clap::ArgAction::SetTrue)
                        .help("Build in release mode"),
                ),
        )
        .get_matches();
    
    let input = matches.get_one::<String>("input").unwrap();
    let output = matches.get_one::<String>("output");
    let verbose = matches.get_count("verbose");
    
    println!("Input: {}", input);
    if let Some(out) = output {
        println!("Output: {}", out);
    }
    println!("Verbose: {}", verbose);
    
    match matches.subcommand() {
        Some(("init", sub_matches)) => {
            let name = sub_matches.get_one::<String>("name").unwrap();
            println!("Initializing project: {}", name);
        }
        Some(("build", sub_matches)) => {
            let release = sub_matches.get_flag("release");
            println!("Building (release: {})", release);
        }
        _ => {}
    }
}

Complete CLI Application Example

use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
 
/// A task management CLI
#[derive(Parser, Debug)]
#[command(name = "task")]
#[command(version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
    
    /// Output format
    #[arg(long, value_enum, global = true, default_value = "text")]
    format: OutputFormat,
    
    /// Quiet mode
    #[arg(short, long, global = true)]
    quiet: bool,
}
 
#[derive(Subcommand, Debug)]
enum Commands {
    /// Add a new task
    Add {
        /// Task title
        title: String,
        /// Task description
        #[arg(short, long)]
        description: Option<String>,
        /// Priority level
        #[arg(short, long, value_enum, default_value = "medium")]
        priority: Priority,
        /// Due date (YYYY-MM-DD)
        #[arg(long)]
        due: Option<String>,
    },
    /// List all tasks
    List {
        /// Filter by status
        #[arg(short, long, value_enum)]
        status: Option<Status>,
        /// Show completed tasks
        #[arg(short, long)]
        all: bool,
    },
    /// Mark a task as complete
    Done {
        /// Task ID
        id: u32,
    },
    /// Remove a task
    Remove {
        /// Task ID(s) to remove
        #[arg(required = true)]
        ids: Vec<u32>,
        /// Force removal without confirmation
        #[arg(short, long)]
        force: bool,
    },
    /// Export tasks
    Export {
        /// Output file
        #[arg(short, long)]
        output: PathBuf,
        /// Export format
        #[arg(long, value_enum, default_value = "json")]
        format: ExportFormat,
    },
}
 
#[derive(ValueEnum, Clone, Debug)]
enum Priority {
    Low,
    Medium,
    High,
}
 
#[derive(ValueEnum, Clone, Debug)]
enum Status {
    Pending,
    InProgress,
    Completed,
}
 
#[derive(ValueEnum, Clone, Debug)]
enum OutputFormat {
    Text,
    Json,
}
 
#[derive(ValueEnum, Clone, Debug)]
enum ExportFormat {
    Json,
    Csv,
}
 
fn main() {
    let cli = Cli::parse();
    
    if !cli.quiet {
        println!("Format: {:?}", cli.format);
    }
    
    match cli.command {
        Commands::Add { title, description, priority, due } => {
            println!("Adding task: {}", title);
            if let Some(desc) = description {
                println!("  Description: {}", desc);
            }
            println!("  Priority: {:?}", priority);
            if let Some(d) = due {
                println!("  Due: {}", d);
            }
        }
        Commands::List { status, all } => {
            println!("Listing tasks...");
            if let Some(s) = status {
                println!("  Status filter: {:?}", s);
            }
            if all {
                println!("  Showing all tasks");
            }
        }
        Commands::Done { id } => {
            println!("Marking task {} as complete", id);
        }
        Commands::Remove { ids, force } => {
            println!("Removing tasks: {:?}", ids);
            if force {
                println!("  (forced, no confirmation)");
            }
        }
        Commands::Export { output, format } => {
            println!("Exporting to {:?} in {:?} format", output, format);
        }
    }
}

Summary

  • Use #[derive(Parser)] on a struct to define CLI arguments
  • Add #[arg(short, long)] to create -s and --short flags
  • Use #[arg(default_value = "...")] for optional arguments with defaults
  • Option<T> fields become optional arguments; Vec<T> collects multiple values
  • Add #[command(subcommand)] with an enum for subcommands
  • Use #[arg(required = true)] for mandatory positional arguments
  • #[arg(action = clap::ArgAction::Count)] counts flag occurrences (like -vvv)
  • #[arg(env = "VAR_NAME")] reads from environment variables
  • #[arg(value_parser = [...])] restricts to allowed values
  • #[arg(conflicts_with = "other")] makes arguments mutually exclusive
  • #[derive(ValueEnum)] on enums creates argument value options
  • #[arg(global = true)] makes flags available to all subcommands
  • Access parsed values directly from the struct fields after Args::parse()
  • Use the builder API (Command::new()) for dynamic argument construction
  • Help text comes from doc comments; use #[command(about, version)] for metadata