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:
- Derive macro — define arguments with struct attributes
- Subcommands — nest commands for complex CLIs
- Type conversion — automatic parsing to target types
- Validation — built-in validation for values, counts, and ranges
- Help generation — automatic --help and usage strings
- 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-sand--shortflags - 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
