CLI Best Practices
Rules for Rust CLIs that are discoverable, scriptable, and respectful of user time.
How to Use This List
- Apply when designing new commands or reviewing CLI PRs
- Every rule supports either human usability or script automation
- Add
--helpexamples for every subcommand
A - Interface Design
- Single clear purpose. One tool, one job. Split unrelated functions into separate binaries.
- Subcommands for verbs.
myapp deploy, notmyapp --deploy. - Consistent flag names.
--verbose/-vacross all subcommands. - Meaningful help text. Every argument documented with an example.
B - Scriptability
- Meaningful exit codes. 0 success, distinct codes for distinct failures.
- stdout for data, stderr for logs. Output stays pipeable.
-
--jsonor--plainflag. Machine-readable output option. - Non-interactive mode.
--yes/--no-inputfor CI and scripts.
C - Configuration
- Flags override env vars override config file. Document precedence.
- Secrets via environment only. Never in config files committed to git.
- Sensible defaults. Common case works with zero flags.
- Validate early. Fail fast on bad config before work starts.
D - User Experience
- Progress for long operations. Spinner or progress bar on stderr.
- Confirm destructive actions. Default No on delete/remove.
- Actionable error messages. What failed, why, and how to fix.
- Shell completion. Generated from clap and documented in README.
E - Distribution
- Thin main, fat lib. Logic in
lib.rsfor testability. - Pinned toolchain.
rust-toolchain.tomlfor reproducible builds. - Semver for CLI changes. Breaking flag changes bump MAJOR.
- README with quickstart. Install, first command, common examples.
FAQs
How many subcommands?
As many as needed, but each should be discoverable via --help.
Should CLIs be interactive by default?
No. Interactive only when explicitly requested or no args provided for a wizard.
clap or manual parsing?
clap for anything beyond a single positional argument.
How do I deprecate a flag?
Warning on use, document in help, remove in next major version.
Logging level flag?
-v once = INFO, twice = DEBUG. Log to stderr.
How do I handle passwords?
Env var or hidden prompt. Never argv flags.
CLI testing strategy?
Unit test lib.rs. Integration tests call Cli::parse_from and assert exit codes.
Version flag?
--version on every tool. Consistent across your CLI suite.
Color output?
Default auto. Support --no-color and NO_COLOR=1.
How long should --help be?
Concise per-flag. Link to docs for long examples.
Related
- CLI Basics - fundamentals
- clap - argument parsing
- Error Reporting - user-facing errors
- Packaging & Distribution - shipping binaries
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+.