Build production Rust CLIs with Clap: subcommands, config layering, validation, exit codes, shell completions, and testable command surfaces

GitHub
Install command
npx skhub add bobmatnyc/clap
Markdown
SKILL.md

Clap (Rust) - Production CLI Patterns

Overview

Clap provides declarative command-line parsing with strong help output, validation, and subcommand support. Use it to build CLIs with predictable UX and testable execution paths.

Quick Start

Minimal CLI

✅ Correct: derive Parser

use clap::Parser;

#[derive(Parser, Debug)]
#[command(name = "mytool", version, about = "Example CLI")]
struct Args {
    /// Enable verbose output
    #[arg(long)]
    verbose: bool,

    /// Input file path
    #[arg(value_name = "FILE")]
    input: String,
}

fn main() {
    let args = Args::parse();
    if args.verbose {
        eprintln!("verbose enabled");
    }
    println!("input={}", args.input);
}

❌ Wrong: parse multiple times

fn main() {
    let _a = Args::parse();
    let _b = Args::parse(); // duplicate parsing and inconsistent behavior
}

Subcommands (real tools)

Model multi-mode CLIs with subcommands and shared global flags.

✅ Correct: global flags + subcommands

use clap::{Parser, Subcommand, ValueEnum};

#[derive(Parser, Debug)]
struct Args {
    #[arg(long, global = true)]
    verbose: bool,

    #[arg(long, global = true, env = "MYTOOL_CONFIG")]
    config: Option<String>,

    #[command(subcommand)]
    cmd: Command,
}

#[derive(Subcommand, Debug)]
enum Command {
    Serve { #[arg(long, default_value_t = 3000)] port: u16 },
    Migrate { #[arg(long, value_enum, default_value_t = Mode::Up)] mode: Mode },
}

#[derive(Copy, Clone, Debug, ValueEnum)]
enum Mode { Up, Down }

fn main() {
    let args = Args::parse();
    match args.cmd {
        Command::Serve { port } => println!("serve on {}", port),
        Command::Migrate { mode } => println!("migrate: {:?}", mode),
    }
}

Config layering (CLI + env + config file)

Prefer explicit precedence:

  1. CLI flags
  2. Environment variables
  3. Config file
  4. Defaults

✅ Correct: merge config with CLI overrides

use clap::Parser;
use serde::Deserialize;

#[derive(Parser, Debug)]
struct Args {
    #[arg(long, env = "APP_PORT")]
    port: Option<u16>,
}

#[derive(Deserialize)]
struct FileConfig {
    port: Option<u16>,
}

fn effective_port(args: &Args, file: &FileConfig) -> u16 {
    args.port.or(file.port).unwrap_or(3000)
}

Exit codes and error handling

Map failures to stable exit codes. Return Result from command handlers and centralize printing.

✅ Correct: command returns Result

use std::process::ExitCode;

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("{e}");
            ExitCode::from(1)
        }
    }
}

fn run() -> Result<(), String> {
    Ok(())
}

Testing (assert_cmd)

Test the binary surface (arguments, output, exit codes) without coupling to internals.

✅ Correct: integration test

use assert_cmd::Command;

#[test]
fn shows_help() {
    Command::cargo_bin("mytool")
        .unwrap()
        .arg("--help")
        .assert()
        .success();
}

Shell completions (optional)

Generate completions for Bash/Zsh/Fish.

✅ Correct: emit completions

use clap::{CommandFactory, Parser};
use clap_complete::{generate, shells::Zsh};
use std::io;

fn print_zsh_completions() {
    let mut cmd = super::Args::command();
    generate(Zsh, &mut cmd, "mytool", &mut io::stdout());
}

Anti-Patterns

  • Parse arguments in library code; parse once in main and pass a typed config down.
  • Hide failures behind unwrap; return stable exit codes and structured errors.
  • Overload one command with flags; use subcommands for distinct modes.

Resources

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

toolchains/rust/cli/clap

Default branch

main

Latest commit

718070a

Tree SHA

3b7f612