Initial commit
This commit is contained in:
249
skills/clap-patterns/SKILL.md
Normal file
249
skills/clap-patterns/SKILL.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
name: clap-patterns
|
||||
description: Modern type-safe Rust CLI patterns with Clap derive macros, Parser trait, Subcommand enums, validation, and value parsers. Use when building CLI applications, creating Clap commands, implementing type-safe Rust CLIs, or when user mentions Clap, CLI patterns, Rust command-line, derive macros, Parser trait, Subcommands, or command-line interfaces.
|
||||
allowed-tools: Read, Write, Edit, Bash
|
||||
---
|
||||
|
||||
# clap-patterns
|
||||
|
||||
Provides modern type-safe Rust CLI patterns using Clap 4.x with derive macros, Parser trait, Subcommand enums, custom validation, value parsers, and environment variable integration for building maintainable command-line applications.
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### 1. Basic Parser with Derive Macros
|
||||
|
||||
Use derive macros for automatic CLI parsing with type safety:
|
||||
|
||||
```rust
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "myapp")]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
/// Input file path
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
input: std::path::PathBuf,
|
||||
|
||||
/// Optional output file
|
||||
#[arg(short, long)]
|
||||
output: Option<std::path::PathBuf>,
|
||||
|
||||
/// Verbose mode
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
|
||||
/// Number of items to process
|
||||
#[arg(short, long, default_value_t = 10)]
|
||||
count: usize,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
if cli.verbose {
|
||||
println!("Processing: {:?}", cli.input);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Subcommand Enums
|
||||
|
||||
Organize complex CLIs with nested subcommands:
|
||||
|
||||
```rust
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "git")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Add files to staging
|
||||
Add {
|
||||
/// Files to add
|
||||
#[arg(value_name = "FILE")]
|
||||
files: Vec<String>,
|
||||
},
|
||||
/// Commit changes
|
||||
Commit {
|
||||
/// Commit message
|
||||
#[arg(short, long)]
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Value Parsers and Validation
|
||||
|
||||
Implement custom parsing and validation:
|
||||
|
||||
```rust
|
||||
use clap::Parser;
|
||||
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!("`{s}` isn't a valid port number"))?;
|
||||
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 {
|
||||
/// Port to listen on
|
||||
#[arg(short, long, value_parser = port_in_range)]
|
||||
port: u16,
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Environment Variable Integration
|
||||
|
||||
Support environment variables with fallback:
|
||||
|
||||
```rust
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
/// API key (or set API_KEY env var)
|
||||
#[arg(long, env = "API_KEY")]
|
||||
api_key: String,
|
||||
|
||||
/// Database URL
|
||||
#[arg(long, env = "DATABASE_URL")]
|
||||
database_url: String,
|
||||
|
||||
/// Optional log level
|
||||
#[arg(long, env = "LOG_LEVEL", default_value = "info")]
|
||||
log_level: String,
|
||||
}
|
||||
```
|
||||
|
||||
### 5. ValueEnum for Constrained Choices
|
||||
|
||||
Use ValueEnum for type-safe option selection:
|
||||
|
||||
```rust
|
||||
use clap::{Parser, ValueEnum};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Format {
|
||||
Json,
|
||||
Yaml,
|
||||
Toml,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
/// Output format
|
||||
#[arg(value_enum, short, long, default_value_t = Format::Json)]
|
||||
format: Format,
|
||||
}
|
||||
```
|
||||
|
||||
## Available Templates
|
||||
|
||||
The following Rust templates demonstrate Clap patterns:
|
||||
|
||||
- **basic-parser.rs**: Simple CLI with Parser derive macro
|
||||
- **subcommands.rs**: Multi-level subcommand structure
|
||||
- **value-parser.rs**: Custom validation with value parsers
|
||||
- **env-variables.rs**: Environment variable integration
|
||||
- **value-enum.rs**: Type-safe enums for options
|
||||
- **builder-pattern.rs**: Manual builder API (for complex cases)
|
||||
- **full-featured-cli.rs**: Complete CLI with all patterns
|
||||
|
||||
## Available Scripts
|
||||
|
||||
Helper scripts for Clap development:
|
||||
|
||||
- **generate-completions.sh**: Generate shell completions (bash, zsh, fish)
|
||||
- **validate-cargo.sh**: Check Cargo.toml for correct Clap dependencies
|
||||
- **test-cli.sh**: Test CLI with various argument combinations
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
1. **Choose the appropriate template** based on your CLI complexity:
|
||||
- Simple single-command → `basic-parser.rs`
|
||||
- Multiple subcommands → `subcommands.rs`
|
||||
- Need validation → `value-parser.rs`
|
||||
- Environment config → `env-variables.rs`
|
||||
|
||||
2. **Add Clap to Cargo.toml**:
|
||||
```toml
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
```
|
||||
|
||||
3. **Implement your CLI** using the selected template as a starting point
|
||||
|
||||
4. **Generate completions** using the provided script for better UX
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Use derive macros for most cases (cleaner, less boilerplate)
|
||||
- Add help text with doc comments (shows in `--help`)
|
||||
- Validate early with value parsers
|
||||
- Use ValueEnum for constrained choices
|
||||
- Support environment variables for sensitive data
|
||||
- Provide sensible defaults with `default_value_t`
|
||||
- Use PathBuf for file/directory arguments
|
||||
- Add version and author metadata
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Multiple Values
|
||||
```rust
|
||||
#[arg(short, long, num_args = 1..)]
|
||||
files: Vec<PathBuf>,
|
||||
```
|
||||
|
||||
### Required Unless Present
|
||||
```rust
|
||||
#[arg(long, required_unless_present = "config")]
|
||||
database_url: Option<String>,
|
||||
```
|
||||
|
||||
### Conflicting Arguments
|
||||
```rust
|
||||
#[arg(long, conflicts_with = "json")]
|
||||
yaml: bool,
|
||||
```
|
||||
|
||||
### Global Arguments (for subcommands)
|
||||
```rust
|
||||
#[arg(global = true, short, long)]
|
||||
verbose: bool,
|
||||
```
|
||||
|
||||
## Testing Your CLI
|
||||
|
||||
Run the test script to validate your CLI:
|
||||
|
||||
```bash
|
||||
bash scripts/test-cli.sh your-binary
|
||||
```
|
||||
|
||||
This tests:
|
||||
- Help output (`--help`)
|
||||
- Version flag (`--version`)
|
||||
- Invalid arguments
|
||||
- Subcommand routing
|
||||
- Environment variable precedence
|
||||
|
||||
## References
|
||||
|
||||
- Templates: `skills/clap-patterns/templates/`
|
||||
- Scripts: `skills/clap-patterns/scripts/`
|
||||
- Examples: `skills/clap-patterns/examples/`
|
||||
- Clap Documentation: https://docs.rs/clap/latest/clap/
|
||||
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/
|
||||
94
skills/clap-patterns/scripts/generate-completions.sh
Executable file
94
skills/clap-patterns/scripts/generate-completions.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate shell completions for Clap CLI applications
|
||||
#
|
||||
# Usage: ./generate-completions.sh <binary-name> [output-dir]
|
||||
#
|
||||
# This script generates shell completion scripts for bash, zsh, fish, and powershell.
|
||||
# The CLI binary must support the --generate-completions flag (built with Clap).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BINARY="${1:-}"
|
||||
OUTPUT_DIR="${2:-completions}"
|
||||
|
||||
if [ -z "$BINARY" ]; then
|
||||
echo "Error: Binary name required"
|
||||
echo "Usage: $0 <binary-name> [output-dir]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "Generating shell completions for: $BINARY"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Check if binary exists
|
||||
if ! command -v "$BINARY" &> /dev/null; then
|
||||
echo "Warning: Binary '$BINARY' not found in PATH"
|
||||
echo "Make sure to build and install it first: cargo install --path ."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate completions for each shell
|
||||
for shell in bash zsh fish powershell elvish; do
|
||||
echo "Generating $shell completions..."
|
||||
|
||||
case "$shell" in
|
||||
bash)
|
||||
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/${BINARY}.bash" 2>/dev/null || {
|
||||
echo " ⚠️ Failed (CLI may not support --generate-completion)"
|
||||
continue
|
||||
}
|
||||
echo " ✓ Generated: $OUTPUT_DIR/${BINARY}.bash"
|
||||
;;
|
||||
zsh)
|
||||
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/_${BINARY}" 2>/dev/null || {
|
||||
echo " ⚠️ Failed"
|
||||
continue
|
||||
}
|
||||
echo " ✓ Generated: $OUTPUT_DIR/_${BINARY}"
|
||||
;;
|
||||
fish)
|
||||
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/${BINARY}.fish" 2>/dev/null || {
|
||||
echo " ⚠️ Failed"
|
||||
continue
|
||||
}
|
||||
echo " ✓ Generated: $OUTPUT_DIR/${BINARY}.fish"
|
||||
;;
|
||||
powershell)
|
||||
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/_${BINARY}.ps1" 2>/dev/null || {
|
||||
echo " ⚠️ Failed"
|
||||
continue
|
||||
}
|
||||
echo " ✓ Generated: $OUTPUT_DIR/_${BINARY}.ps1"
|
||||
;;
|
||||
elvish)
|
||||
"$BINARY" --generate-completion "$shell" > "$OUTPUT_DIR/${BINARY}.elv" 2>/dev/null || {
|
||||
echo " ⚠️ Failed"
|
||||
continue
|
||||
}
|
||||
echo " ✓ Generated: $OUTPUT_DIR/${BINARY}.elv"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✓ Completion generation complete!"
|
||||
echo ""
|
||||
echo "Installation instructions:"
|
||||
echo ""
|
||||
echo "Bash:"
|
||||
echo " sudo cp $OUTPUT_DIR/${BINARY}.bash /etc/bash_completion.d/"
|
||||
echo " Or: echo 'source $PWD/$OUTPUT_DIR/${BINARY}.bash' >> ~/.bashrc"
|
||||
echo ""
|
||||
echo "Zsh:"
|
||||
echo " cp $OUTPUT_DIR/_${BINARY} /usr/local/share/zsh/site-functions/"
|
||||
echo " Or add to fpath: fpath=($PWD/$OUTPUT_DIR \$fpath)"
|
||||
echo ""
|
||||
echo "Fish:"
|
||||
echo " cp $OUTPUT_DIR/${BINARY}.fish ~/.config/fish/completions/"
|
||||
echo ""
|
||||
echo "PowerShell:"
|
||||
echo " Add to profile: . $PWD/$OUTPUT_DIR/_${BINARY}.ps1"
|
||||
159
skills/clap-patterns/scripts/test-cli.sh
Executable file
159
skills/clap-patterns/scripts/test-cli.sh
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test a Clap CLI application with various argument combinations
|
||||
#
|
||||
# Usage: ./test-cli.sh <binary-path> [test-suite]
|
||||
#
|
||||
# Test suites: basic, subcommands, validation, env, all (default)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BINARY="${1:-}"
|
||||
TEST_SUITE="${2:-all}"
|
||||
|
||||
if [ -z "$BINARY" ]; then
|
||||
echo "Error: Binary path required"
|
||||
echo "Usage: $0 <binary-path> [test-suite]"
|
||||
echo ""
|
||||
echo "Test suites:"
|
||||
echo " basic - Test help, version, basic flags"
|
||||
echo " subcommands - Test subcommand routing"
|
||||
echo " validation - Test input validation"
|
||||
echo " env - Test environment variables"
|
||||
echo " all - Run all tests (default)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$BINARY" ]; then
|
||||
echo "Error: Binary not found or not executable: $BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
run_test() {
|
||||
local name="$1"
|
||||
local expected_exit="$2"
|
||||
shift 2
|
||||
|
||||
echo -n "Testing: $name ... "
|
||||
|
||||
if "$BINARY" "$@" &>/dev/null; then
|
||||
actual_exit=0
|
||||
else
|
||||
actual_exit=$?
|
||||
fi
|
||||
|
||||
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
||||
echo "✓ PASS"
|
||||
((PASS++))
|
||||
else
|
||||
echo "❌ FAIL (expected exit $expected_exit, got $actual_exit)"
|
||||
((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
test_basic() {
|
||||
echo ""
|
||||
echo "=== Basic Tests ==="
|
||||
|
||||
run_test "Help output" 0 --help
|
||||
run_test "Version output" 0 --version
|
||||
run_test "Short help" 0 -h
|
||||
run_test "Invalid flag" 1 --invalid-flag
|
||||
run_test "No arguments (might fail for some CLIs)" 0
|
||||
}
|
||||
|
||||
test_subcommands() {
|
||||
echo ""
|
||||
echo "=== Subcommand Tests ==="
|
||||
|
||||
run_test "Subcommand help" 0 help
|
||||
run_test "Invalid subcommand" 1 invalid-command
|
||||
|
||||
# Try common subcommands
|
||||
for cmd in init add build test deploy; do
|
||||
if "$BINARY" help 2>&1 | grep -q "$cmd"; then
|
||||
run_test "Subcommand '$cmd' help" 0 "$cmd" --help
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
test_validation() {
|
||||
echo ""
|
||||
echo "=== Validation Tests ==="
|
||||
|
||||
# Test file arguments with non-existent files
|
||||
run_test "Non-existent file" 1 --input /nonexistent/file.txt
|
||||
|
||||
# Test numeric ranges
|
||||
run_test "Invalid number" 1 --count abc
|
||||
run_test "Negative number" 1 --count -5
|
||||
|
||||
# Test conflicting flags
|
||||
if "$BINARY" --help 2>&1 | grep -q "conflicts with"; then
|
||||
echo " (Found conflicting arguments in help text)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_env() {
|
||||
echo ""
|
||||
echo "=== Environment Variable Tests ==="
|
||||
|
||||
# Check if binary supports environment variables
|
||||
if "$BINARY" --help 2>&1 | grep -q "\[env:"; then
|
||||
echo "✓ Environment variable support detected"
|
||||
|
||||
# Extract env vars from help text
|
||||
ENV_VARS=$("$BINARY" --help 2>&1 | grep -o '\[env: [A-Z_]*\]' | sed 's/\[env: \(.*\)\]/\1/' || true)
|
||||
|
||||
if [ -n "$ENV_VARS" ]; then
|
||||
echo "Found environment variables:"
|
||||
echo "$ENV_VARS" | while read -r var; do
|
||||
echo " - $var"
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo " No environment variable support detected"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run requested test suite
|
||||
case "$TEST_SUITE" in
|
||||
basic)
|
||||
test_basic
|
||||
;;
|
||||
subcommands)
|
||||
test_subcommands
|
||||
;;
|
||||
validation)
|
||||
test_validation
|
||||
;;
|
||||
env)
|
||||
test_env
|
||||
;;
|
||||
all)
|
||||
test_basic
|
||||
test_subcommands
|
||||
test_validation
|
||||
test_env
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown test suite: $TEST_SUITE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "=== Test Summary ==="
|
||||
echo "Passed: $PASS"
|
||||
echo "Failed: $FAIL"
|
||||
echo ""
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo "❌ Some tests failed"
|
||||
exit 1
|
||||
else
|
||||
echo "✓ All tests passed!"
|
||||
exit 0
|
||||
fi
|
||||
113
skills/clap-patterns/scripts/validate-cargo.sh
Executable file
113
skills/clap-patterns/scripts/validate-cargo.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# Validate Cargo.toml for correct Clap configuration
|
||||
#
|
||||
# Usage: ./validate-cargo.sh [path-to-Cargo.toml]
|
||||
#
|
||||
# Checks:
|
||||
# - Clap dependency exists
|
||||
# - Clap version is 4.x or newer
|
||||
# - Required features are enabled (derive)
|
||||
# - Optional features (env, cargo) are present if needed
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CARGO_TOML="${1:-Cargo.toml}"
|
||||
|
||||
if [ ! -f "$CARGO_TOML" ]; then
|
||||
echo "❌ Error: $CARGO_TOML not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validating Clap configuration in: $CARGO_TOML"
|
||||
echo ""
|
||||
|
||||
# Check if clap is listed as a dependency
|
||||
if ! grep -q "clap" "$CARGO_TOML"; then
|
||||
echo "❌ Clap not found in dependencies"
|
||||
echo ""
|
||||
echo "Add to $CARGO_TOML:"
|
||||
echo ""
|
||||
echo '[dependencies]'
|
||||
echo 'clap = { version = "4.5", features = ["derive"] }'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Clap dependency found"
|
||||
|
||||
# Extract clap version
|
||||
VERSION=$(grep -A 5 '^\[dependencies\]' "$CARGO_TOML" | grep 'clap' | head -1)
|
||||
|
||||
# Check version
|
||||
if echo "$VERSION" | grep -q '"4\.' || echo "$VERSION" | grep -q "'4\."; then
|
||||
echo "✓ Clap version 4.x detected"
|
||||
elif echo "$VERSION" | grep -q '"3\.' || echo "$VERSION" | grep -q "'3\."; then
|
||||
echo "⚠️ Warning: Clap version 3.x detected"
|
||||
echo " Consider upgrading to 4.x for latest features"
|
||||
else
|
||||
echo "⚠️ Warning: Could not determine Clap version"
|
||||
fi
|
||||
|
||||
# Check for derive feature
|
||||
if echo "$VERSION" | grep -q 'features.*derive' || echo "$VERSION" | grep -q 'derive.*features'; then
|
||||
echo "✓ 'derive' feature enabled"
|
||||
else
|
||||
echo "❌ 'derive' feature not found"
|
||||
echo " Add: features = [\"derive\"]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for optional but recommended features
|
||||
echo ""
|
||||
echo "Optional features:"
|
||||
|
||||
if echo "$VERSION" | grep -q '"env"' || echo "$VERSION" | grep -q "'env'"; then
|
||||
echo "✓ 'env' feature enabled (environment variable support)"
|
||||
else
|
||||
echo " 'env' feature not enabled"
|
||||
echo " Add for environment variable support: features = [\"derive\", \"env\"]"
|
||||
fi
|
||||
|
||||
if echo "$VERSION" | grep -q '"cargo"' || echo "$VERSION" | grep -q "'cargo'"; then
|
||||
echo "✓ 'cargo' feature enabled (automatic version from Cargo.toml)"
|
||||
else
|
||||
echo " 'cargo' feature not enabled"
|
||||
echo " Add for automatic version: features = [\"derive\", \"cargo\"]"
|
||||
fi
|
||||
|
||||
if echo "$VERSION" | grep -q '"color"' || echo "$VERSION" | grep -q "'color'"; then
|
||||
echo "✓ 'color' feature enabled (colored output)"
|
||||
else
|
||||
echo " 'color' feature not enabled"
|
||||
echo " Add for colored help: features = [\"derive\", \"color\"]"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check for common patterns in src/
|
||||
if [ -d "src" ]; then
|
||||
echo "Checking source files for Clap usage patterns..."
|
||||
|
||||
if grep -r "use clap::Parser" src/ &>/dev/null; then
|
||||
echo "✓ Parser trait usage found"
|
||||
fi
|
||||
|
||||
if grep -r "use clap::Subcommand" src/ &>/dev/null; then
|
||||
echo "✓ Subcommand trait usage found"
|
||||
fi
|
||||
|
||||
if grep -r "use clap::ValueEnum" src/ &>/dev/null; then
|
||||
echo "✓ ValueEnum trait usage found"
|
||||
fi
|
||||
|
||||
if grep -r "#\[derive(Parser)\]" src/ &>/dev/null; then
|
||||
echo "✓ Parser derive macro usage found"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Validation complete!"
|
||||
echo ""
|
||||
echo "Recommended Cargo.toml configuration:"
|
||||
echo ""
|
||||
echo '[dependencies]'
|
||||
echo 'clap = { version = "4.5", features = ["derive", "env", "cargo"] }'
|
||||
66
skills/clap-patterns/templates/basic-parser.rs
Normal file
66
skills/clap-patterns/templates/basic-parser.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
/// Basic Parser Template with Clap Derive Macros
|
||||
///
|
||||
/// This template demonstrates:
|
||||
/// - Parser derive macro
|
||||
/// - Argument attributes (short, long, default_value)
|
||||
/// - PathBuf for file handling
|
||||
/// - Boolean flags
|
||||
/// - Doc comments as help text
|
||||
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "myapp")]
|
||||
#[command(author = "Your Name <you@example.com>")]
|
||||
#[command(version = "1.0.0")]
|
||||
#[command(about = "A simple CLI application", long_about = None)]
|
||||
struct Cli {
|
||||
/// Input file to process
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
input: PathBuf,
|
||||
|
||||
/// Optional output file
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// Enable verbose output
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
|
||||
/// Number of items to process
|
||||
#[arg(short = 'c', long, default_value_t = 10)]
|
||||
count: usize,
|
||||
|
||||
/// Dry run mode (don't make changes)
|
||||
#[arg(short = 'n', long)]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.verbose {
|
||||
println!("Input file: {:?}", cli.input);
|
||||
println!("Output file: {:?}", cli.output);
|
||||
println!("Count: {}", cli.count);
|
||||
println!("Dry run: {}", cli.dry_run);
|
||||
}
|
||||
|
||||
// Check if input file exists
|
||||
if !cli.input.exists() {
|
||||
eprintln!("Error: Input file does not exist: {:?}", cli.input);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Your processing logic here
|
||||
println!("Processing {} with count {}...", cli.input.display(), cli.count);
|
||||
|
||||
if let Some(output) = cli.output {
|
||||
if !cli.dry_run {
|
||||
println!("Would write to: {}", output.display());
|
||||
} else {
|
||||
println!("Dry run: Skipping write to {}", output.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
112
skills/clap-patterns/templates/builder-pattern.rs
Normal file
112
skills/clap-patterns/templates/builder-pattern.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
/// Builder Pattern Template (Manual API)
|
||||
///
|
||||
/// This template demonstrates the builder API for advanced use cases:
|
||||
/// - Dynamic CLI construction
|
||||
/// - Runtime configuration
|
||||
/// - Custom help templates
|
||||
/// - Complex validation logic
|
||||
///
|
||||
/// Note: Prefer derive macros unless you need this level of control.
|
||||
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn build_cli() -> Command {
|
||||
Command::new("advanced-cli")
|
||||
.version("1.0.0")
|
||||
.author("Your Name <you@example.com>")
|
||||
.about("Advanced CLI using builder pattern")
|
||||
.arg(
|
||||
Arg::new("input")
|
||||
.short('i')
|
||||
.long("input")
|
||||
.value_name("FILE")
|
||||
.help("Input file to process")
|
||||
.required(true)
|
||||
.value_parser(clap::value_parser!(PathBuf)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("output")
|
||||
.short('o')
|
||||
.long("output")
|
||||
.value_name("FILE")
|
||||
.help("Output file (optional)")
|
||||
.value_parser(clap::value_parser!(PathBuf)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("verbose")
|
||||
.short('v')
|
||||
.long("verbose")
|
||||
.help("Enable verbose output")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("count")
|
||||
.short('c')
|
||||
.long("count")
|
||||
.value_name("NUM")
|
||||
.help("Number of items to process")
|
||||
.default_value("10")
|
||||
.value_parser(clap::value_parser!(usize)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("format")
|
||||
.short('f')
|
||||
.long("format")
|
||||
.value_name("FORMAT")
|
||||
.help("Output format")
|
||||
.value_parser(["json", "yaml", "toml"])
|
||||
.default_value("json"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("tags")
|
||||
.short('t')
|
||||
.long("tag")
|
||||
.value_name("TAG")
|
||||
.help("Tags to apply (can be specified multiple times)")
|
||||
.action(ArgAction::Append),
|
||||
)
|
||||
}
|
||||
|
||||
fn process_args(matches: &ArgMatches) {
|
||||
let input = matches.get_one::<PathBuf>("input").unwrap();
|
||||
let output = matches.get_one::<PathBuf>("output");
|
||||
let verbose = matches.get_flag("verbose");
|
||||
let count = *matches.get_one::<usize>("count").unwrap();
|
||||
let format = matches.get_one::<String>("format").unwrap();
|
||||
let tags: Vec<_> = matches
|
||||
.get_many::<String>("tags")
|
||||
.unwrap_or_default()
|
||||
.map(|s| s.as_str())
|
||||
.collect();
|
||||
|
||||
if verbose {
|
||||
println!("Configuration:");
|
||||
println!(" Input: {:?}", input);
|
||||
println!(" Output: {:?}", output);
|
||||
println!(" Count: {}", count);
|
||||
println!(" Format: {}", format);
|
||||
println!(" Tags: {:?}", tags);
|
||||
}
|
||||
|
||||
// Your processing logic here
|
||||
println!("Processing {} items from {}", count, input.display());
|
||||
|
||||
if !tags.is_empty() {
|
||||
println!("Applying tags: {}", tags.join(", "));
|
||||
}
|
||||
|
||||
if let Some(output_path) = output {
|
||||
println!("Writing {} format to {}", format, output_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = build_cli().get_matches();
|
||||
process_args(&matches);
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
//
|
||||
// cargo run -- -i input.txt -o output.json -v -c 20 -f yaml -t alpha -t beta
|
||||
// cargo run -- --input data.txt --format toml --tag important
|
||||
99
skills/clap-patterns/templates/env-variables.rs
Normal file
99
skills/clap-patterns/templates/env-variables.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
/// Environment Variable Integration Template
|
||||
///
|
||||
/// This template demonstrates:
|
||||
/// - Reading from environment variables
|
||||
/// - Fallback to CLI arguments
|
||||
/// - Default values
|
||||
/// - Sensitive data handling (API keys, tokens)
|
||||
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "envapp")]
|
||||
#[command(about = "CLI with environment variable support")]
|
||||
struct Cli {
|
||||
/// API key (or set API_KEY env var)
|
||||
///
|
||||
/// Sensitive data like API keys should preferably be set via environment
|
||||
/// variables to avoid exposing them in shell history or process lists.
|
||||
#[arg(long, env = "API_KEY", hide_env_values = true)]
|
||||
api_key: String,
|
||||
|
||||
/// Database URL (or set DATABASE_URL env var)
|
||||
#[arg(long, env = "DATABASE_URL")]
|
||||
database_url: String,
|
||||
|
||||
/// Log level: debug, info, warn, error
|
||||
///
|
||||
/// Defaults to "info" if not provided via CLI or LOG_LEVEL env var.
|
||||
#[arg(long, env = "LOG_LEVEL", default_value = "info")]
|
||||
log_level: String,
|
||||
|
||||
/// Configuration file path
|
||||
///
|
||||
/// Reads from CONFIG_FILE env var, or uses default if not specified.
|
||||
#[arg(long, env = "CONFIG_FILE", default_value = "config.toml")]
|
||||
config: PathBuf,
|
||||
|
||||
/// Number of workers (default from env or 4)
|
||||
#[arg(long, env = "WORKER_COUNT", default_value_t = 4)]
|
||||
workers: usize,
|
||||
|
||||
/// Enable debug mode
|
||||
///
|
||||
/// Can be set via DEBUG=1 or --debug flag
|
||||
#[arg(long, env = "DEBUG", value_parser = clap::value_parser!(bool))]
|
||||
debug: bool,
|
||||
|
||||
/// Host to bind to
|
||||
#[arg(long, env = "HOST", default_value = "127.0.0.1")]
|
||||
host: String,
|
||||
|
||||
/// Port to listen on
|
||||
#[arg(short, long, env = "PORT", default_value_t = 8080)]
|
||||
port: u16,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
println!("Configuration loaded:");
|
||||
println!(" Database URL: {}", cli.database_url);
|
||||
println!(" API Key: {}...", &cli.api_key[..4.min(cli.api_key.len())]);
|
||||
println!(" Log level: {}", cli.log_level);
|
||||
println!(" Config file: {}", cli.config.display());
|
||||
println!(" Workers: {}", cli.workers);
|
||||
println!(" Debug mode: {}", cli.debug);
|
||||
println!(" Host: {}", cli.host);
|
||||
println!(" Port: {}", cli.port);
|
||||
|
||||
// Initialize logging based on log_level
|
||||
match cli.log_level.to_lowercase().as_str() {
|
||||
"debug" => println!("Log level set to DEBUG"),
|
||||
"info" => println!("Log level set to INFO"),
|
||||
"warn" => println!("Log level set to WARN"),
|
||||
"error" => println!("Log level set to ERROR"),
|
||||
_ => println!("Unknown log level: {}", cli.log_level),
|
||||
}
|
||||
|
||||
// Your application logic here
|
||||
println!("\nStarting application...");
|
||||
println!("Listening on {}:{}", cli.host, cli.port);
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
//
|
||||
// 1. Set environment variables:
|
||||
// export API_KEY="sk-1234567890abcdef"
|
||||
// export DATABASE_URL="postgres://localhost/mydb"
|
||||
// export LOG_LEVEL="debug"
|
||||
// export WORKER_COUNT="8"
|
||||
// cargo run
|
||||
//
|
||||
// 2. Override with CLI arguments:
|
||||
// cargo run -- --api-key "other-key" --workers 16
|
||||
//
|
||||
// 3. Mix environment and CLI:
|
||||
// export DATABASE_URL="postgres://localhost/mydb"
|
||||
// cargo run -- --api-key "sk-1234" --debug
|
||||
290
skills/clap-patterns/templates/full-featured-cli.rs
Normal file
290
skills/clap-patterns/templates/full-featured-cli.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
/// Full-Featured CLI Template
|
||||
///
|
||||
/// This template combines all patterns:
|
||||
/// - Parser derive with subcommands
|
||||
/// - ValueEnum for type-safe options
|
||||
/// - Environment variable support
|
||||
/// - Custom value parsers
|
||||
/// - Global arguments
|
||||
/// - Comprehensive help text
|
||||
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "myapp")]
|
||||
#[command(author = "Your Name <you@example.com>")]
|
||||
#[command(version = "1.0.0")]
|
||||
#[command(about = "A full-featured CLI application", long_about = None)]
|
||||
#[command(propagate_version = true)]
|
||||
struct Cli {
|
||||
/// Configuration file path
|
||||
#[arg(short, long, env = "CONFIG_FILE", global = true)]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Enable verbose output
|
||||
#[arg(short, long, global = true)]
|
||||
verbose: bool,
|
||||
|
||||
/// Output format
|
||||
#[arg(short, long, value_enum, global = true, default_value_t = Format::Text)]
|
||||
format: Format,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Initialize a new project
|
||||
Init {
|
||||
/// Project directory
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
|
||||
/// Project template
|
||||
#[arg(short, long, value_enum, default_value_t = Template::Basic)]
|
||||
template: Template,
|
||||
|
||||
/// Skip interactive prompts
|
||||
#[arg(short = 'y', long)]
|
||||
yes: bool,
|
||||
},
|
||||
|
||||
/// Build the project
|
||||
Build {
|
||||
/// Build mode
|
||||
#[arg(short, long, value_enum, default_value_t = BuildMode::Debug)]
|
||||
mode: BuildMode,
|
||||
|
||||
/// 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_dir: PathBuf,
|
||||
|
||||
/// Clean before building
|
||||
#[arg(long)]
|
||||
clean: bool,
|
||||
},
|
||||
|
||||
/// Test the project
|
||||
Test {
|
||||
/// Test name pattern
|
||||
pattern: Option<String>,
|
||||
|
||||
/// Run ignored tests
|
||||
#[arg(long)]
|
||||
ignored: bool,
|
||||
|
||||
/// Number of test threads
|
||||
#[arg(long, value_parser = clap::value_parser!(usize).range(1..))]
|
||||
test_threads: Option<usize>,
|
||||
|
||||
/// Show output for passing tests
|
||||
#[arg(long)]
|
||||
nocapture: bool,
|
||||
},
|
||||
|
||||
/// Deploy the project
|
||||
Deploy {
|
||||
/// Deployment environment
|
||||
#[arg(value_enum)]
|
||||
environment: Environment,
|
||||
|
||||
/// Skip pre-deployment checks
|
||||
#[arg(long)]
|
||||
skip_checks: bool,
|
||||
|
||||
/// Deployment tag/version
|
||||
#[arg(short, long)]
|
||||
tag: Option<String>,
|
||||
|
||||
/// Deployment configuration
|
||||
#[command(subcommand)]
|
||||
config: Option<DeployConfig>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum DeployConfig {
|
||||
/// Configure database settings
|
||||
Database {
|
||||
/// Database URL
|
||||
#[arg(long, env = "DATABASE_URL")]
|
||||
url: String,
|
||||
|
||||
/// Run migrations
|
||||
#[arg(long)]
|
||||
migrate: bool,
|
||||
},
|
||||
|
||||
/// Configure server settings
|
||||
Server {
|
||||
/// Server host
|
||||
#[arg(long, default_value = "0.0.0.0")]
|
||||
host: String,
|
||||
|
||||
/// Server port
|
||||
#[arg(long, default_value_t = 8080, value_parser = port_in_range)]
|
||||
port: u16,
|
||||
|
||||
/// Number of workers
|
||||
#[arg(long, default_value_t = 4)]
|
||||
workers: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Format {
|
||||
/// Human-readable text
|
||||
Text,
|
||||
/// JSON output
|
||||
Json,
|
||||
/// YAML output
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Template {
|
||||
/// Basic template
|
||||
Basic,
|
||||
/// Full-featured template
|
||||
Full,
|
||||
/// Minimal template
|
||||
Minimal,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum BuildMode {
|
||||
/// Debug build with symbols
|
||||
Debug,
|
||||
/// Release build with optimizations
|
||||
Release,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Environment {
|
||||
/// Development environment
|
||||
Dev,
|
||||
/// Staging environment
|
||||
Staging,
|
||||
/// Production environment
|
||||
Prod,
|
||||
}
|
||||
|
||||
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()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.verbose {
|
||||
println!("Verbose mode enabled");
|
||||
if let Some(config) = &cli.config {
|
||||
println!("Using config: {}", config.display());
|
||||
}
|
||||
println!("Output format: {:?}", cli.format);
|
||||
}
|
||||
|
||||
match &cli.command {
|
||||
Commands::Init { path, template, yes } => {
|
||||
println!("Initializing project at {}", path.display());
|
||||
println!("Template: {:?}", template);
|
||||
if *yes {
|
||||
println!("Skipping prompts");
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Build {
|
||||
mode,
|
||||
jobs,
|
||||
target_dir,
|
||||
clean,
|
||||
} => {
|
||||
if *clean {
|
||||
println!("Cleaning target directory");
|
||||
}
|
||||
println!("Building in {:?} mode", mode);
|
||||
println!("Using {} parallel jobs", jobs);
|
||||
println!("Target directory: {}", target_dir.display());
|
||||
}
|
||||
|
||||
Commands::Test {
|
||||
pattern,
|
||||
ignored,
|
||||
test_threads,
|
||||
nocapture,
|
||||
} => {
|
||||
println!("Running tests");
|
||||
if let Some(pat) = pattern {
|
||||
println!("Pattern: {}", pat);
|
||||
}
|
||||
if *ignored {
|
||||
println!("Including ignored tests");
|
||||
}
|
||||
if let Some(threads) = test_threads {
|
||||
println!("Test threads: {}", threads);
|
||||
}
|
||||
if *nocapture {
|
||||
println!("Showing test output");
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Deploy {
|
||||
environment,
|
||||
skip_checks,
|
||||
tag,
|
||||
config,
|
||||
} => {
|
||||
println!("Deploying to {:?}", environment);
|
||||
if *skip_checks {
|
||||
println!("⚠️ Skipping pre-deployment checks");
|
||||
}
|
||||
if let Some(version) = tag {
|
||||
println!("Version: {}", version);
|
||||
}
|
||||
|
||||
if let Some(deploy_config) = config {
|
||||
match deploy_config {
|
||||
DeployConfig::Database { url, migrate } => {
|
||||
println!("Database URL: {}", url);
|
||||
if *migrate {
|
||||
println!("Running migrations");
|
||||
}
|
||||
}
|
||||
DeployConfig::Server { host, port, workers } => {
|
||||
println!("Server: {}:{}", host, port);
|
||||
println!("Workers: {}", workers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
//
|
||||
// myapp init --template full
|
||||
// myapp build --mode release --jobs 8 --clean
|
||||
// myapp test integration --test-threads 4
|
||||
// myapp deploy prod --tag v1.0.0 server --host 0.0.0.0 --port 443 --workers 16
|
||||
139
skills/clap-patterns/templates/subcommands.rs
Normal file
139
skills/clap-patterns/templates/subcommands.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
/// Subcommand Template with Clap
|
||||
///
|
||||
/// This template demonstrates:
|
||||
/// - Subcommand derive macro
|
||||
/// - Nested command structure
|
||||
/// - Per-subcommand arguments
|
||||
/// - Enum-based command routing
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "git-like")]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(propagate_version = true)]
|
||||
struct Cli {
|
||||
/// Enable verbose output
|
||||
#[arg(global = true, short, long)]
|
||||
verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Initialize a new repository
|
||||
Init {
|
||||
/// Directory to initialize
|
||||
#[arg(value_name = "DIR", default_value = ".")]
|
||||
path: PathBuf,
|
||||
|
||||
/// Create a bare repository
|
||||
#[arg(long)]
|
||||
bare: bool,
|
||||
},
|
||||
|
||||
/// Add files to staging area
|
||||
Add {
|
||||
/// Files to add
|
||||
#[arg(value_name = "FILE", required = true)]
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
/// Add all files
|
||||
#[arg(short = 'A', long)]
|
||||
all: bool,
|
||||
},
|
||||
|
||||
/// Commit staged changes
|
||||
Commit {
|
||||
/// Commit message
|
||||
#[arg(short, long)]
|
||||
message: String,
|
||||
|
||||
/// Amend previous commit
|
||||
#[arg(long)]
|
||||
amend: bool,
|
||||
},
|
||||
|
||||
/// Remote repository operations
|
||||
Remote {
|
||||
#[command(subcommand)]
|
||||
command: RemoteCommands,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum RemoteCommands {
|
||||
/// Add a new remote
|
||||
Add {
|
||||
/// Remote name
|
||||
name: String,
|
||||
|
||||
/// Remote URL
|
||||
url: String,
|
||||
},
|
||||
|
||||
/// Remove a remote
|
||||
Remove {
|
||||
/// Remote name
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// List all remotes
|
||||
List {
|
||||
/// Show URLs
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Commands::Init { path, bare } => {
|
||||
if cli.verbose {
|
||||
println!("Initializing repository at {:?}", path);
|
||||
}
|
||||
println!(
|
||||
"Initialized {} repository in {}",
|
||||
if *bare { "bare" } else { "normal" },
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Commands::Add { files, all } => {
|
||||
if *all {
|
||||
println!("Adding all files");
|
||||
} else {
|
||||
println!("Adding {} file(s)", files.len());
|
||||
if cli.verbose {
|
||||
for file in files {
|
||||
println!(" - {}", file.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Commit { message, amend } => {
|
||||
if *amend {
|
||||
println!("Amending previous commit");
|
||||
}
|
||||
println!("Committing with message: {}", message);
|
||||
}
|
||||
|
||||
Commands::Remote { command } => match command {
|
||||
RemoteCommands::Add { name, url } => {
|
||||
println!("Adding remote '{}' -> {}", name, url);
|
||||
}
|
||||
RemoteCommands::Remove { name } => {
|
||||
println!("Removing remote '{}'", name);
|
||||
}
|
||||
RemoteCommands::List { verbose } => {
|
||||
println!("Listing remotes{}", if *verbose { " (verbose)" } else { "" });
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
143
skills/clap-patterns/templates/value-enum.rs
Normal file
143
skills/clap-patterns/templates/value-enum.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
/// ValueEnum Template for Type-Safe Options
|
||||
///
|
||||
/// This template demonstrates:
|
||||
/// - ValueEnum trait for constrained choices
|
||||
/// - Type-safe option selection
|
||||
/// - Automatic validation and help text
|
||||
/// - Pattern matching on enums
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
|
||||
/// Output format options
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum Format {
|
||||
/// JavaScript Object Notation
|
||||
Json,
|
||||
/// YAML Ain't Markup Language
|
||||
Yaml,
|
||||
/// Tom's Obvious, Minimal Language
|
||||
Toml,
|
||||
/// Comma-Separated Values
|
||||
Csv,
|
||||
}
|
||||
|
||||
/// Log level options
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum LogLevel {
|
||||
/// Detailed debug information
|
||||
Debug,
|
||||
/// General information
|
||||
Info,
|
||||
/// Warning messages
|
||||
Warn,
|
||||
/// Error messages only
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Color output mode
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum ColorMode {
|
||||
/// Always use colors
|
||||
Always,
|
||||
/// Never use colors
|
||||
Never,
|
||||
/// Automatically detect (default)
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "converter")]
|
||||
#[command(about = "Convert data between formats with type-safe options")]
|
||||
struct Cli {
|
||||
/// Input file
|
||||
input: std::path::PathBuf,
|
||||
|
||||
/// Output format
|
||||
#[arg(short, long, value_enum, default_value_t = Format::Json)]
|
||||
format: Format,
|
||||
|
||||
/// Log level
|
||||
#[arg(short, long, value_enum, default_value_t = LogLevel::Info)]
|
||||
log_level: LogLevel,
|
||||
|
||||
/// Color mode for output
|
||||
#[arg(long, value_enum, default_value_t = ColorMode::Auto)]
|
||||
color: ColorMode,
|
||||
|
||||
/// Pretty print output (for supported formats)
|
||||
#[arg(short, long)]
|
||||
pretty: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Configure logging based on log level
|
||||
match cli.log_level {
|
||||
LogLevel::Debug => println!("🔍 Debug logging enabled"),
|
||||
LogLevel::Info => println!("ℹ️ Info logging enabled"),
|
||||
LogLevel::Warn => println!("⚠️ Warning logging enabled"),
|
||||
LogLevel::Error => println!("❌ Error logging only"),
|
||||
}
|
||||
|
||||
// Check color mode
|
||||
let use_colors = match cli.color {
|
||||
ColorMode::Always => true,
|
||||
ColorMode::Never => false,
|
||||
ColorMode::Auto => atty::is(atty::Stream::Stdout),
|
||||
};
|
||||
|
||||
if use_colors {
|
||||
println!("🎨 Color output enabled");
|
||||
}
|
||||
|
||||
// Process based on format
|
||||
println!("Converting {} to {:?}", cli.input.display(), cli.format);
|
||||
|
||||
match cli.format {
|
||||
Format::Json => {
|
||||
println!("Converting to JSON{}", if cli.pretty { " (pretty)" } else { "" });
|
||||
// JSON conversion logic here
|
||||
}
|
||||
Format::Yaml => {
|
||||
println!("Converting to YAML");
|
||||
// YAML conversion logic here
|
||||
}
|
||||
Format::Toml => {
|
||||
println!("Converting to TOML");
|
||||
// TOML conversion logic here
|
||||
}
|
||||
Format::Csv => {
|
||||
println!("Converting to CSV");
|
||||
// CSV conversion logic here
|
||||
}
|
||||
}
|
||||
|
||||
println!("✓ Conversion complete");
|
||||
}
|
||||
|
||||
// Helper function to check if stdout is a terminal (for color auto-detection)
|
||||
mod atty {
|
||||
pub enum Stream {
|
||||
Stdout,
|
||||
}
|
||||
|
||||
pub fn is(_stream: Stream) -> bool {
|
||||
// Simple implementation - checks if stdout is a TTY
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 }
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
//
|
||||
// cargo run -- input.txt --format json --log-level debug
|
||||
// cargo run -- data.yml --format toml --color always --pretty
|
||||
// cargo run -- config.json --format yaml --log-level warn
|
||||
109
skills/clap-patterns/templates/value-parser.rs
Normal file
109
skills/clap-patterns/templates/value-parser.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
/// Value Parser Template with Custom Validation
|
||||
///
|
||||
/// This template demonstrates:
|
||||
/// - Custom value parsers
|
||||
/// - Range validation
|
||||
/// - Format validation (regex)
|
||||
/// - Error handling with helpful messages
|
||||
|
||||
use clap::Parser;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
const PORT_RANGE: RangeInclusive<usize> = 1..=65535;
|
||||
|
||||
/// Parse and validate port number
|
||||
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()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate email format (basic validation)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse percentage (0-100)
|
||||
fn parse_percentage(s: &str) -> Result<u8, String> {
|
||||
let value: u8 = s
|
||||
.parse()
|
||||
.map_err(|_| format!("`{}` isn't a valid number", s))?;
|
||||
|
||||
if value <= 100 {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err("percentage must be between 0 and 100".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate directory exists
|
||||
fn validate_directory(s: &str) -> Result<std::path::PathBuf, String> {
|
||||
let path = std::path::PathBuf::from(s);
|
||||
|
||||
if path.exists() && path.is_dir() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(format!("directory does not exist: {}", s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "validator")]
|
||||
#[command(about = "CLI with custom value parsers and validation")]
|
||||
struct Cli {
|
||||
/// Port number (1-65535)
|
||||
#[arg(short, long, value_parser = port_in_range)]
|
||||
port: u16,
|
||||
|
||||
/// Email address
|
||||
#[arg(short, long, value_parser = validate_email)]
|
||||
email: String,
|
||||
|
||||
/// Success threshold percentage (0-100)
|
||||
#[arg(short, long, value_parser = parse_percentage, default_value = "80")]
|
||||
threshold: u8,
|
||||
|
||||
/// Working directory (must exist)
|
||||
#[arg(short, long, value_parser = validate_directory)]
|
||||
workdir: Option<std::path::PathBuf>,
|
||||
|
||||
/// Number of retries (1-10)
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "3",
|
||||
value_parser = clap::value_parser!(u8).range(1..=10)
|
||||
)]
|
||||
retries: u8,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
println!("Configuration:");
|
||||
println!(" Port: {}", cli.port);
|
||||
println!(" Email: {}", cli.email);
|
||||
println!(" Threshold: {}%", cli.threshold);
|
||||
println!(" Retries: {}", cli.retries);
|
||||
|
||||
if let Some(workdir) = cli.workdir {
|
||||
println!(" Working directory: {}", workdir.display());
|
||||
}
|
||||
|
||||
// Your application logic here
|
||||
println!("\nValidation passed! All inputs are valid.");
|
||||
}
|
||||
Reference in New Issue
Block a user