Files
2025-11-30 09:04:14 +08:00

9.8 KiB

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

[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

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:

#[arg(short, long, global = true)]
verbose: bool,

2. Environment Variables

Fallback to environment variables:

#[arg(short, long, env = "MY_TOOL_CONFIG")]
config: PathBuf,

3. Validation

Numeric range validation:

#[arg(short, long, value_parser = clap::value_parser!(u8).range(1..=32))]
jobs: u8,

4. Type-Safe Enums

Constrained choices with ValueEnum:

#[derive(ValueEnum)]
enum Environment {
    Dev,
    Staging,
    Prod,
}

5. Nested Subcommands

Multi-level command structure:

Config {
    #[command(subcommand)]
    action: ConfigAction,
}

6. Default Values

Sensible defaults for all options:

#[arg(short, long, default_value = "config.json")]
config: PathBuf,

Integration Tests

tests/cli_tests.rs:

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

# 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:

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

# 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