Initial commit
This commit is contained in:
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`
|
||||
Reference in New Issue
Block a user