clap
The most popular command-line argument parser for Rust. Features fast, feature-rich, and customizable design.
GitHub Overview
clap-rs/clap
A full featured, fast Command Line Argument Parser for Rust
Topics
Star History
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!(),
}
}