Initial commit
This commit is contained in:
164
skills/clap-patterns/examples/quick-start.md
Normal file
164
skills/clap-patterns/examples/quick-start.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Clap Quick Start Guide
|
||||
|
||||
This guide will help you build your first Clap CLI application in minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust installed (1.70.0 or newer)
|
||||
- Cargo (comes with Rust)
|
||||
|
||||
## Step 1: Create a New Project
|
||||
|
||||
```bash
|
||||
cargo new my-cli
|
||||
cd my-cli
|
||||
```
|
||||
|
||||
## Step 2: Add Clap Dependency
|
||||
|
||||
Edit `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
```
|
||||
|
||||
## Step 3: Write Your First CLI
|
||||
|
||||
Replace `src/main.rs` with:
|
||||
|
||||
```rust
|
||||
use clap::Parser;
|
||||
|
||||
/// Simple program to greet a person
|
||||
#[derive(Parser)]
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Build and Run
|
||||
|
||||
```bash
|
||||
# Build the project
|
||||
cargo build --release
|
||||
|
||||
# Run with arguments
|
||||
./target/release/my-cli --name Alice --count 3
|
||||
|
||||
# Check help output
|
||||
./target/release/my-cli --help
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
```
|
||||
$ ./target/release/my-cli --name Alice --count 3
|
||||
Hello Alice!
|
||||
Hello Alice!
|
||||
Hello Alice!
|
||||
```
|
||||
|
||||
## Help Output
|
||||
|
||||
```
|
||||
$ ./target/release/my-cli --help
|
||||
Simple program to greet a person
|
||||
|
||||
Usage: my-cli --name <NAME> [--count <COUNT>]
|
||||
|
||||
Options:
|
||||
-n, --name <NAME> Name of the person to greet
|
||||
-c, --count <COUNT> Number of times to greet [default: 1]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Add Subcommands**: See `subcommands.rs` template
|
||||
2. **Add Validation**: See `value-parser.rs` template
|
||||
3. **Environment Variables**: See `env-variables.rs` template
|
||||
4. **Type-Safe Options**: See `value-enum.rs` template
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Optional Arguments
|
||||
|
||||
```rust
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
```
|
||||
|
||||
### Multiple Values
|
||||
|
||||
```rust
|
||||
#[arg(short, long, num_args = 1..)]
|
||||
files: Vec<PathBuf>,
|
||||
```
|
||||
|
||||
### Boolean Flags
|
||||
|
||||
```rust
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
```
|
||||
|
||||
### With Default Value
|
||||
|
||||
```rust
|
||||
#[arg(short, long, default_value = "config.toml")]
|
||||
config: String,
|
||||
```
|
||||
|
||||
### Required Unless Present
|
||||
|
||||
```rust
|
||||
#[arg(long, required_unless_present = "config")]
|
||||
database_url: Option<String>,
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Parser trait not found"
|
||||
|
||||
Add the import:
|
||||
```rust
|
||||
use clap::Parser;
|
||||
```
|
||||
|
||||
### "derive feature not enabled"
|
||||
|
||||
Update `Cargo.toml`:
|
||||
```toml
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
```
|
||||
|
||||
### Help text not showing
|
||||
|
||||
Add doc comments above fields:
|
||||
```rust
|
||||
/// This shows up in --help output
|
||||
#[arg(short, long)]
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Full templates: `skills/clap-patterns/templates/`
|
||||
- Helper scripts: `skills/clap-patterns/scripts/`
|
||||
- Official docs: https://docs.rs/clap/latest/clap/
|
||||
474
skills/clap-patterns/examples/real-world-cli.md
Normal file
474
skills/clap-patterns/examples/real-world-cli.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# Real-World Clap CLI Example
|
||||
|
||||
A complete, production-ready CLI application demonstrating best practices.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
my-tool/
|
||||
├── Cargo.toml
|
||||
├── src/
|
||||
│ ├── main.rs # CLI definition and entry point
|
||||
│ ├── commands/ # Command implementations
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── init.rs
|
||||
│ │ ├── build.rs
|
||||
│ │ └── deploy.rs
|
||||
│ ├── config.rs # Configuration management
|
||||
│ └── utils.rs # Helper functions
|
||||
├── tests/
|
||||
│ └── cli_tests.rs # Integration tests
|
||||
└── completions/ # Generated shell completions
|
||||
```
|
||||
|
||||
## Cargo.toml
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "my-tool"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = ["derive", "env", "cargo"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
colored = "2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0"
|
||||
predicates = "3.0"
|
||||
```
|
||||
|
||||
## main.rs - Complete CLI Definition
|
||||
|
||||
```rust
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod utils;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "my-tool")]
|
||||
#[command(author, version, about = "A production-ready CLI tool", long_about = None)]
|
||||
#[command(propagate_version = true)]
|
||||
struct Cli {
|
||||
/// Configuration file
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
env = "MY_TOOL_CONFIG",
|
||||
global = true,
|
||||
default_value = "config.json"
|
||||
)]
|
||||
config: PathBuf,
|
||||
|
||||
/// Enable verbose output
|
||||
#[arg(short, long, global = true)]
|
||||
verbose: bool,
|
||||
|
||||
/// Output format
|
||||
#[arg(short = 'F', long, value_enum, global = true, default_value_t = OutputFormat::Text)]
|
||||
format: OutputFormat,
|
||||
|
||||
/// Log file path
|
||||
#[arg(long, env = "MY_TOOL_LOG", global = true)]
|
||||
log_file: Option<PathBuf>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Initialize a new project
|
||||
Init {
|
||||
/// Project directory
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
|
||||
/// Project name
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
|
||||
/// Project template
|
||||
#[arg(short, long, value_enum, default_value_t = Template::Default)]
|
||||
template: Template,
|
||||
|
||||
/// Skip interactive prompts
|
||||
#[arg(short = 'y', long)]
|
||||
yes: bool,
|
||||
|
||||
/// Git repository URL
|
||||
#[arg(short, long)]
|
||||
git: Option<String>,
|
||||
},
|
||||
|
||||
/// Build the project
|
||||
Build {
|
||||
/// Build profile
|
||||
#[arg(short, long, value_enum, default_value_t = Profile::Debug)]
|
||||
profile: Profile,
|
||||
|
||||
/// Number of parallel jobs
|
||||
#[arg(short, long, value_parser = clap::value_parser!(u8).range(1..=32), default_value_t = 4)]
|
||||
jobs: u8,
|
||||
|
||||
/// Target directory
|
||||
#[arg(short, long, default_value = "target")]
|
||||
target: PathBuf,
|
||||
|
||||
/// Clean before building
|
||||
#[arg(long)]
|
||||
clean: bool,
|
||||
|
||||
/// Watch for changes
|
||||
#[arg(short, long)]
|
||||
watch: bool,
|
||||
},
|
||||
|
||||
/// Deploy to environment
|
||||
Deploy {
|
||||
/// Target environment
|
||||
#[arg(value_enum)]
|
||||
environment: Environment,
|
||||
|
||||
/// Deployment version/tag
|
||||
#[arg(short, long)]
|
||||
version: String,
|
||||
|
||||
/// Dry run (don't actually deploy)
|
||||
#[arg(short = 'n', long)]
|
||||
dry_run: bool,
|
||||
|
||||
/// Skip pre-deployment checks
|
||||
#[arg(long)]
|
||||
skip_checks: bool,
|
||||
|
||||
/// Deployment timeout in seconds
|
||||
#[arg(short, long, default_value_t = 300)]
|
||||
timeout: u64,
|
||||
|
||||
/// Rollback on failure
|
||||
#[arg(long)]
|
||||
rollback: bool,
|
||||
},
|
||||
|
||||
/// Manage configuration
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
action: ConfigAction,
|
||||
},
|
||||
|
||||
/// Generate shell completions
|
||||
Completions {
|
||||
/// Shell type
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
|
||||
/// Output directory
|
||||
#[arg(short, long, default_value = "completions")]
|
||||
output: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ConfigAction {
|
||||
/// Show current configuration
|
||||
Show,
|
||||
|
||||
/// Set a configuration value
|
||||
Set {
|
||||
/// Configuration key
|
||||
key: String,
|
||||
|
||||
/// Configuration value
|
||||
value: String,
|
||||
},
|
||||
|
||||
/// Get a configuration value
|
||||
Get {
|
||||
/// Configuration key
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Reset configuration to defaults
|
||||
Reset {
|
||||
/// Confirm reset
|
||||
#[arg(short = 'y', long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum OutputFormat {
|
||||
Text,
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Template {
|
||||
Default,
|
||||
Minimal,
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Profile {
|
||||
Debug,
|
||||
Release,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Environment {
|
||||
Dev,
|
||||
Staging,
|
||||
Prod,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Shell {
|
||||
Bash,
|
||||
Zsh,
|
||||
Fish,
|
||||
PowerShell,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize logging
|
||||
if let Some(log_file) = &cli.log_file {
|
||||
utils::init_file_logging(log_file, cli.verbose)?;
|
||||
} else {
|
||||
utils::init_console_logging(cli.verbose);
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
let config = config::load(&cli.config)?;
|
||||
|
||||
// Execute command
|
||||
match &cli.command {
|
||||
Commands::Init {
|
||||
path,
|
||||
name,
|
||||
template,
|
||||
yes,
|
||||
git,
|
||||
} => {
|
||||
commands::init::execute(path, name.as_deref(), *template, *yes, git.as_deref()).await?;
|
||||
}
|
||||
|
||||
Commands::Build {
|
||||
profile,
|
||||
jobs,
|
||||
target,
|
||||
clean,
|
||||
watch,
|
||||
} => {
|
||||
commands::build::execute(*profile, *jobs, target, *clean, *watch).await?;
|
||||
}
|
||||
|
||||
Commands::Deploy {
|
||||
environment,
|
||||
version,
|
||||
dry_run,
|
||||
skip_checks,
|
||||
timeout,
|
||||
rollback,
|
||||
} => {
|
||||
commands::deploy::execute(
|
||||
*environment,
|
||||
version,
|
||||
*dry_run,
|
||||
*skip_checks,
|
||||
*timeout,
|
||||
*rollback,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Commands::Config { action } => match action {
|
||||
ConfigAction::Show => config::show(&config, cli.format),
|
||||
ConfigAction::Set { key, value } => config::set(&cli.config, key, value)?,
|
||||
ConfigAction::Get { key } => config::get(&config, key, cli.format)?,
|
||||
ConfigAction::Reset { yes } => config::reset(&cli.config, *yes)?,
|
||||
},
|
||||
|
||||
Commands::Completions { shell, output } => {
|
||||
commands::completions::generate(*shell, output)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
### 1. Global Arguments
|
||||
|
||||
Arguments available to all subcommands:
|
||||
|
||||
```rust
|
||||
#[arg(short, long, global = true)]
|
||||
verbose: bool,
|
||||
```
|
||||
|
||||
### 2. Environment Variables
|
||||
|
||||
Fallback to environment variables:
|
||||
|
||||
```rust
|
||||
#[arg(short, long, env = "MY_TOOL_CONFIG")]
|
||||
config: PathBuf,
|
||||
```
|
||||
|
||||
### 3. Validation
|
||||
|
||||
Numeric range validation:
|
||||
|
||||
```rust
|
||||
#[arg(short, long, value_parser = clap::value_parser!(u8).range(1..=32))]
|
||||
jobs: u8,
|
||||
```
|
||||
|
||||
### 4. Type-Safe Enums
|
||||
|
||||
Constrained choices with ValueEnum:
|
||||
|
||||
```rust
|
||||
#[derive(ValueEnum)]
|
||||
enum Environment {
|
||||
Dev,
|
||||
Staging,
|
||||
Prod,
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Nested Subcommands
|
||||
|
||||
Multi-level command structure:
|
||||
|
||||
```rust
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
action: ConfigAction,
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Default Values
|
||||
|
||||
Sensible defaults for all options:
|
||||
|
||||
```rust
|
||||
#[arg(short, long, default_value = "config.json")]
|
||||
config: PathBuf,
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
`tests/cli_tests.rs`:
|
||||
|
||||
```rust
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_help() {
|
||||
let mut cmd = Command::cargo_bin("my-tool").unwrap();
|
||||
cmd.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("A production-ready CLI tool"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
let mut cmd = Command::cargo_bin("my-tool").unwrap();
|
||||
cmd.arg("--version")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("1.0.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_command() {
|
||||
let mut cmd = Command::cargo_bin("my-tool").unwrap();
|
||||
cmd.arg("init")
|
||||
.arg("--name")
|
||||
.arg("test-project")
|
||||
.arg("--yes")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
```
|
||||
|
||||
## Building for Production
|
||||
|
||||
```bash
|
||||
# Build release binary
|
||||
cargo build --release
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Generate completions
|
||||
./target/release/my-tool completions bash
|
||||
./target/release/my-tool completions zsh
|
||||
./target/release/my-tool completions fish
|
||||
|
||||
# Install locally
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
## Distribution
|
||||
|
||||
### Cross-Platform Binaries
|
||||
|
||||
Use `cross` for cross-compilation:
|
||||
|
||||
```bash
|
||||
cargo install cross
|
||||
cross build --release --target x86_64-unknown-linux-gnu
|
||||
cross build --release --target x86_64-pc-windows-gnu
|
||||
cross build --release --target x86_64-apple-darwin
|
||||
```
|
||||
|
||||
### Package for Distribution
|
||||
|
||||
```bash
|
||||
# Linux/macOS tar.gz
|
||||
tar czf my-tool-linux-x64.tar.gz -C target/release my-tool
|
||||
|
||||
# Windows zip
|
||||
zip my-tool-windows-x64.zip target/release/my-tool.exe
|
||||
```
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
- ✓ Clear, descriptive help text
|
||||
- ✓ Sensible default values
|
||||
- ✓ Environment variable support
|
||||
- ✓ Input validation
|
||||
- ✓ Type-safe options (ValueEnum)
|
||||
- ✓ Global arguments for common options
|
||||
- ✓ Proper error handling (anyhow)
|
||||
- ✓ Integration tests
|
||||
- ✓ Shell completion generation
|
||||
- ✓ Version information
|
||||
- ✓ Verbose/quiet modes
|
||||
- ✓ Configuration file support
|
||||
- ✓ Dry-run mode for destructive operations
|
||||
|
||||
## Resources
|
||||
|
||||
- Full templates: `skills/clap-patterns/templates/`
|
||||
- Validation examples: `examples/validation-examples.md`
|
||||
- Test scripts: `scripts/test-cli.sh`
|
||||
300
skills/clap-patterns/examples/validation-examples.md
Normal file
300
skills/clap-patterns/examples/validation-examples.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Clap Validation Examples
|
||||
|
||||
Comprehensive examples for validating CLI input with Clap value parsers.
|
||||
|
||||
## 1. Port Number Validation
|
||||
|
||||
Validate port numbers are in the valid range (1-65535):
|
||||
|
||||
```rust
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
const PORT_RANGE: RangeInclusive<usize> = 1..=65535;
|
||||
|
||||
fn port_in_range(s: &str) -> Result<u16, String> {
|
||||
let port: usize = s
|
||||
.parse()
|
||||
.map_err(|_| format!("`{}` isn't a valid port number", s))?;
|
||||
|
||||
if PORT_RANGE.contains(&port) {
|
||||
Ok(port as u16)
|
||||
} else {
|
||||
Err(format!(
|
||||
"port not in range {}-{}",
|
||||
PORT_RANGE.start(),
|
||||
PORT_RANGE.end()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[arg(short, long, value_parser = port_in_range)]
|
||||
port: u16,
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
$ my-cli --port 8080 # ✓ Valid
|
||||
$ my-cli --port 80000 # ❌ Error: port not in range 1-65535
|
||||
$ my-cli --port abc # ❌ Error: `abc` isn't a valid port number
|
||||
```
|
||||
|
||||
## 2. Email Validation
|
||||
|
||||
Basic email format validation:
|
||||
|
||||
```rust
|
||||
fn validate_email(s: &str) -> Result<String, String> {
|
||||
if s.contains('@') && s.contains('.') && s.len() > 5 {
|
||||
Ok(s.to_string())
|
||||
} else {
|
||||
Err(format!("`{}` is not a valid email address", s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[arg(short, long, value_parser = validate_email)]
|
||||
email: String,
|
||||
}
|
||||
```
|
||||
|
||||
## 3. File/Directory Existence
|
||||
|
||||
Validate that files or directories exist:
|
||||
|
||||
```rust
|
||||
fn file_exists(s: &str) -> Result<PathBuf, String> {
|
||||
let path = PathBuf::from(s);
|
||||
if path.exists() && path.is_file() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(format!("file does not exist: {}", s))
|
||||
}
|
||||
}
|
||||
|
||||
fn dir_exists(s: &str) -> Result<PathBuf, String> {
|
||||
let path = PathBuf::from(s);
|
||||
if path.exists() && path.is_dir() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(format!("directory does not exist: {}", s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[arg(short, long, value_parser = file_exists)]
|
||||
input: PathBuf,
|
||||
|
||||
#[arg(short, long, value_parser = dir_exists)]
|
||||
output_dir: PathBuf,
|
||||
}
|
||||
```
|
||||
|
||||
## 4. URL Validation
|
||||
|
||||
Validate URL format:
|
||||
|
||||
```rust
|
||||
fn validate_url(s: &str) -> Result<String, String> {
|
||||
if s.starts_with("http://") || s.starts_with("https://") {
|
||||
Ok(s.to_string())
|
||||
} else {
|
||||
Err(format!("`{}` must start with http:// or https://", s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[arg(short, long, value_parser = validate_url)]
|
||||
endpoint: String,
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Numeric Range Validation
|
||||
|
||||
Use built-in range validation:
|
||||
|
||||
```rust
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
/// Port (1-65535)
|
||||
#[arg(long, value_parser = clap::value_parser!(u16).range(1..=65535))]
|
||||
port: u16,
|
||||
|
||||
/// Threads (1-32)
|
||||
#[arg(long, value_parser = clap::value_parser!(u8).range(1..=32))]
|
||||
threads: u8,
|
||||
|
||||
/// Percentage (0-100)
|
||||
#[arg(long, value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||
percentage: u8,
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Regex Pattern Validation
|
||||
|
||||
Validate against regex patterns:
|
||||
|
||||
```rust
|
||||
use regex::Regex;
|
||||
|
||||
fn validate_version(s: &str) -> Result<String, String> {
|
||||
let re = Regex::new(r"^\d+\.\d+\.\d+$").unwrap();
|
||||
if re.is_match(s) {
|
||||
Ok(s.to_string())
|
||||
} else {
|
||||
Err(format!("`{}` is not a valid semantic version (e.g., 1.2.3)", s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[arg(short, long, value_parser = validate_version)]
|
||||
version: String,
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Add `regex = "1"` to `Cargo.toml` for this example.
|
||||
|
||||
## 7. Multiple Validation Rules
|
||||
|
||||
Combine multiple validation rules:
|
||||
|
||||
```rust
|
||||
fn validate_username(s: &str) -> Result<String, String> {
|
||||
// Must be 3-20 characters
|
||||
if s.len() < 3 || s.len() > 20 {
|
||||
return Err("username must be 3-20 characters".to_string());
|
||||
}
|
||||
|
||||
// Must start with letter
|
||||
if !s.chars().next().unwrap().is_alphabetic() {
|
||||
return Err("username must start with a letter".to_string());
|
||||
}
|
||||
|
||||
// Only alphanumeric and underscore
|
||||
if !s.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
||||
return Err("username can only contain letters, numbers, and underscores".to_string());
|
||||
}
|
||||
|
||||
Ok(s.to_string())
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[arg(short, long, value_parser = validate_username)]
|
||||
username: String,
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Conditional Validation
|
||||
|
||||
Validate based on other arguments:
|
||||
|
||||
```rust
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
/// Enable SSL
|
||||
#[arg(long)]
|
||||
ssl: bool,
|
||||
|
||||
/// SSL certificate (required if --ssl is set)
|
||||
#[arg(long, required_if_eq("ssl", "true"))]
|
||||
cert: Option<PathBuf>,
|
||||
|
||||
/// SSL key (required if --ssl is set)
|
||||
#[arg(long, required_if_eq("ssl", "true"))]
|
||||
key: Option<PathBuf>,
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Mutually Exclusive Arguments
|
||||
|
||||
Ensure only one option is provided:
|
||||
|
||||
```rust
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
/// Use JSON format
|
||||
#[arg(long, conflicts_with = "yaml")]
|
||||
json: bool,
|
||||
|
||||
/// Use YAML format
|
||||
#[arg(long, conflicts_with = "json")]
|
||||
yaml: bool,
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Custom Type with FromStr
|
||||
|
||||
Implement `FromStr` for automatic parsing:
|
||||
|
||||
```rust
|
||||
use std::str::FromStr;
|
||||
|
||||
struct IpPort {
|
||||
ip: std::net::IpAddr,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl FromStr for IpPort {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let parts: Vec<&str> = s.split(':').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("format must be IP:PORT (e.g., 127.0.0.1:8080)".to_string());
|
||||
}
|
||||
|
||||
let ip = parts[0]
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid IP address: {}", parts[0]))?;
|
||||
|
||||
let port = parts[1]
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid port: {}", parts[1]))?;
|
||||
|
||||
Ok(IpPort { ip, port })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
/// Bind address (IP:PORT)
|
||||
#[arg(short, long)]
|
||||
bind: IpPort,
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
$ my-cli --bind 127.0.0.1:8080 # ✓ Valid
|
||||
$ my-cli --bind 192.168.1.1:3000 # ✓ Valid
|
||||
$ my-cli --bind invalid # ❌ Error
|
||||
```
|
||||
|
||||
## Testing Validation
|
||||
|
||||
Use the provided test script:
|
||||
|
||||
```bash
|
||||
bash scripts/test-cli.sh ./target/debug/my-cli validation
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Provide Clear Error Messages**: Tell users what went wrong and how to fix it
|
||||
2. **Validate Early**: Use value parsers instead of validating after parsing
|
||||
3. **Use Type System**: Leverage Rust's type system for compile-time safety
|
||||
4. **Document Constraints**: Add constraints to help text
|
||||
5. **Test Edge Cases**: Test boundary values and invalid inputs
|
||||
|
||||
## Resources
|
||||
|
||||
- Value parser template: `templates/value-parser.rs`
|
||||
- Test script: `scripts/test-cli.sh`
|
||||
- Clap docs: https://docs.rs/clap/latest/clap/
|
||||
Reference in New Issue
Block a user