How do I work with Clap for Command Line Argument Parsing in Rust?

Walkthrough

Clap (Command Line Argument Parser) is a popular Rust library for parsing command line arguments. It provides a derive macro for declarative argument definition and a builder API for more control. Clap automatically generates help messages, handles argument validation, and supports subcommands.

Key concepts:

  • Derive macro#[derive(Parser)] for declarative definition
  • Arguments — Positional arguments with #[arg]
  • Options — Named arguments with -- prefix
  • Flags — Boolean switches
  • Subcommands — Nested command structures
  • Derive vs Builder — Declarative vs programmatic API

When to use Clap:

  • CLI applications with complex argument structures
  • Tools requiring subcommands (like git)
  • Applications needing validation and help generation
  • Scripts requiring user-friendly argument handling

When NOT to use Clap:

  • Very simple argument parsing (use std::env::args)
  • Non-CLI applications
  • When you need minimal dependencies

Code Examples

Basic CLI Application

use clap::Parser;
 
#[derive(Parser, Debug)]
#[command(name = "myapp")]\n#[command(about = "A simple CLI application", long_about = None)]
struct Args {
    /// Name of the person to greet
    #[arg(short, long)]
    name: String,
    
    /// Number of times to greet
    #[arg(short, long, default_value_t = 1)]
    count: u8,
}
 
fn main() {
    let args = Args::parse();
    
    for _ in 0..args.count {
        println!("Hello, {}!", args.name);
    }
}

Positional Arguments

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Input file path
    input: String,
    
    /// Output file path
    output: String,
}
 
fn main() {
    let args = Args::parse();
    println!("Input: {}, Output: {}", args.input, args.output);
}

Optional Arguments

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Input file
    input: String,
    
    /// Output file (optional)
    #[arg(short, long)]
    output: Option<String>,
}
 
fn main() {
    let args = Args::parse();
    match args.output {
        Some(path) => println!("Output to: {}", path),
        None => println!("Output to stdout"),
    }
}

Flags (Boolean Switches)

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Enable verbose output
    #[arg(short, long)]
    verbose: bool,
    
    /// Force overwrite
    #[arg(short, long)]
    force: bool,
}
 
fn main() {
    let args = Args::parse();
    
    if args.verbose {
        println!("Verbose mode enabled");
    }
    
    if args.force {
        println!("Force mode enabled");
    }
}

Default Values

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Port to listen on
    #[arg(short, long, default_value = "8080")]
    port: u16,
    
    /// Host address
    #[arg(long, default_value = "localhost")]
    host: String,
}
 
fn main() {
    let args = Args::parse();
    println!("Listening on {}: {}", args.host, args.port);
}

Subcommands

use clap::{Parser, Subcommand};
 
#[derive(Parser, Debug)]
#[command(name = "git")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}
 
#[derive(Subcommand, Debug)]
enum Commands {
    /// Add files to staging
    Add {
        /// Files to add
        #[arg(required = true)]
        files: Vec<String>,
        
        /// Add all files
        #[arg(short, long)]
        all: bool,
    },
    /// Commit changes
    Commit {
        /// Commit message
        #[arg(short, long)]
        message: String,
    },
    /// Push to remote
    Push {
        /// Remote name
        #[arg(default_value = "origin")]
        remote: String,
        
        /// Branch name
        #[arg(default_value = "main")]
        branch: String,
    },
}
 
fn main() {
    let cli = Cli::parse();
    
    match cli.command {
        Commands::Add { files, all } => {
            if all {
                println!("Adding all files");
            } else {
                println!("Adding files: {:?}", files);
            }
        }
        Commands::Commit { message } => {
            println!("Committing: {}", message);
        }
        Commands::Push { remote, branch } => {
            println!("Pushing to {}/{}", remote, branch);
        }
    }
}

Multiple Values

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Files to process
    #[arg(short, long)]
    files: Vec<String>,
}
 
fn main() {
    let args = Args::parse();
    println!("Processing {} files:", args.files.len());
    for file in &args.files {
        println!("  - {}", file);
    }
}

Argument Validation

use clap::Parser;
use std::path::PathBuf;
 
#[derive(Parser, Debug)]
struct Args {
    /// Input file (must exist)
    #[arg(short, long, value_parser = clap::value_parser!(PathBuf))]
    input: PathBuf,
    
    /// Port number (1-65535)
    #[arg(long, value_parser = validate_port)]
    port: u16,
}
 
fn validate_port(s: &str) -> Result<u16, String> {
    let port: u16 = s.parse().map_err(|_| "Invalid port number".to_string())?;
    if port == 0 {
        Err("Port cannot be zero".to_string())
    } else {
        Ok(port)
    }
}
 
fn main() {
    let args = Args::parse();
    println!("Input: {:?}, Port: {}", args.input, args.port);
}

Custom Parser

use clap::Parser;
use std::net::SocketAddr;
 
#[derive(Parser, Debug)]
struct Args {
    /// Server address (e.g., 127.0.0.1:8080)
    #[arg(short, long, value_parser = parse_addr)]
    addr: SocketAddr,
}
 
fn parse_addr(s: &str) -> Result<SocketAddr, String> {
    s.parse().map_err(|_| format!("Invalid address: {}", s))
}
 
fn main() {
    let args = Args::parse();
    println!("Listening on {}", args.addr);
}

Environment Variables

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// API key (from --api-key or API_KEY env var)
    #[arg(long, env = "API_KEY")]
    api_key: String,
    
    /// Database URL
    #[arg(long, env = "DATABASE_URL")]
    database_url: Option<String>,
}
 
fn main() {
    let args = Args::parse();
    println!("API Key: {}", args.api_key);
    if let Some(url) = args.database_url {
        println!("Database: {}", url);
    }
}

Conflicting Arguments

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Use HTTP
    #[arg(long)]
    http: bool,
    
    /// Use HTTPS
    #[arg(long, conflicts_with = "http")]
    https: bool,
    
    /// Verbose output
    #[arg(short, long)]
    verbose: bool,
}
 
fn main() {
    let args = Args::parse();
    
    if args.http {
        println!("Using HTTP");
    }
    if args.https {
        println!("Using HTTPS");
    }
}

Required If

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Enable SSL
    #[arg(long)]
    ssl: bool,
    
    /// SSL certificate path (required if --ssl)
    #[arg(long, required_if_eq("ssl", "true"))]
    ssl_cert: Option<String>,
}
 
fn main() {
    let args = Args::parse();
    println!("SSL: {:?}, Cert: {:?}", args.ssl, args.ssl_cert);
}

Argument Groups

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Input file
    #[arg(short, long, group = "input")]
    file: Option<String>,
    
    /// Input from stdin
    #[arg(long, group = "input")]
    stdin: bool,
}
 
fn main() {
    let args = Args::parse();
    
    if args.stdin {
        println!("Reading from stdin");
    } else if let Some(file) = args.file {
        println!("Reading from file: {}", file);
    }
}

Version Information

use clap::Parser;
 
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    /// Input file
    input: String,
}
 
fn main() {
    let args = Args::parse();
    println!("Input: {}", args.input);
}

Hide Arguments

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Public argument
    #[arg(long)]
    public: String,
    
    /// Hidden argument (not shown in help)
    #[arg(long, hide = true)]
    secret: Option<String>,
}
 
fn main() {
    let args = Args::parse();
    if let Some(secret) = args.secret {
        println!("Secret provided");
    }
}

Short and Long Aliases

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    /// Output file
    #[arg(short = 'o', long = "output")]
    output: String,
    
    /// Verbose (can use -v or --verbose)
    #[arg(short, long)]
    verbose: bool,
}
 
fn main() {
    let args = Args::parse();
    println!("Output: {}, Verbose: {}", args.output, args.verbose);
}

Builder API

use clap::{Arg, Command};
 
fn main() {
    let matches = Command::new("myapp")
        .about("A CLI application")
        .arg(
            Arg::new("name")
                .short('n')
                .long("name")
                .value_name("NAME")
                .help("Name to greet")
                .required(true),
        )
        .arg(
            Arg::new("count")
                .short('c')
                .long("count")
                .value_name("N")
                .help("Number of greetings")
                .default_value("1"),
        )
        .get_matches();
    
    let name = matches.get_one::<String>("name").unwrap();
    let count: u32 = matches.get_one::<String>("count").unwrap().parse().unwrap();
    
    for _ in 0..count {
        println!("Hello, {}!", name);
    }
}

Nested Subcommands

use clap::{Parser, Subcommand};
 
#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}
 
#[derive(Subcommand)]
enum Commands {
    /// Database operations
    Db {
        #[command(subcommand)]
        command: DbCommands,
    },
}
 
#[derive(Subcommand)]
enum DbCommands {
    /// Run migrations
    Migrate {
        /// Migration name
        name: String,
    },
    /// Reset database
    Reset,
    /// Seed database
    Seed {
        /// Seed file
        #[arg(short, long)]
        file: String,
    },
}
 
fn main() {
    let cli = Cli::parse();
    
    match cli.command {
        Commands::Db { command } => match command {
            DbCommands::Migrate { name } => println!("Migrating: {}", name),
            DbCommands::Reset => println!("Resetting database"),
            DbCommands::Seed { file } => println!("Seeding from: {}", file),
        },
    }
}

Custom Help Template

use clap::Parser;
 
#[derive(Parser, Debug)]
#[command(help_template = "{name} {version}\n{usage}\n\n{all-args}")]
struct Args {
    /// Input file
    input: String,
    
    /// Output file
    #[arg(short, long)]
    output: Option<String>,
}
 
fn main() {
    let args = Args::parse();
    println!("Input: {:?}", args.input);
}

Testing CLI Applications

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    #[arg(short, long)]
    name: String,
    
    #[arg(short, long, default_value_t = 1)]
    count: u8,
}
 
fn run(args: Args) {
    for _ in 0..args.count {
        println!("Hello, {}!", args.name);
    }
}
 
fn main() {
    let args = Args::parse();
    run(args);
}
 
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_greeting() {
        let args = Args::try_parse_from(["test", "--name", "World", "--count", "2"]);
        assert!(args.is_ok());
        let args = args.unwrap();
        assert_eq!(args.name, "World");
        assert_eq!(args.count, 2);
    }
    
    #[test]
    fn test_missing_required() {
        let result = Args::try_parse_from(["test"]);
        assert!(result.is_err());
    }
}

Color Output

use clap::Parser;
 
#[derive(Parser, Debug)]
#[command(color = clap::ColorChoice::Always)]
struct Args {
    /// Input file
    input: String,
}
 
fn main() {
    let args = Args::parse();
    println!("Input: {}", args.input);
}

Parse from Iterator

use clap::Parser;
 
#[derive(Parser, Debug)]
struct Args {
    #[arg(short, long)]
    name: String,
}
 
fn main() {
    // Parse from custom iterator
    let args = Args::parse_from(["myapp", "--name", "Alice"]);
    println!("Name: {}", args.name);
    
    // Or try_parse_from for error handling
    match Args::try_parse_from(["myapp", "--name", "Bob"]) {
        Ok(args) => println!("Parsed: {}", args.name),
        Err(e) => println!("Error: {}", e),
    }
}

Summary

Clap Key Imports:

use clap::{Parser, Subcommand};

Derive Macros:

Macro Description
#[derive(Parser)] Main argument struct
#[derive(Subcommand)] Subcommand enum
#[derive(Args)] Nested argument groups

Common Attributes:

Attribute Description
short Short flag (-x)
long Long flag (--name)
default_value Default if not provided
required Must be provided
help Help text
env Environment variable
conflicts_with Mutually exclusive

Argument Types:

Type Example
Positional input: String
Option #[arg(short, long)] output: Option<String>
Flag #[arg(short, long)] verbose: bool
Multiple #[arg(short, long)] files: Vec<String>

Subcommand Structure:

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}
 
#[derive(Subcommand)]
enum Commands {
    Add { /* ... */ },
    Remove { /* ... */ },
}

Key Points:

  • Use derive macro for most cases (cleaner)
  • Use builder API for dynamic argument generation
  • Subcommands support nested structures
  • Automatic help generation with doc comments
  • Support for validation and custom parsers
  • Environment variable integration
  • Testing with try_parse_from
  • Color output customization