|
| 1 | +# Clap Argument Validation |
| 2 | + |
| 3 | +Not every argument can be trusted simply because it parses into the right type. A string might |
| 4 | +convert fine into a [`PathBuf`] or a number, while still being meaningless in context, such as a |
| 5 | +percentage over 100 or a path that does not point to an existing file. Clap addresses this through |
| 6 | +the [`value_parser`] attribute, which lets you attach custom validation logic to an argument beyond |
| 7 | +basic type conversion. This recipe uses a disk-cleanup tool to demonstrate three common validation |
| 8 | +patterns: numeric ranges, file existence checks, and custom string formats. |
| 9 | + |
| 10 | +For numeric ranges, Clap provides a built-in solution. The `threshold` field uses |
| 11 | +`value_parser!(u8).range(0..=100)`, which rejects any value outside that range before the program |
| 12 | +even runs, removing the need for manual bounds checking in your own code. |
| 13 | + |
| 14 | +For validation that goes beyond simple ranges, you can write your own parser function and pass it to |
| 15 | +`value_parser`. The `config` field uses `parse_config`, which checks that the given path actually |
| 16 | +exists as a file. Similarly, the `notify` field uses `parse_email`, which performs a basic |
| 17 | +structural check on the input. Refer to this [`recipe`] for concrete email validation. Both |
| 18 | +functions return a `Result<T, String>`, where the error message becomes part of the output Clap |
| 19 | +shows the user when validation fails, making it clear which argument was invalid and why. |
| 20 | + |
| 21 | +> This recipe requires the [`derive`] feature flag to be enabled in `Cargo.toml`. |
| 22 | +
|
| 23 | +```rust,edition2024,no_run |
| 24 | +use clap::{Parser, value_parser}; |
| 25 | +use std::path::PathBuf; |
| 26 | +
|
| 27 | +#[derive(Debug, Parser)] |
| 28 | +struct CliArgs { |
| 29 | + /// Path to scan for cleanup |
| 30 | + #[arg(short = 'p', long = "path")] |
| 31 | + path: PathBuf, |
| 32 | +
|
| 33 | + /// Free space until usage falls below this percentage |
| 34 | + #[arg(short = 't', long = "threshold", value_parser = value_parser!(u8).range(0..=100))] |
| 35 | + threshold: u8, |
| 36 | +
|
| 37 | + /// Path to config file |
| 38 | + #[arg(short = 'c', long = "config", value_parser = parse_config)] |
| 39 | + config: PathBuf, |
| 40 | +
|
| 41 | + /// Email to notify on completion |
| 42 | + #[arg(short = 'n', long = "notify", value_parser = parse_email)] |
| 43 | + notify: String, |
| 44 | +} |
| 45 | +
|
| 46 | +fn parse_email(s: &str) -> Result<String, String> { |
| 47 | + match s.split_once('@') { |
| 48 | + Some((user, domain)) if !user.is_empty() && domain.contains('.') => Ok(s.to_string()), |
| 49 | + _ => Err("Invalid email format".to_string()), |
| 50 | + } |
| 51 | +} |
| 52 | +
|
| 53 | +fn parse_config(s: &str) -> Result<PathBuf, String> { |
| 54 | + let path = PathBuf::from(s); |
| 55 | + if path.is_file() { |
| 56 | + Ok(path) |
| 57 | + } else { |
| 58 | + Err("Provided config path is not a file".to_string()) |
| 59 | + } |
| 60 | +} |
| 61 | +
|
| 62 | +fn main() { |
| 63 | + let args = CliArgs::parse(); |
| 64 | + dbg!(args); |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +[`PathBuf`]: https://doc.rust-lang.org/std/path/struct.PathBuf.html |
| 69 | +[`derive`]: https://docs.rs/clap/*/clap/_features/index.html |
| 70 | +[`recipe`]: https://rust-lang-nursery.github.io/rust-cookbook/text/regex.html#verify-and-extract-login-from-an-email-address |
| 71 | +[`value_parser`]: https://docs.rs/clap/*/clap/macro.value_parser.html |
0 commit comments