clap

The most popular command-line argument parser for Rust. Features fast, feature-rich, and customizable design.

rustclicommand-linederivebuilder

GitHub Overview

clap-rs/clap

A full featured, fast Command Line Argument Parser for Rust

Stars15,326
Watchers65
Forks1,113
Created:February 25, 2015
Language:Rust
License:Apache License 2.0

Topics

argument-parserargument-parsingcommand-linecommand-line-parserparsed-argumentspositional-argumentsrustsubcommands

Star History

clap-rs/clap Star History
Data as of: 7/25/2025, 02:04 AM

Framework

clap

Overview

clap is the most popular command-line parser library in Rust. Standing for "Command Line Argument Parser," it's designed to make building powerful and flexible CLI applications easy. In v4, the Derive API has become the primary recommended approach, enabling more intuitive and type-safe CLI development. The current version is 4.5.41 (2025) and is adopted by many famous Rust projects.

Details

clap v4 represents a significant evolution, focusing on API refinement, improved user experience, and enhanced performance. The most notable change is that the Derive API, integrated from structopt, has become the primary and recommended way to build CLIs. Originally, clap was developed based on structopt, directly integrating structopt in v3, and providing a unified API in v4.

Position in Rust Ecosystem

Why clap is the most popular CLI library in Rust:

  • Type Safety: Argument validation leveraging Rust's powerful type system
  • High Performance: Small binary size and high-speed parsing
  • Rich Features: Supports everything from simple CLIs to complex subcommand structures
  • Excellent DX: Compile-time error checking and excellent error messages
  • Active Maintenance: Regular updates and bug fixes

Key Features

  • Derive API: Declarative CLI definition using structs and attributes
  • Builder API: Flexible procedural approach for dynamic CLIs
  • Polished Help Output: Modern and readable help messages
  • Improved Error Messages: rustc-style diagnostic error reporting
  • Type Safety: Argument validation and conversion through a strong type system
  • Shell Completion: Auto-completion support for Bash, Zsh, Fish, PowerShell, and Elvish
  • High Performance: Reduced binary size and improved parsing speed
  • Customizable: Fine control through feature flags

New Features in v4

  • New Attributes: Clear notation with #[command(...)], #[arg(...)], #[value(...)]
  • Arg::num_args(range): Unified API for flexibly specifying argument counts
  • Styling: Customizable help output via Command::styles
  • Dynamic String Handling: Flexible handling of &'static str, Cow<'static, str>, Box<str>
  • Lightweight Dependencies: Removed heavy dependencies like Viper in v4
  • Better Error Messages: rustc-style diagnostic information and color output

Latest Trends (2024-2025)

  • Continuous Improvements: Regular patch releases with bug fixes and optimizations
  • Community Support: Integration with related crates like crossterm, inquire, and comfy-table
  • Enhanced Shell Completion: More advanced dynamic completion features
  • Builder and Derive API Interoperability: Both APIs can be used simultaneously

Pros and Cons

Pros

  • Type-safe and intuitive Derive API
  • Rich features and high flexibility
  • Excellent documentation and community support
  • High performance and small binary size
  • Capable of handling complex CLI applications
  • Follows standard command-line conventions

Cons

  • May be overkill for simple scripts
  • Somewhat steep learning curve (especially Builder API)
  • Impact on compile times
  • Complexity due to many feature flags

Key Links

Code Examples

Basic Derive API Example

use clap::Parser;

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(author, version, about, 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);
    }
}

Subcommands Example

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Add files to the index
    Add {
        /// Files to add
        #[arg(short, long)]
        files: Vec<String>,
    },
    /// Commit changes
    Commit {
        /// Commit message
        #[arg(short, long)]
        message: String,
        
        /// Amend the previous commit
        #[arg(long)]
        amend: bool,
    },
    /// Show repository status
    Status {
        /// Show short format
        #[arg(short, long)]
        short: bool,
    },
}

fn main() {
    let cli = Cli::parse();
    
    match &cli.command {
        Commands::Add { files } => {
            println!("Adding files: {:?}", files);
        }
        Commands::Commit { message, amend } => {
            if *amend {
                println!("Amending previous commit: {}", message);
            } else {
                println!("Committing: {}", message);
            }
        }
        Commands::Status { short } => {
            if *short {
                println!("M  file1.rs\nA  file2.rs");
            } else {
                println!("Modified files:\n  file1.rs\nNew files:\n  file2.rs");
            }
        }
    }
}

Builder API Example

use clap::{Command, Arg, ArgAction};

fn main() {
    let matches = Command::new("myapp")
        .version("1.0")
        .author("Your Name <[email protected]>")
        .about("Brief description of the program")
        .arg(
            Arg::new("config")
                .short('c')
                .long("config")
                .value_name("FILE")
                .help("Specify config file")
                .action(ArgAction::Set)
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .help("Enable verbose output")
                .action(ArgAction::Count)
        )
        .get_matches();
    
    if let Some(config) = matches.get_one::<String>("config") {
        println!("Config file: {}", config);
    }
    
    match matches.get_count("verbose") {
        0 => println!("Normal mode"),
        1 => println!("Verbose mode"),
        2 => println!("Debug mode"),
        _ => println!("Super verbose mode"),
    }
}

Advanced Features Example

use clap::{Parser, ValueEnum};
use std::path::PathBuf;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Input file
    #[arg(value_name = "FILE")]
    input: Option<PathBuf>,
    
    /// Output format
    #[arg(short, long, value_enum, default_value_t = OutputFormat::Json)]
    format: OutputFormat,
    
    /// Port number (1024-65535)
    #[arg(short, long, value_parser = clap::value_parser!(u16).range(1024..))]
    port: u16,
    
    /// Verbosity level (can be specified multiple times)
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
    
    /// Config file (can also be set via environment variable)
    #[arg(short, long, env = "MYAPP_CONFIG")]
    config: Option<PathBuf>,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum OutputFormat {
    Json,
    Yaml,
    Toml,
}

fn main() {
    let cli = Cli::parse();
    
    if let Some(input) = cli.input {
        println!("Input file: {}", input.display());
    }
    
    println!("Output format: {:?}", cli.format);
    println!("Port: {}", cli.port);
    println!("Verbosity level: {}", cli.verbose);
    
    if let Some(config) = cli.config {
        println!("Config file: {}", config.display());
    }
}

Custom Validation Example

use clap::Parser;
use std::net::IpAddr;

#[derive(Parser)]
struct Cli {
    /// Server address
    #[arg(long, value_parser = parse_server_address)]
    server: (IpAddr, u16),
    
    /// Email address
    #[arg(long, value_parser = validate_email)]
    email: String,
}

fn parse_server_address(s: &str) -> Result<(IpAddr, u16), String> {
    let parts: Vec<&str> = s.split(':').collect();
    if parts.len() != 2 {
        return Err("Address must be in 'IP:port' format".to_string());
    }
    
    let ip: IpAddr = parts[0].parse()
        .map_err(|_| "Invalid IP address".to_string())?;
    let port: u16 = parts[1].parse()
        .map_err(|_| "Invalid port number".to_string())?;
    
    Ok((ip, port))
}

fn validate_email(s: &str) -> Result<String, String> {
    if s.contains('@') && s.contains('.') {
        Ok(s.to_string())
    } else {
        Err("Please enter a valid email address".to_string())
    }
}

fn main() {
    let cli = Cli::parse();
    println!("Server: {:?}", cli.server);
    println!("Email: {}", cli.email);
}

Practical CLI Tool Example

use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use std::fs;

/// Efficient File Management CLI Tool
#[derive(Parser)]
#[command(name = "filemgr")]
#[command(about = "An efficient file management tool")]
#[command(version = "1.0.0")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
    
    /// Enable verbose output
    #[arg(short, long, global = true)]
    verbose: bool,
    
    /// Dry-run mode (don't actually execute)
    #[arg(short = 'n', long, global = true)]
    dry_run: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Copy files
    Copy {
        /// Source file/directory
        source: PathBuf,
        
        /// Destination directory
        dest: PathBuf,
        
        /// Overwrite existing files
        #[arg(short, long)]
        force: bool,
        
        /// Copy directories recursively
        #[arg(short, long)]
        recursive: bool,
    },
    
    /// Find files
    Find {
        /// Search pattern (glob format)
        pattern: String,
        
        /// Starting directory for search
        #[arg(short = 'd', long, default_value = ".")]
        directory: PathBuf,
        
        /// Filter by file type
        #[arg(short = 't', long, value_enum)]
        file_type: Option<FileType>,
        
        /// Maximum search depth
        #[arg(long, value_parser = clap::value_parser!(u32).range(1..=100))]
        max_depth: Option<u32>,
    },
    
    /// Display file information
    Info {
        /// Target files
        files: Vec<PathBuf>,
        
        /// Display in human-readable format
        #[arg(short = 'h', long)]
        human_readable: bool,
        
        /// Calculate checksum
        #[arg(long)]
        checksum: bool,
    },
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum FileType {
    /// Files only
    File,
    /// Directories only
    Dir,
    /// Symbolic links only
    Symlink,
}

fn main() {
    let cli = Cli::parse();
    
    match &cli.command {
        Commands::Copy { source, dest, force, recursive } => {
            if cli.verbose {
                println!("Starting copy: {} -> {}", source.display(), dest.display());
                println!("Settings: force={}, recursive={}, dry_run={}", force, recursive, cli.dry_run);
            }
            
            if !cli.dry_run {
                // Implement actual copy logic here
                println!("Copy completed");
            } else {
                println!("[Dry run] Skipped copy operation");
            }
        }
        
        Commands::Find { pattern, directory, file_type, max_depth } => {
            if cli.verbose {
                println!("Starting search: pattern='{}', directory='{}'", pattern, directory.display());
                if let Some(ft) = file_type {
                    println!("File type filter: {:?}", ft);
                }
                if let Some(depth) = max_depth {
                    println!("Max depth: {}", depth);
                }
            }
            
            // Implement actual search logic here
            println!("Search results: 5 files found");
        }
        
        Commands::Info { files, human_readable, checksum } => {
            for file in files {
                if cli.verbose {
                    println!("Getting file info: {}", file.display());
                }
                
                match fs::metadata(file) {
                    Ok(metadata) => {
                        println!("File: {}", file.display());
                        
                        if *human_readable {
                            println!("  Size: {} bytes", metadata.len());
                            println!("  Type: {}", if metadata.is_file() { "File" } else { "Directory" });
                        } else {
                            println!("  Size: {}", metadata.len());
                            println!("  Type: {}", metadata.file_type().is_file());
                        }
                        
                        if *checksum && metadata.is_file() {
                            println!("  Hash: [calculating...]");
                        }
                    }
                    Err(e) => {
                        eprintln!("Error: {}: {}", file.display(), e);
                    }
                }
            }
        }
    }
}

Derive API and Builder API Interoperability Example

use clap::{Args, Command, Parser, Subcommand};

#[derive(Parser)]
#[command(name = "hybrid")]
#[command(about = "Example combining Derive API and Builder API")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Statically defined command
    Static(StaticArgs),
    
    /// Dynamically generated command (extended with Builder API)
    Dynamic,
}

#[derive(Args)]
struct StaticArgs {
    /// Input file
    #[arg(short, long)]
    input: String,
    
    /// Output format
    #[arg(short, long, default_value = "json")]
    format: String,
}

fn main() {
    let mut app = Cli::command();
    
    // Use Builder API to dynamically add subcommands
    let dynamic_cmd = Command::new("dynamic")
        .about("Dynamically generated command")
        .arg(clap::Arg::new("count")
            .short('c')
            .long("count")
            .value_parser(clap::value_parser!(u32))
            .help("Number of executions"))
        .arg(clap::Arg::new("output")
            .short('o')
            .long("output")
            .value_name("FILE")
            .help("Output file"));
    
    // Replace existing subcommand
    if let Some(subcommands) = app.get_subcommands_mut().find(|cmd| cmd.get_name() == "dynamic") {
        *subcommands = dynamic_cmd;
    }
    
    let matches = app.get_matches();
    
    match matches.subcommand() {
        Some(("static", sub_matches)) => {
            let args = StaticArgs::from_arg_matches(sub_matches).unwrap();
            println!("Static command executed: input={}, format={}", args.input, args.format);
        }
        Some(("dynamic", sub_matches)) => {
            let count = sub_matches.get_one::<u32>("count").unwrap_or(&1);
            let output = sub_matches.get_one::<String>("output");
            
            println!("Dynamic command executed: count={}", count);
            if let Some(output) = output {
                println!("Output to: {}", output);
            }
        }
        _ => unreachable!(),
    }
}