Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:10 +08:00
commit cc8f6e6dfa
15 changed files with 6805 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
---
name: clap-patterns
description: Common Clap patterns and idioms for argument parsing, validation, and CLI design. Use when implementing CLI arguments with Clap v4+.
---
# Clap Patterns Skill
Common patterns and idioms for using Clap v4+ effectively in Rust CLI applications.
## Derive API vs Builder API
### When to Use Derive API
- CLI structure known at compile time
- Want type safety and compile-time validation
- Prefer declarative style
- Standard CLI patterns are sufficient
```rust
#[derive(Parser)]
#[command(version, about)]
struct Cli {
#[arg(short, long)]
input: PathBuf,
}
```
### When to Use Builder API
- CLI needs to be built dynamically at runtime
- Building plugin systems
- Arguments depend on configuration
- Need maximum flexibility
```rust
fn build_cli() -> Command {
Command::new("app")
.arg(Arg::new("input").short('i'))
}
```
## Common Patterns
### Global Options with Subcommands
```rust
#[derive(Parser)]
struct Cli {
#[arg(short, long, global = true, action = ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Commands,
}
```
### Argument Groups for Mutual Exclusivity
```rust
#[derive(Parser)]
#[command(group(
ArgGroup::new("format")
.required(true)
.args(&["json", "yaml", "toml"])
))]
struct Cli {
#[arg(long)]
json: bool,
#[arg(long)]
yaml: bool,
#[arg(long)]
toml: bool,
}
```
### Custom Value Parsers
```rust
fn parse_port(s: &str) -> Result<u16, String> {
let port: u16 = s.parse()
.map_err(|_| format!("`{s}` isn't a valid port"))?;
if (1024..=65535).contains(&port) {
Ok(port)
} else {
Err(format!("port not in range 1024-65535"))
}
}
#[derive(Parser)]
struct Cli {
#[arg(long, value_parser = parse_port)]
port: u16,
}
```
### Environment Variable Fallbacks
```rust
#[derive(Parser)]
struct Cli {
#[arg(long, env = "API_TOKEN")]
token: String,
#[arg(long, env = "API_ENDPOINT", default_value = "https://api.example.com")]
endpoint: String,
}
```
### Flattening Shared Options
```rust
#[derive(Args)]
struct CommonOpts {
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
config: Option<PathBuf>,
}
#[derive(Parser)]
struct Cli {
#[command(flatten)]
common: CommonOpts,
#[command(subcommand)]
command: Commands,
}
```
### Multiple Values
```rust
#[derive(Parser)]
struct Cli {
/// Tags (can be specified multiple times)
#[arg(short, long)]
tag: Vec<String>,
/// Files to process
files: Vec<PathBuf>,
}
// Usage: myapp --tag rust --tag cli file1.txt file2.txt
```
### Subcommand with Shared Arguments
```rust
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Build(BuildArgs),
Test(TestArgs),
}
#[derive(Args)]
struct BuildArgs {
#[command(flatten)]
common: CommonOpts,
#[arg(short, long)]
release: bool,
}
```
### Argument Counting (Verbosity Levels)
```rust
#[derive(Parser)]
struct Cli {
/// Verbosity (-v, -vv, -vvv)
#[arg(short, long, action = ArgAction::Count)]
verbose: u8,
}
// Usage: -v (1), -vv (2), -vvv (3)
```
### Help Template Customization
```rust
#[derive(Parser)]
#[command(
after_help = "EXAMPLES:\n \
myapp --input file.txt\n \
myapp -i file.txt -vv\n\n\
For more info: https://example.com"
)]
struct Cli {
// ...
}
```
### Value Hints
```rust
#[derive(Parser)]
struct Cli {
#[arg(short, long, value_name = "FILE", value_hint = ValueHint::FilePath)]
input: PathBuf,
#[arg(short, long, value_name = "DIR", value_hint = ValueHint::DirPath)]
output: PathBuf,
#[arg(short, long, value_name = "URL", value_hint = ValueHint::Url)]
endpoint: String,
}
```
### Default Values with Functions
```rust
fn default_config_path() -> PathBuf {
dirs::config_dir()
.unwrap()
.join("myapp")
.join("config.toml")
}
#[derive(Parser)]
struct Cli {
#[arg(long, default_value_os_t = default_config_path())]
config: PathBuf,
}
```
## Best Practices
1. **Use `value_name`** for clearer help text
2. **Provide both short and long flags** where appropriate
3. **Add help text** to all arguments
4. **Use `ValueEnum`** for fixed set of choices
5. **Validate early** with custom parsers
6. **Support environment variables** for sensitive data
7. **Use argument groups** for mutually exclusive options
8. **Document with examples** in `after_help`
9. **Use semantic types** (PathBuf, not String for paths)
10. **Test CLI parsing** with integration tests
## References
- [Clap Documentation](https://docs.rs/clap/)
- [Clap Derive Reference](https://docs.rs/clap/latest/clap/_derive/index.html)
- [Clap Examples](https://github.com/clap-rs/clap/tree/master/examples)

View File

@@ -0,0 +1,471 @@
---
name: cli-configuration
description: Configuration management patterns including file formats, precedence, environment variables, and XDG directories. Use when implementing configuration systems for CLI applications.
---
# CLI Configuration Skill
Patterns and best practices for managing configuration in command-line applications.
## Configuration Precedence
The standard precedence order (lowest to highest priority):
1. **Compiled defaults** - Hard-coded sensible defaults
2. **System config** - /etc/myapp/config.toml
3. **User config** - ~/.config/myapp/config.toml
4. **Project config** - ./myapp.toml or ./.myapp.toml
5. **Environment variables** - MYAPP_KEY=value
6. **CLI arguments** - --key value (highest priority)
```rust
use config::{Config as ConfigBuilder, Environment, File};
pub fn load_config(cli: &Cli) -> Result<Config> {
let mut builder = ConfigBuilder::builder()
// 1. Defaults
.set_default("port", 8080)?
.set_default("host", "localhost")?
.set_default("log_level", "info")?;
// 2. System config (if exists)
builder = builder
.add_source(File::with_name("/etc/myapp/config").required(false));
// 3. User config (if exists)
if let Some(config_dir) = dirs::config_dir() {
builder = builder.add_source(
File::from(config_dir.join("myapp/config.toml")).required(false)
);
}
// 4. Project config (if exists)
builder = builder
.add_source(File::with_name("myapp").required(false))
.add_source(File::with_name(".myapp").required(false));
// 5. CLI-specified config (if provided)
if let Some(config_path) = &cli.config {
builder = builder.add_source(File::from(config_path.as_ref()));
}
// 6. Environment variables
builder = builder.add_source(
Environment::with_prefix("MYAPP")
.separator("_")
.try_parsing(true)
);
// 7. CLI arguments (highest priority)
if let Some(port) = cli.port {
builder = builder.set_override("port", port)?;
}
Ok(builder.build()?.try_deserialize()?)
}
```
## Config File Formats
### TOML (Recommended)
Clear, human-readable, good error messages.
```toml
# config.toml
[general]
port = 8080
host = "localhost"
log_level = "info"
[database]
url = "postgresql://localhost/mydb"
pool_size = 10
[features]
caching = true
metrics = false
[[servers]]
name = "primary"
address = "192.168.1.1"
[[servers]]
name = "backup"
address = "192.168.1.2"
```
```rust
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct Config {
general: General,
database: Database,
features: Features,
servers: Vec<Server>,
}
#[derive(Debug, Deserialize, Serialize)]
struct General {
port: u16,
host: String,
log_level: String,
}
```
### YAML (Alternative)
More concise, supports comments, complex structures.
```yaml
# config.yaml
general:
port: 8080
host: localhost
log_level: info
database:
url: postgresql://localhost/mydb
pool_size: 10
features:
caching: true
metrics: false
servers:
- name: primary
address: 192.168.1.1
- name: backup
address: 192.168.1.2
```
### JSON (Machine-Readable)
Good for programmatic generation, less human-friendly.
```json
{
"general": {
"port": 8080,
"host": "localhost",
"log_level": "info"
},
"database": {
"url": "postgresql://localhost/mydb",
"pool_size": 10
}
}
```
## XDG Base Directory Support
Follow the XDG Base Directory specification for cross-platform compatibility.
```rust
use directories::ProjectDirs;
pub struct AppPaths {
pub config_dir: PathBuf,
pub data_dir: PathBuf,
pub cache_dir: PathBuf,
pub state_dir: PathBuf,
}
impl AppPaths {
pub fn new(app_name: &str) -> Result<Self> {
let proj_dirs = ProjectDirs::from("com", "example", app_name)
.ok_or_else(|| anyhow!("Could not determine project directories"))?;
Ok(Self {
config_dir: proj_dirs.config_dir().to_path_buf(),
data_dir: proj_dirs.data_dir().to_path_buf(),
cache_dir: proj_dirs.cache_dir().to_path_buf(),
state_dir: proj_dirs.state_dir()
.unwrap_or_else(|| proj_dirs.data_dir())
.to_path_buf(),
})
}
pub fn config_file(&self) -> PathBuf {
self.config_dir.join("config.toml")
}
pub fn ensure_dirs(&self) -> Result<()> {
fs::create_dir_all(&self.config_dir)?;
fs::create_dir_all(&self.data_dir)?;
fs::create_dir_all(&self.cache_dir)?;
fs::create_dir_all(&self.state_dir)?;
Ok(())
}
}
```
**Directory locations by platform:**
| Platform | Config | Data | Cache |
|----------|--------|------|-------|
| Linux | ~/.config/myapp | ~/.local/share/myapp | ~/.cache/myapp |
| macOS | ~/Library/Application Support/myapp | ~/Library/Application Support/myapp | ~/Library/Caches/myapp |
| Windows | %APPDATA%\example\myapp | %APPDATA%\example\myapp | %LOCALAPPDATA%\example\myapp |
## Environment Variable Patterns
### Naming Convention
Use `APPNAME_SECTION_KEY` format:
```bash
MYAPP_DATABASE_URL=postgresql://localhost/db
MYAPP_LOG_LEVEL=debug
MYAPP_FEATURES_CACHING=true
MYAPP_PORT=9000
```
### Integration with Clap
```rust
#[derive(Parser)]
struct Cli {
/// Database URL (env: MYAPP_DATABASE_URL)
#[arg(long, env = "MYAPP_DATABASE_URL")]
database_url: Option<String>,
/// Log level (env: MYAPP_LOG_LEVEL)
#[arg(long, env = "MYAPP_LOG_LEVEL", default_value = "info")]
log_level: String,
/// Port (env: MYAPP_PORT)
#[arg(long, env = "MYAPP_PORT", default_value = "8080")]
port: u16,
}
```
### Sensitive Data Pattern
**Never** put secrets in config files. Use environment variables instead.
```rust
#[derive(Debug, Deserialize)]
struct Config {
pub host: String,
pub port: u16,
// Loaded from environment only
#[serde(skip)]
pub api_token: String,
}
impl Config {
pub fn load() -> Result<Self> {
let mut config: Config = /* load from file */;
// Sensitive data from env only
config.api_token = env::var("MYAPP_API_TOKEN")
.context("MYAPP_API_TOKEN environment variable required")?;
Ok(config)
}
}
```
## Configuration Validation
Validate configuration early at load time:
```rust
#[derive(Debug, Deserialize)]
struct Config {
pub port: u16,
pub host: String,
pub workers: usize,
}
impl Config {
pub fn validate(&self) -> Result<()> {
// Port range
if !(1024..=65535).contains(&self.port) {
bail!("Port must be between 1024 and 65535, got {}", self.port);
}
// Workers
if self.workers == 0 {
bail!("Workers must be at least 1");
}
let max_workers = num_cpus::get() * 2;
if self.workers > max_workers {
bail!(
"Workers ({}) exceeds recommended maximum ({})",
self.workers,
max_workers
);
}
// Host validation
if self.host.is_empty() {
bail!("Host cannot be empty");
}
Ok(())
}
}
```
## Generating Default Config
Provide a command to generate a default configuration file:
```rust
impl Config {
pub fn default_config() -> Self {
Self {
general: General {
port: 8080,
host: "localhost".to_string(),
log_level: "info".to_string(),
},
database: Database {
url: "postgresql://localhost/mydb".to_string(),
pool_size: 10,
},
features: Features {
caching: true,
metrics: false,
},
}
}
pub fn write_default(path: &Path) -> Result<()> {
let config = Self::default_config();
let toml = toml::to_string_pretty(&config)?;
// Add helpful comments
let content = format!(
"# Configuration file for myapp\n\
# See: https://example.com/docs/config\n\n\
{toml}"
);
fs::write(path, content)?;
Ok(())
}
}
```
**CLI Command:**
```rust
#[derive(Subcommand)]
enum Commands {
/// Generate a default configuration file
InitConfig {
/// Output path (default: ~/.config/myapp/config.toml)
#[arg(short, long)]
output: Option<PathBuf>,
},
}
fn handle_init_config(output: Option<PathBuf>) -> Result<()> {
let path = output.unwrap_or_else(|| {
AppPaths::new("myapp")
.unwrap()
.config_file()
});
if path.exists() {
bail!("Config file already exists: {}", path.display());
}
Config::write_default(&path)?;
println!("Created config file: {}", path.display());
Ok(())
}
```
## Config Migration Pattern
Handle breaking changes in config format:
```rust
#[derive(Debug, Deserialize)]
struct ConfigV2 {
version: u32,
#[serde(flatten)]
data: ConfigData,
}
impl ConfigV2 {
pub fn load(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)?;
let mut config: ConfigV2 = toml::from_str(&content)?;
// Migrate from older versions
match config.version {
1 => {
eprintln!("Migrating config from v1 to v2...");
config = migrate_v1_to_v2(config)?;
// Optionally save migrated config
config.save(path)?;
}
2 => {}, // Current version
v => bail!("Unsupported config version: {}", v),
}
Ok(config)
}
}
```
## Configuration Examples Command
Provide examples in help text:
```rust
#[derive(Subcommand)]
enum Commands {
/// Show configuration examples
ConfigExamples,
}
fn show_config_examples() {
println!("Configuration Examples:\n");
println!("1. Basic configuration (config.toml):");
println!("{}", r#"
[general]
port = 8080
host = "localhost"
"#);
println!("\n2. Environment variables:");
println!(" MYAPP_PORT=9000");
println!(" MYAPP_DATABASE_URL=postgresql://localhost/db");
println!("\n3. CLI override:");
println!(" myapp --port 9000 --host 0.0.0.0");
println!("\n4. Precedence (highest to lowest):");
println!(" CLI args > Env vars > Config file > Defaults");
}
```
## Best Practices
1. **Provide sensible defaults** - App should work out-of-box
2. **Document precedence** - Make override behavior clear
3. **Validate early** - Catch config errors at startup
4. **Use XDG directories** - Follow platform conventions
5. **Support env vars** - Essential for containers/CI
6. **Generate defaults** - Help users get started
7. **Version config format** - Enable migrations
8. **Keep secrets out** - Use env vars for sensitive data
9. **Clear error messages** - Help users fix config issues
10. **Document all options** - With examples and defaults
## References
- [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
- [The Twelve-Factor App: Config](https://12factor.net/config)
- [directories crate](https://docs.rs/directories/)
- [config crate](https://docs.rs/config/)

View File

@@ -0,0 +1,550 @@
---
name: cli-distribution
description: Distribution and packaging patterns including shell completions, man pages, cross-compilation, and release automation. Use when preparing CLI tools for distribution.
---
# CLI Distribution Skill
Patterns and best practices for distributing Rust CLI applications to users.
## Shell Completion Generation
### Using clap_complete
```rust
use clap::{CommandFactory, Parser};
use clap_complete::{generate, Generator, Shell};
use std::io;
#[derive(Parser)]
struct Cli {
/// Generate shell completions
#[arg(long = "generate", value_enum)]
generator: Option<Shell>,
// ... other fields
}
fn print_completions<G: Generator>(gen: G, cmd: &mut clap::Command) {
generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
}
fn main() {
let cli = Cli::parse();
if let Some(generator) = cli.generator {
let mut cmd = Cli::command();
print_completions(generator, &mut cmd);
return;
}
// ... rest of application
}
```
### Installation Instructions by Shell
**Bash:**
```bash
# Generate and save
myapp --generate bash > /etc/bash_completion.d/myapp
# Or add to ~/.bashrc
eval "$(myapp --generate bash)"
```
**Zsh:**
```bash
# Generate and save
myapp --generate zsh > ~/.zfunc/_myapp
# Add to ~/.zshrc
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit
```
**Fish:**
```bash
# Generate and save
myapp --generate fish > ~/.config/fish/completions/myapp.fish
# Or load directly
myapp --generate fish | source
```
**PowerShell:**
```powershell
# Add to $PROFILE
Invoke-Expression (& myapp --generate powershell)
```
### Dynamic Completions
For commands with dynamic values (like listing resources):
```rust
use clap::CommandFactory;
use clap_complete::{generate, Generator};
pub fn generate_with_values<G: Generator>(
gen: G,
resources: &[String],
) -> String {
let mut cmd = Cli::command();
// Add dynamic values to completion
if let Some(subcommand) = cmd.find_subcommand_mut("get") {
for resource in resources {
subcommand = subcommand.arg(
clap::Arg::new("resource")
.value_parser(clap::builder::PossibleValuesParser::new(resource))
);
}
}
let mut buf = Vec::new();
generate(gen, &mut cmd, "myapp", &mut buf);
String::from_utf8(buf).unwrap()
}
```
## Man Page Generation
### Using clap_mangen
```toml
[dependencies]
clap_mangen = "0.2"
```
```rust
use clap::CommandFactory;
use clap_mangen::Man;
use std::io;
fn generate_man_page() {
let cmd = Cli::command();
let man = Man::new(cmd);
man.render(&mut io::stdout()).unwrap();
}
```
### Build Script for Man Pages
```rust
// build.rs
use clap::CommandFactory;
use clap_mangen::Man;
use std::fs;
use std::path::PathBuf;
include!("src/cli.rs");
fn main() {
let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/man");
fs::create_dir_all(&out_dir).unwrap();
let cmd = Cli::command();
let man = Man::new(cmd);
let mut buffer = Vec::new();
man.render(&mut buffer).unwrap();
fs::write(out_dir.join("myapp.1"), buffer).unwrap();
}
```
### Install Man Page
```bash
# System-wide
sudo cp target/man/myapp.1 /usr/local/share/man/man1/
# User-local
mkdir -p ~/.local/share/man/man1
cp target/man/myapp.1 ~/.local/share/man/man1/
```
## Cross-Compilation
### Target Triples
Common targets for CLI distribution:
```bash
# Linux
x86_64-unknown-linux-gnu # GNU Linux
x86_64-unknown-linux-musl # MUSL Linux (static)
aarch64-unknown-linux-gnu # ARM64 Linux
# macOS
x86_64-apple-darwin # Intel Mac
aarch64-apple-darwin # Apple Silicon
# Windows
x86_64-pc-windows-msvc # Windows MSVC
x86_64-pc-windows-gnu # Windows GNU
```
### Cross-Compilation with cross
```bash
# Install cross
cargo install cross
# Build for Linux from any platform
cross build --release --target x86_64-unknown-linux-gnu
# Build static binary with MUSL
cross build --release --target x86_64-unknown-linux-musl
```
### GitHub Actions for Cross-Compilation
```yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: myapp
asset_name: myapp-linux-amd64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
artifact_name: myapp
asset_name: myapp-linux-musl-amd64
- os: macos-latest
target: x86_64-apple-darwin
artifact_name: myapp
asset_name: myapp-macos-amd64
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: myapp
asset_name: myapp-macos-arm64
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact_name: myapp.exe
asset_name: myapp-windows-amd64.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Upload binaries
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.asset_name }}
path: target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
```
## Binary Size Optimization
### Cargo.toml optimizations
```toml
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Strip symbols
panic = "abort" # Smaller panic handler
```
### Additional size reduction
```bash
# Install upx
brew install upx # macOS
apt install upx # Linux
# Compress binary
upx --best --lzma target/release/myapp
```
**Before/After example:**
```
Original: 2.5 MB
Optimized: 1.2 MB (strip = true)
UPX: 400 KB (upx --best --lzma)
```
## Package Distribution
### Homebrew (macOS/Linux)
Create a formula:
```ruby
# Formula/myapp.rb
class Myapp < Formula
desc "Description of your CLI tool"
homepage "https://github.com/username/myapp"
url "https://github.com/username/myapp/archive/v1.0.0.tar.gz"
sha256 "abc123..."
license "MIT"
depends_on "rust" => :build
def install
system "cargo", "install", "--locked", "--root", prefix, "--path", "."
# Install shell completions
generate_completions_from_executable(bin/"myapp", "--generate")
# Install man page
man1.install "target/man/myapp.1"
end
test do
assert_match "myapp 1.0.0", shell_output("#{bin}/myapp --version")
end
end
```
### Debian Package (.deb)
Using `cargo-deb`:
```bash
cargo install cargo-deb
# Create debian package
cargo deb
# Package will be in target/debian/myapp_1.0.0_amd64.deb
```
**Cargo.toml metadata:**
```toml
[package.metadata.deb]
maintainer = "Your Name <you@example.com>"
copyright = "2024, Your Name"
license-file = ["LICENSE", "4"]
extended-description = """
A longer description of your CLI tool
that spans multiple lines."""
depends = "$auto"
section = "utility"
priority = "optional"
assets = [
["target/release/myapp", "usr/bin/", "755"],
["README.md", "usr/share/doc/myapp/", "644"],
["target/completions/myapp.bash", "usr/share/bash-completion/completions/", "644"],
["target/man/myapp.1", "usr/share/man/man1/", "644"],
]
```
### Docker Distribution
```dockerfile
# Dockerfile
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp
ENTRYPOINT ["myapp"]
```
**Multi-stage with MUSL (smaller image):**
```dockerfile
FROM rust:1.75-alpine as builder
RUN apk add --no-cache musl-dev
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp
ENTRYPOINT ["/myapp"]
```
### Cargo-binstall Support
Add metadata for faster installation:
```toml
[package.metadata.binstall]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }{ binary-ext }"
bin-dir = "{ bin }{ binary-ext }"
pkg-fmt = "bin"
```
Users can then install with:
```bash
cargo binstall myapp
```
## Auto-Update
### Using self_update crate
```toml
[dependencies]
self_update = "0.39"
```
```rust
use self_update::cargo_crate_version;
fn update() -> Result<()> {
let status = self_update::backends::github::Update::configure()
.repo_owner("username")
.repo_name("myapp")
.bin_name("myapp")
.show_download_progress(true)
.current_version(cargo_crate_version!())
.build()?
.update()?;
println!("Update status: `{}`!", status.version());
Ok(())
}
```
### Update Command
```rust
#[derive(Subcommand)]
enum Commands {
/// Update to the latest version
Update,
}
fn handle_update() -> Result<()> {
println!("Checking for updates...");
match update() {
Ok(_) => {
println!("Updated successfully! Please restart the application.");
Ok(())
}
Err(e) => {
eprintln!("Update failed: {}", e);
eprintln!("Download manually: https://github.com/username/myapp/releases");
Err(e)
}
}
}
```
## Release Automation
### Cargo-release
```bash
cargo install cargo-release
# Dry run
cargo release --dry-run
# Release patch version
cargo release patch --execute
# Release minor version
cargo release minor --execute
```
### GitHub Release Action
```yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: taiki-e/create-gh-release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
changelog: CHANGELOG.md
upload-assets:
needs: release
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
- target: x86_64-apple-darwin
- target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: taiki-e/upload-rust-binary-action@v1
with:
bin: myapp
target: ${{ matrix.target }}
token: ${{ secrets.GITHUB_TOKEN }}
```
## Best Practices
1. **Provide multiple installation methods** - Cargo, Homebrew, apt, etc.
2. **Generate completions** - Essential for good UX
3. **Create man pages** - Professional documentation
4. **Test cross-platform** - Build for all major platforms
5. **Optimize binary size** - Users appreciate smaller downloads
6. **Automate releases** - Use CI/CD for consistent builds
7. **Version clearly** - Semantic versioning
8. **Sign binaries** - Build trust (especially on macOS)
9. **Provide checksums** - Verify download integrity
10. **Document installation** - Clear, platform-specific instructions
## Distribution Checklist
- [ ] Shell completions generated (bash, zsh, fish, powershell)
- [ ] Man pages created
- [ ] Cross-compiled for major platforms
- [ ] Binary size optimized
- [ ] Release artifacts uploaded to GitHub
- [ ] Installation instructions in README
- [ ] Homebrew formula (if applicable)
- [ ] Debian package (if applicable)
- [ ] Docker image (if applicable)
- [ ] Checksums provided
- [ ] Changelog maintained
- [ ] Version bumped properly
## References
- [clap_complete Documentation](https://docs.rs/clap_complete/)
- [clap_mangen Documentation](https://docs.rs/clap_mangen/)
- [cargo-deb](https://github.com/kornelski/cargo-deb)
- [cross](https://github.com/cross-rs/cross)
- [Rust Platform Support](https://doc.rust-lang.org/nightly/rustc/platform-support.html)

View File

@@ -0,0 +1,366 @@
---
name: cli-ux-patterns
description: CLI user experience best practices for error messages, colors, progress indicators, and output formatting. Use when improving CLI usability and user experience.
---
# CLI UX Patterns Skill
Best practices and patterns for creating delightful command-line user experiences.
## Error Message Patterns
### The Three Parts of Good Error Messages
1. **What went wrong** - Clear description of the error
2. **Why it matters** - Context about the operation
3. **How to fix it** - Actionable suggestions
```rust
bail!(
"Failed to read config file: {}\n\n\
The application needs a valid configuration to start.\n\n\
To fix this:\n\
1. Create a config file: myapp init\n\
2. Or specify a different path: --config /path/to/config.toml\n\
3. Check file permissions: ls -l {}",
path.display(),
path.display()
);
```
### Using miette for Rich Diagnostics
```rust
#[derive(Error, Debug, Diagnostic)]
#[error("Configuration error")]
#[diagnostic(
code(config::invalid),
url("https://docs.example.com/config"),
help("Check the syntax of your configuration file")
)]
struct ConfigError {
#[source_code]
src: String,
#[label("invalid value here")]
span: SourceSpan,
}
```
## Color Usage Patterns
### Semantic Colors
- **Red** - Errors, failures, destructive actions
- **Yellow** - Warnings, cautions
- **Green** - Success, completion, safe operations
- **Blue** - Information, hints, links
- **Cyan** - Highlights, emphasis
- **Dim/Gray** - Less important info, metadata
```rust
use owo_colors::OwoColorize;
// Status indicators with colors
println!("{} Build succeeded", "".green().bold());
println!("{} Warning: using default", "".yellow().bold());
println!("{} Error: file not found", "".red().bold());
println!("{} Info: processing 10 files", "".blue().bold());
```
### Respecting NO_COLOR
```rust
use owo_colors::{OwoColorize, Stream};
fn print_status(message: &str, is_error: bool) {
let stream = if is_error { Stream::Stderr } else { Stream::Stdout };
if is_error {
eprintln!("{}", message.if_supports_color(stream, |text| text.red()));
} else {
println!("{}", message.if_supports_color(stream, |text| text.green()));
}
}
```
## Progress Indication Patterns
### When to Use Progress Bars
- File downloads/uploads
- Bulk processing with known count
- Multi-step processes
- Any operation > 2 seconds with known total
```rust
use indicatif::{ProgressBar, ProgressStyle};
let pb = ProgressBar::new(items.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40}] {pos}/{len} {msg}")?
.progress_chars("=>-")
);
for item in items {
pb.set_message(format!("Processing {}", item.name));
process(item)?;
pb.inc(1);
}
pb.finish_with_message("Complete!");
```
### When to Use Spinners
- Unknown duration operations
- Waiting for external resources
- Operations < 2 seconds
- Indeterminate progress
```rust
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")?
);
spinner.set_message("Connecting to server...");
// Do work
spinner.finish_with_message("Connected!");
```
## Interactive Prompt Patterns
### When to Prompt vs When to Fail
**Prompt when:**
- Optional information for better UX
- Choosing from known options
- Confirmation for destructive operations
- First-time setup/initialization
**Fail with error when:**
- Required information
- Non-interactive environment (CI/CD)
- Piped input/output
- --yes flag provided
```rust
use dialoguer::Confirm;
fn delete_resource(name: &str, force: bool) -> Result<()> {
if !force && atty::is(atty::Stream::Stdin) {
let confirmed = Confirm::new()
.with_prompt(format!("Delete {}? This cannot be undone", name))
.default(false)
.interact()?;
if !confirmed {
println!("Cancelled");
return Ok(());
}
}
// Perform deletion
Ok(())
}
```
### Smart Defaults
```rust
use dialoguer::Input;
fn get_project_name(current_dir: &Path) -> Result<String> {
let default = current_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("my-project");
Input::new()
.with_prompt("Project name")
.default(default.to_string())
.interact_text()
}
```
## Output Formatting Patterns
### Human-Readable vs Machine-Readable
```rust
#[derive(Parser)]
struct Cli {
#[arg(long)]
json: bool,
#[arg(short, long)]
verbose: bool,
}
fn print_results(results: &[Item], cli: &Cli) {
if cli.json {
// Machine-readable
println!("{}", serde_json::to_string_pretty(&results).unwrap());
} else {
// Human-readable
for item in results {
println!("{} {} - {}",
if item.active { "".green() } else { "".red() },
item.name.bold(),
item.description.dimmed()
);
}
}
}
```
### Table Output
```rust
use comfy_table::{Table, Cell, Color};
fn print_table(items: &[Item]) {
let mut table = Table::new();
table.set_header(vec!["Name", "Status", "Created"]);
for item in items {
let status_color = if item.active { Color::Green } else { Color::Red };
table.add_row(vec![
Cell::new(&item.name),
Cell::new(&item.status).fg(status_color),
Cell::new(&item.created),
]);
}
println!("{table}");
}
```
## Verbosity Patterns
### Progressive Disclosure
```rust
fn log_message(level: u8, quiet: bool, message: &str) {
match (level, quiet) {
(_, true) => {}, // Quiet mode: no output
(0, false) => {}, // Default: only errors
(1, false) => println!("{}", message), // -v: basic info
(2, false) => println!("INFO: {}", message), // -vv: detailed
_ => println!("[DEBUG] {}", message), // -vvv: everything
}
}
```
### Quiet Mode
```rust
#[derive(Parser)]
struct Cli {
#[arg(short, long)]
quiet: bool,
#[arg(short, long, action = ArgAction::Count, conflicts_with = "quiet")]
verbose: u8,
}
```
## Confirmation Patterns
### Destructive Operations
```rust
// Always require confirmation for:
// - Deleting data
// - Overwriting files
// - Production deployments
// - Irreversible operations
fn deploy_to_production(force: bool) -> Result<()> {
if !force {
println!("{}", "WARNING: Deploying to PRODUCTION".red().bold());
println!("This will affect live users.");
let confirmed = Confirm::new()
.with_prompt("Are you absolutely sure?")
.default(false)
.interact()?;
if !confirmed {
return Ok(());
}
}
// Deploy
Ok(())
}
```
## Stdout vs Stderr
### Best Practices
- **stdout** - Program output, data, results
- **stderr** - Errors, warnings, progress, diagnostics
```rust
// Correct usage
println!("result: {}", data); // stdout - actual output
eprintln!("Error: {}", error); // stderr - error message
eprintln!("Processing..."); // stderr - progress update
// This allows piping output while seeing progress:
// myapp process file.txt | other_command
// (progress messages don't interfere with piped data)
```
## Accessibility Considerations
### Screen Reader Friendly
```rust
// Always include text prefixes, not just symbols
fn print_status(level: Level, message: &str) {
let (symbol, prefix) = match level {
Level::Success => ("", "SUCCESS:"),
Level::Error => ("", "ERROR:"),
Level::Warning => ("", "WARNING:"),
Level::Info => ("", "INFO:"),
};
// Both symbol and text for accessibility
println!("{} {} {}", symbol, prefix, message);
}
```
### Color Blindness Considerations
- Don't rely on color alone
- Use symbols/icons with colors
- Test with color blindness simulators
- Provide text alternatives
## The 12-Factor CLI Principles
1. **Great help** - Comprehensive, discoverable
2. **Prefer flags to args** - More explicit
3. **Respect POSIX** - Follow conventions
4. **Use stdout for output** - Enable piping
5. **Use stderr for messaging** - Keep output clean
6. **Handle signals** - Respond to Ctrl+C gracefully
7. **Be quiet by default** - User controls verbosity
8. **Fail fast** - Validate early
9. **Support --help and --version** - Always
10. **Be explicit** - Avoid surprising behavior
11. **Be consistent** - Follow patterns
12. **Make it easy** - Good defaults, clear errors
## References
- [CLI Guidelines](https://clig.dev/)
- [12 Factor CLI Apps](https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46)
- [NO_COLOR](https://no-color.org/)
- [Human-First CLI Design](https://uxdesign.cc/human-first-cli-design-principles-b2b4b4e7e7c1)