Reference: A CLI Tool Shipped to Users
A reference for user-facing CLI tools: clap 4 for parsing, structured errors, cross-platform builds with cargo-dist, and support-friendly --version output with git SHA and Rust toolchain metadata.
Recipe
Quick-reference recipe card - copy-paste ready.
acme-cli/
src/
main.rs
cmd/ # subcommands
error.rs
tests/
cli_integration.rs
dist.toml # cargo-dist manifest
CHANGELOG.mdWhen to reach for this:
- Shipping developer or operator tooling
- Replacing bash scripts with typed Rust
- Enterprise customers require SBOM/signed binaries
Working Example
use clap::{Parser, Subcommand};
use std::process::ExitCode;
#[derive(Parser)]
#[command(name = "acme", version, about = "Acme operator CLI")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Export tenant data to JSON
Export { tenant_id: String, #[arg(long)] out: std::path::PathBuf },
}
fn main() -> ExitCode {
let cli = Cli::parse();
match run(cli) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
e.exit_code()
}
}
}
fn run(cli: Cli) -> Result<(), AppError> {
match cli.command {
Commands::Export { tenant_id, out } => export(&tenant_id, &out)?,
}
Ok(())
}# cargo-dist release (simplified)
cargo dist build --artifacts=archives
cargo dist publish --tag v1.2.0// tests/cli_integration.rs
use assert_cmd::Command;
#[test]
fn export_requires_tenant() {
Command::cargo_bin("acme").unwrap()
.arg("export")
.assert()
.failure();
}Deep Dive
UX Conventions
- Subcommands over flags soup for >3 operations.
- Stable JSON output flag
--format jsonfor automation. - Progress on stderr; data on stdout.
- Document exit codes: 0 ok, 1 user error, 2 internal.
Distribution
- cargo-dist for Linux/macOS/Windows matrix.
- Sign binaries where policy requires.
- Homebrew tap or package manager recipes as optional channel.
Support
acme --version includes semver, SHA, rustc version for ticket correlation.
Gotchas
- Breaking CLI flags without semver - Scripts break. Fix: Deprecate one release; major bump on removal.
- Panics on bad input - Fix:
Resulteverywhere inrun(). - Huge binary - Fix:
strip, LTO, avoid unused features. - No integration tests - Regressions ship. Fix:
assert_cmdsmoke tests in CI.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| xtask crate | Repo maintainer tasks only | End-user product |
| Shell wrapper | Legacy install path | Complex validation needed |
| TUI (ratatui) | Interactive ops | CI automation primary |
FAQs
MSRV for public CLI?
State in README; align with enterprise LTS if applicable.
Auto-update channel?
Separate from server API; document signature verification.
Windows support?
Test on CI matrix; path and line ending edge cases.
Config file + env?
clap env overrides; document precedence in --help.
Telemetry in CLI?
Opt-in only; document in privacy notice.
Related
- Clap - CLI parsing
- CLI Tools Best Practices - patterns
- Release Management - versioning
- Supply Chain Security - SBOM
- Testing Best Practices - integration tests
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.