P

Pro Rust Cli Builder

Boost productivity using this plan, build, production, ready. Includes structured workflows, validation checks, and reusable patterns for development.

SkillClipticsdevelopmentv1.0.0MIT
0 views0 copies

Pro Rust CLI Builder

A complete skill for building professional command-line tools in Rust using clap for argument parsing, with support for subcommands, configuration files, colored output, progress bars, and cross-platform distribution.

When to Use This Skill

Choose this skill when:

  • Scaffolding a new Rust CLI tool from scratch with argument parsing
  • Adding subcommands, flags, and positional arguments to existing CLIs
  • Implementing configuration file loading (TOML, JSON, YAML)
  • Adding colored terminal output, progress bars, and interactive prompts
  • Packaging and distributing CLI tools via crates.io or GitHub releases

Consider alternatives when:

  • Building a Rust web server → use an Actix/Axum skill
  • Need a quick script without compilation → use a Python or Bash skill
  • Building a GUI application → use a Rust GUI framework skill
  • Working with Rust async networking → use a Tokio skill

Quick Start

# Create a new Rust CLI project cargo init my-cli cd my-cli cargo add clap --features derive cargo add anyhow colored serde toml cargo add indicatif # progress bars
// src/main.rs use clap::{Parser, Subcommand}; use anyhow::Result; #[derive(Parser)] #[command(name = "mytool", version, about = "A professional CLI tool")] struct Cli { /// Enable verbose output #[arg(short, long, global = true)] verbose: bool, /// Config file path #[arg(short, long, default_value = "~/.config/mytool/config.toml")] config: String, #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Initialize a new project Init { /// Project name name: String, /// Template to use #[arg(short, long, default_value = "default")] template: String, }, /// Run analysis on the project Analyze { /// Files to analyze #[arg(required = true)] files: Vec<String>, /// Output format #[arg(short, long, value_enum, default_value = "text")] format: OutputFormat, }, } #[derive(clap::ValueEnum, Clone)] enum OutputFormat { Text, Json, Table } fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { Commands::Init { name, template } => init_project(&name, &template, cli.verbose), Commands::Analyze { files, format } => analyze_files(&files, format, cli.verbose), } }

Core Concepts

CLI Architecture Components

ComponentCratePurpose
Arg parsingclap (derive)Declarative argument definitions with types
Error handlinganyhowContextual error chains with ? operator
Colored outputcoloredTerminal colors: "text".red().bold()
Progress barsindicatifBars, spinners, multi-progress
Config filesserde + tomlTyped config deserialization
File dialogsdialoguerInteractive prompts, confirmations, selects
Loggingenv_loggerRUST_LOG=debug environment-based logging

Configuration File Loading

use serde::Deserialize; use std::path::PathBuf; #[derive(Deserialize, Default)] struct Config { #[serde(default)] output_dir: Option<PathBuf>, #[serde(default = "default_threads")] threads: usize, #[serde(default)] ignore_patterns: Vec<String>, } fn default_threads() -> usize { num_cpus::get() } fn load_config(path: &str) -> Result<Config> { let expanded = shellexpand::tilde(path); match std::fs::read_to_string(expanded.as_ref()) { Ok(content) => Ok(toml::from_str(&content)?), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Config::default()), Err(e) => Err(e.into()), } }

Progress Bar for Long Operations

use indicatif::{ProgressBar, ProgressStyle, MultiProgress}; fn process_files(files: &[String]) -> Result<()> { let multi = MultiProgress::new(); let pb = multi.add(ProgressBar::new(files.len() as u64)); pb.set_style(ProgressStyle::default_bar() .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")? .progress_chars("=>-")); for file in files { pb.set_message(format!("Processing {file}")); process_single_file(file)?; pb.inc(1); } pb.finish_with_message("Done!"); Ok(()) }

Configuration

ParameterTypeDefaultDescription
argParserstring'clap-derive'Argument parsing style: derive macros vs builder API
errorHandlingstring'anyhow'Error library: anyhow (apps) or thiserror (libs)
configFormatstring'toml'Config file format: TOML, JSON, or YAML
colorSupportstring'auto'Color output: auto-detect, always, or never
completionsbooleantrueGenerate shell completions (bash, zsh, fish)
manPagebooleanfalseGenerate man page from clap definitions

Best Practices

  1. Use clap's derive macros for type-safe argument parsing — The derive API provides compile-time validation, automatic help generation, and type conversion. Builder API is only needed for dynamic argument construction.

  2. Layer configuration: CLI args → env vars → config file → defaults — Higher-priority sources override lower ones. Clap handles args, std::env handles env vars, serde handles config files. This follows the principle of least surprise.

  3. Use anyhow for applications, thiserror for libraries — Application CLIs benefit from anyhow's ergonomic ? operator and context chains. Library crates should use thiserror for structured, matchable error types.

  4. Respect NO_COLOR and --no-color conventions — Check the NO_COLOR environment variable and provide a --no-color flag. Automatically disable colors when stdout is not a TTY (piped to file or another program).

  5. Generate shell completions at build time — Use clap's generate feature to create completion scripts for bash, zsh, fish, and PowerShell. Include these in your distribution package for a polished user experience.

Common Issues

Arguments conflict or get parsed incorrectly — Clap processes arguments left-to-right. When a subcommand name conflicts with a flag value, use -- to separate flags from positional arguments. Set trailing_var_arg = true for commands that accept arbitrary trailing arguments.

Config file not found on first run — Don't error when the config file doesn't exist — return defaults instead. Create the config directory and a template file with mytool init or mytool config --init. Use directories crate for platform-appropriate config paths.

Binary size too large for distribution — Rust release binaries can be large due to debug symbols and static linking. Add [profile.release] optimizations: strip = true, lto = true, opt-level = "s". Use cargo-bloat to identify the largest contributors to binary size.

Community

Reviews

Write a review

No reviews yet. Be the first to review this template!

Similar Templates