Command Line Applications in Rust (CLAiR)
repository·master·Indexed 21 days ago
https://github.com/rust-cli/bookA resource maintained by the Rust CLI working group for learning how to build command-line tools in Rust. The book covers everything from a beginner tutorial building a grep clone (grrs) to advanced topics including argument parsing with clap, configuration management with confy, error handling with anyhow, and user-friendly panic handling with human-panic.
What's inside CLAiR
- This book is a guide for learning how to build command line applications (CLIs) using Rust. It is designed to take beginners from a quick tutorial—resulting in a working CLI tool—to more advanced, detailed chapters covering core Rust concepts and specialized CLI application aspects. Rust is recommended for CLIs because it is statically compiled, fast, and produces small, portable, and quick-to-run binaries.
Optimize file reading with BufReader
masterThe basic implementation usingfs::read_to_string()reads the entire file into memory at once. For large files, this is inefficient. To optimize memory usage, usestd::io::BufReaderto read the file line-by-line instead of loading the whole content into a string.Best practices for deciding what to test
masterTo use testing time efficiently, follow these guidelines:
- Integration Tests: Focus on observable user behavior (e.g., does the program exit with the correct code? Does it print the expected output?). You do not need to cover every edge case here; use them for 'happy paths' and major functional flows.
- Unit Tests: Use these to cover specific edge cases and complex business logic.
- Avoid Testing Uncontrollable Output: Do not test the exact layout of auto-generated content like
--helptext. Instead, assert that specific required elements are present. - Advanced Techniques:
- Use
proptestif you find yourself writing many manual unit tests for complex input permutations. - Use a fuzzer if your program parses arbitrary files to find unexpected edge-case bugs.
- Use
Implement machine-friendly JSON output
masterWhen designing CLI tools for composition, providing a structured format like JSON is superior to plain text or TSV for complex data.
A common and effective pattern is to use Line-delimited JSON (also known as JSON Lines). Instead of emitting one large JSON array, emit one complete JSON object per message, each on a new line. This allows consumers to parse messages one by one as they are emitted (streaming), which is useful for long-running processes.
You can use the
serde_jsoncrate and itsjson!macro to construct these messages easily.// Example of emitting line-delimited JSON // Output: {"content":"Hello world","type":"message"} println!("{}", serde_json::json!({ "content": "Hello world", "type": "message" }));When to implement custom signal handling
masterBy default, the operating system handles signals (like
Ctrl+C) by terminating the process immediately. This is sufficient if your application does not require a graceful shutdown.You should implement custom signal handling if your application needs to perform cleanup tasks before exiting, such as:
- Properly closing network connections (sending 'goodbye' packets).
- Removing temporary files.
- Resetting system settings.
How unit tests and integration tests differ
masterThere are two primary ways to test application functionality:
- Unit Tests: Testing small, individual units of code (like single functions) in isolation. These are typically used to verify core logic without depending on external setup like CLI arguments.
- Integration Tests (Black Box Tests): Testing the final application from the outside, simulating how a user interacts with the complete program (e.g., running the CLI, providing files, and checking output).
Manage log severity using the `log` crate
masterTo allow users to control the amount of information displayed (via
--verboseflags or theRUST_LOGenvironment variable), use consistent log levels. Thelogcrate defines levels in increasing order of severity:tracedebuginfo(Recommended as the default level for informative output)warningerror
Ensure messages provide enough context to be useful when filtered (e.g., via
grep) without being excessively verbose.Model CLI arguments as a custom data type
masterInstead of manually parsing a list of strings, it is a best practice in Rust to represent CLI arguments as a custom
struct. This allows you to define the expected types (e.g.,Stringfor patterns orPathBuffor file paths) and requirements (e.g., which arguments are mandatory) upfront.Using
std::path::PathBufis recommended for file system paths as it provides cross-platform compatibility compared to a standardString.use std::path::PathBuf; struct Cli { pattern: String, path: PathBuf, }Design effective CLI progress and status messages
masterWhen an application is running normally, use informative and concise messages to tell a story about what the application is doing and how it impacts the user.
Key principles:
- Be consistent: Use the same prefixes and sentence structures to make logs easily skimmable.
- Avoid jargon: Do not use overly technical terms in standard logs; the application is not crashing, so users shouldn't feel the need to look up errors.
- Show progress: Use timelines, progress bars, or indicators for long-running actions so the user never feels the application is doing something mysterious.
=> Downloading repository index => Downloading packages...Handle repeated Ctrl+C presses
masterUsers often press
Ctrl+Cmultiple times if the application does not respond immediately to the first press.Recommended Behavior: If the application receives a second
Ctrl+Cwhile it is already attempting to handle the first one (e.g., during a graceful shutdown phase), it should quit immediately to avoid frustrating the user.Recommended distribution strategy for Rust tools
masterWhen distributing a new Rust-based command-line tool, do not rely on a single method. Instead, follow a tiered approach to maximize accessibility:
- Start with
cargo install: This is the easiest way for Rust users to get your tool. - Add binary releases: Provide pre-compiled binaries (e.g., via GitHub Releases) so users don't need to install the Rust toolchain.
- Distribute via system package managers: Finally, work on getting your tool into major package managers (like
brew,apt, etc.) to reach a wider audience.
- Start with
Configure Rust edition for the tutorial
masterThe tutorial is written for Rust 2018. To ensure compatibility with the code examples, you must use Rust version 1.31.0 or later and specify the 2018 edition in your
Cargo.tomlfile.[package] edition = "2018"