How does clap::Arg::value_parser validate and transform CLI arguments before they reach the application?
clap::Arg::value_parser intercepts raw string arguments during parsing and applies a transformation function that validates input and converts it to a typed value, ensuring that only valid, properly-typed data reaches the application code. The value parser acts as a gatekeeper that can reject invalid arguments with helpful error messages, perform type conversions like parsing integers or enums, and even suggest valid alternatives when users make typos—all before the application sees any command-line input.
Basic value_parser Usage
use clap::{Arg, Command, value_parser};
fn basic_usage() {
// value_parser transforms string arguments to typed values
let cmd = Command::new("myapp")
.arg(
Arg::new("count")
.long("count")
.value_parser(value_parser!(u32)) // Parse as u32
);
let matches = cmd.try_get_matches_from(["myapp", "--count", "42"]);
match matches {
Ok(m) => {
// Value is already parsed as u32
let count: u32 = *m.get_one::<u32>("count").unwrap();
assert_eq!(count, 42);
}
Err(e) => {
// Invalid input like "--count abc" produces error
println!("Error: {}", e);
}
}
// Without value_parser, you'd get a &str and parse manually:
// let count: u32 = matches.get_one::<String>("count").unwrap().parse().unwrap();
}value_parser handles parsing and validation, returning typed values directly.
Built-in Type Parsers
use clap::{Arg, Command, value_parser};
fn builtin_parsers() {
// Numeric types
let cmd = Command::new("myapp")
.arg(Arg::new("port")
.long("port")
.value_parser(value_parser!(u16))) // u16: 0-65535
.arg(Arg::new("size")
.long("size")
.value_parser(value_parser!(i64))) // i64: any integer
.arg(Arg::new("ratio")
.long("ratio")
.value_parser(value_parser!(f64))); // f64: floating point
// Boolean
let cmd = Command::new("myapp")
.arg(Arg::new("verbose")
.long("verbose")
.value_parser(value_parser!(bool))
.default_value("false"));
// String types
let cmd = Command::new("myapp")
.arg(Arg::new("path")
.long("path")
.value_parser(value_parser!(std::path::PathBuf)));
// All standard types are supported:
// u8, u16, u32, u64, u128, usize
// i8, i16, i32, i64, i128, isize
// f32, f64
// bool, String, PathBuf, OsString
}Built-in parsers handle common types with appropriate error messages.
Custom Validation with Closures
use clap::{Arg, Command, value_parser};
fn custom_validation() {
// value_parser can take a closure for custom validation
let cmd = Command::new("myapp")
.arg(
Arg::new("port")
.long("port")
.value_parser(|s: &str| {
// Parse the string
let port: u16 = s.parse()
.map_err(|e| format!("Invalid port number: {}", e))?;
// Validate range
if port < 1024 {
return Err("Port must be >= 1024 (non-privileged)".into());
}
Ok(port)
})
);
// Valid port
let matches = cmd.clone().try_get_matches_from(["myapp", "--port", "8080"]);
assert!(matches.is_ok());
// Invalid: below 1024
let matches = cmd.clone().try_get_matches_from(["myapp", "--port", "80"]);
assert!(matches.is_err());
if let Err(e) = matches {
assert!(e.to_string().contains("non-privileged"));
}
// Invalid: not a number
let matches = cmd.clone().try_get_matches_from(["myapp", "--port", "abc"]);
assert!(matches.is_err());
}Closures enable arbitrary validation logic with custom error messages.
PathBuf and Path Validation
use clap::{Arg, Command, value_parser};
use std::path::PathBuf;
fn path_validation() {
// Built-in PathBuf parser validates paths
let cmd = Command::new("myapp")
.arg(
Arg::new("input")
.long("input")
.value_parser(value_parser!(PathBuf))
.exists(true) // Additional validation: file must exist
)
.arg(
Arg::new("output")
.long("output")
.value_parser(|s: &str| {
let path = PathBuf::from(s);
if path.exists() {
Err("Output file already exists".into())
} else {
Ok(path)
}
})
);
// exists(true) adds validation that file exists
// Custom parser above ensures output doesn't exist
}Path validation can check for existence or other filesystem properties.
Enum Value Parsing
use clap::{Arg, Command, value_parser, PossibleValue};
#[derive(Debug, Clone, Copy, PartialEq)]
enum OutputFormat {
Json,
Yaml,
Toml,
}
// Implement ValueEnum for enum parsing
impl clap::ValueEnum for OutputFormat {
fn value_variants<'a>() -> &'a [Self] {
&[OutputFormat::Json, OutputFormat::Yaml, OutputFormat::Toml]
}
fn to_possible_value<'a>(&self) -> Option<PossibleValue<'a>> {
match self {
OutputFormat::Json => Some(PossibleValue::new("json")),
OutputFormat::Yaml => Some(PossibleValue::new("yaml")),
OutputFormat::Toml => Some(PossibleValue::new("toml")),
}
}
}
fn enum_parsing() {
let cmd = Command::new("myapp")
.arg(
Arg::new("format")
.long("format")
.value_parser(value_parser!(OutputFormat))
);
let matches = cmd.try_get_matches_from(["myapp", "--format", "json"]);
assert!(matches.is_ok());
let format: OutputFormat = *matches.unwrap().get_one("format").unwrap();
assert_eq!(format, OutputFormat::Json);
// Invalid enum value produces helpful error
let matches = cmd.try_get_matches_from(["myapp", "--format", "xml"]);
assert!(matches.is_err());
// Error message lists valid values: json, yaml, toml
}Enum parsing automatically generates error messages listing valid values.
Range Validation
use clap::{Arg, Command, value_parser, value_parser_range};
fn range_validation() {
// value_parser_range restricts numeric values to a range
let cmd = Command::new("myapp")
.arg(
Arg::new("level")
.long("level")
.value_parser(value_parser_range!(1..=10)) // 1 to 10 inclusive
)
.arg(
Arg::new("percentage")
.long("percentage")
.value_parser(value_parser_range!(0..100)) // 0 to 99
);
// Valid
let matches = cmd.clone().try_get_matches_from(["myapp", "--level", "5"]);
assert!(matches.is_ok());
// Invalid: outside range
let matches = cmd.clone().try_get_matches_from(["myapp", "--level", "15"]);
assert!(matches.is_err());
if let Err(e) = matches {
assert!(e.to_string().contains("1..=10"));
}
}Range parsers validate that numeric values fall within specified bounds.
Default Values and Parser Interaction
use clap::{Arg, Command, value_parser};
fn default_values() {
// Default values go through the same parser
let cmd = Command::new("myapp")
.arg(
Arg::new("port")
.long("port")
.value_parser(value_parser!(u16))
.default_value("8080") // This string is parsed by value_parser
);
// No argument provided -> uses default
let matches = cmd.clone().try_get_matches_from(["myapp"]);
let port: u16 = *matches.unwrap().get_one("port").unwrap();
assert_eq!(port, 8080);
// Default values must be valid for the parser
// Invalid default causes panic at runtime:
// .value_parser(value_parser!(u16))
// .default_value("not_a_number") // PANIC: not valid u16
}Default values are also validated by the parser, catching configuration errors early.
Multiple Values and Parsers
use clap::{Arg, Command, value_parser};
fn multiple_values() {
// Parser applies to each value individually
let cmd = Command::new("myapp")
.arg(
Arg::new("ports")
.long("port")
.action(clap::ArgAction::Append) // Allow multiple values
.value_parser(value_parser!(u16))
);
let matches = cmd.try_get_matches_from(["myapp", "--port", "8080", "--port", "3000"]);
assert!(matches.is_ok());
let ports: Vec<u16> = matches.unwrap()
.get_many::<u16>("ports")
.unwrap()
.copied()
.collect();
assert_eq!(ports, vec![8080, 3000]);
// Invalid value among valid ones
let matches = cmd.try_get_matches_from(["myapp", "--port", "8080", "--port", "abc"]);
assert!(matches.is_err()); // Fails on "abc"
}Each value in a multi-value argument is parsed and validated independently.
Suggested Values and Typo Correction
use clap::{Arg, Command, value_parser, PossibleValue};
fn suggested_values() {
// value_parser with possible_values enables suggestions
#[derive(Clone, Debug)]
enum Mode {
Dev,
Staging,
Production,
}
impl clap::ValueEnum for Mode {
fn value_variants<'a>() -> &'a [Self] {
&[Mode::Dev, Mode::Staging, Mode::Production]
}
fn to_possible_value<'a>(&self) -> Option<PossibleValue<'a>> {
match self {
Mode::Dev => Some(PossibleValue::new("dev").alias("development")),
Mode::Staging => Some(PossibleValue::new("staging")),
Mode::Production => Some(PossibleValue::new("production").alias("prod")),
}
}
}
let cmd = Command::new("myapp")
.arg(
Arg::new("mode")
.long("mode")
.value_parser(value_parser!(Mode))
);
// Typo correction: "deve" -> suggests "dev"
let matches = cmd.clone().try_get_matches_from(["myapp", "--mode", "deve"]);
assert!(matches.is_err());
if let Err(e) = matches {
// Error includes suggested value
println!("Error: {}", e); // "did you mean 'dev'?"
}
// Aliases work too
let matches = cmd.try_get_matches_from(["myapp", "--mode", "prod"]);
assert!(matches.is_ok());
}Enum parsers enable typo suggestions for user-friendly error messages.
Error Message Customization
use clap::{Arg, Command, value_parser};
fn custom_errors() {
// Custom parser allows custom error messages
let cmd = Command::new("myapp")
.arg(
Arg::new("email")
.long("email")
.value_parser(|s: &str| -> Result<String, String> {
if !s.contains('@') {
return Err(format!("'{}' is not a valid email address (missing @)", s));
}
if s.len() > 100 {
return Err("Email address too long (max 100 chars)".into());
}
Ok(s.to_string())
})
);
let matches = cmd.clone().try_get_matches_from(["myapp", "--email", "user@example.com"]);
assert!(matches.is_ok());
let matches = cmd.clone().try_get_matches_from(["myapp", "--email", "invalid"]);
assert!(matches.is_err());
if let Err(e) = matches {
assert!(e.to_string().contains("missing @"));
}
}Custom error messages guide users toward correct input.
Validation Before Application Logic
use clap::{Arg, Command, value_parser};
fn separation_of_concerns() {
// Without value_parser: application handles parsing
fn app_without_parser() {
let cmd = Command::new("myapp")
.arg(Arg::new("count").long("count"));
let matches = cmd.get_matches();
let count_str = matches.get_one::<String>("count").unwrap();
// Application must handle parsing and errors
let count: u32 = match count_str.parse() {
Ok(n) => n,
Err(_) => {
eprintln!("Error: Invalid number");
return;
}
};
// Application must handle validation
if count > 100 {
eprintln!("Error: Count must be <= 100");
return;
}
println!("Processing {} items", count);
}
// With value_parser: validation happens before application
fn app_with_parser() {
let cmd = Command::new("myapp")
.arg(
Arg::new("count")
.long("count")
.value_parser(|s: &str| {
let n: u32 = s.parse()
.map_err(|_| "Invalid number")?;
if n > 100 {
return Err("Count must be <= 100".into());
}
Ok(n)
})
);
let matches = cmd.get_matches();
// Value is already validated and parsed
let count: u32 = *matches.get_one("count").unwrap();
// Application can trust the value
println!("Processing {} items", count);
}
}value_parser keeps validation at the CLI boundary, leaving application logic clean.
Combining with Other Validations
use clap::{Arg, Command, value_parser};
fn combined_validations() {
// value_parser works with other clap validation features
let cmd = Command::new("myapp")
.arg(
Arg::new("input")
.long("input")
.value_parser(value_parser!(std::path::PathBuf))
.exists(true) // File must exist
)
.arg(
Arg::new("count")
.long("count")
.value_parser(value_parser!(u32))
.default_value("1") // Default value
.value_parser(|s: &str| { // Custom validation
let n: u32 = s.parse().map_err(|_| "Invalid number")?;
if n == 0 { return Err("Count cannot be zero".into()); }
Ok(n)
})
)
.arg(
Arg::new("mode")
.long("mode")
.value_parser(["read", "write", "append"]) // Allowed values
);
// All validations are combined
// 1. PathBuf parser validates path format
// 2. exists(true) checks file exists
// 3. Custom parser validates count > 0
// 4. Allowed values restrict mode
}Multiple validation rules compose naturally.
Complete Example
use clap::{Arg, Command, value_parser, value_parser_range};
use std::path::PathBuf;
fn complete_example() {
let cmd = Command::new("server")
.about("Start a web server")
.arg(
Arg::new("port")
.long("port")
.short('p')
.value_parser(value_parser_range!(1..=65535))
.default_value("8080")
.help("Port to listen on (1-65535)")
)
.arg(
Arg::new("host")
.long("host")
.short('H')
.value_parser(|s: &str| {
// Basic hostname validation
if s.is_empty() {
return Err("Host cannot be empty".into());
}
if s.len() > 253 {
return Err("Hostname too long".into());
}
Ok(s.to_string())
})
.default_value("localhost")
.help("Host address to bind")
)
.arg(
Arg::new("workers")
.long("workers")
.short('w')
.value_parser(value_parser_range!(1..=100))
.default_value("4")
.help("Number of worker threads (1-100)")
)
.arg(
Arg::new("log_level")
.long("log-level")
.short('l')
.value_parser(["trace", "debug", "info", "warn", "error"])
.default_value("info")
.help("Log level")
)
.arg(
Arg::new("config")
.long("config")
.short('c')
.value_parser(value_parser!(PathBuf))
.exists(true)
.help("Configuration file path")
);
// Parse and use
let matches = cmd.get_matches();
let port: u16 = *matches.get_one("port").unwrap();
let host: &String = matches.get_one("host").unwrap();
let workers: usize = *matches.get_one("workers").unwrap();
let log_level: &String = matches.get_one("log_level").unwrap();
let config: Option<&PathBuf> = matches.get_one("config");
// All values are validated and typed
println!("Starting server on {}:{}", host, port);
println!("Workers: {}, Log level: {}", workers, log_level);
if let Some(config_path) = config {
println!("Config file: {:?}", config_path);
}
}A complete CLI with validated, typed arguments demonstrates the power of value_parser.
Synthesis
Core purpose:
// value_parser intercepts arguments during parsing
Arg::new("count")
.value_parser(value_parser!(u32))
// This means:
// 1. String "42" -> validated as u32 -> returned as u32
// 2. String "abc" -> validation fails -> error before app sees it
// 3. Application receives validated, typed valueTransformation flow:
CLI Arg (String) -> value_parser -> Validated Value (T)
|
v
[Parsing]
|
v
[Validation]
|
[Error or T]
Key benefits:
| Benefit | Description |
|---|---|
| Early validation | Invalid args rejected before app code |
| Type conversion | String -> typed value automatically |
| Custom errors | User-friendly error messages |
| Suggestions | Typo correction for enums |
| Separation | Validation at boundary, not in app logic |
Common patterns:
// Built-in type
.value_parser(value_parser!(u32))
// Range restriction
.value_parser(value_parser_range!(1..=100))
// Enum with suggestions
.value_parser(value_parser!(MyEnum))
// Custom validation
.value_parser(|s: &str| {
let value: Type = s.parse()?;
validate(value)?;
Ok(value)
})
// Allowed values
.value_parser(["read", "write", "append"])Key insight: value_parser shifts validation and parsing from application code to the CLI boundary, ensuring that when your application receives arguments from clap, they are already validated, parsed, and typed—invalid arguments never reach application logic, eliminating defensive parsing code and centralizing validation rules where they belong: at the interface between user input and application code. The parser transforms raw strings into typed values and can apply arbitrary validation logic, producing clear error messages that guide users toward correct input without cluttering application code with validation checks. This separation of concerns means application code can trust that get_one::<T>("arg") returns a valid value of the correct type, with all validation errors handled by clap's error system before the application even starts.
