Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:04:14 +08:00
commit 70c36b5eff
248 changed files with 47482 additions and 0 deletions

View 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/

View 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`

View 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/