Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
81: Building a Command-Line Tool in Rust
When I first started writing CLI tools in Rust, I had this romantic notion that "the Rust way" meant doing everything from scratch using the standard library. I thought that if I relied on a crate for basic argument parsing, I was cheating or adding unnecessary bloat. I spent hours writing fragile match statements over std::env::args(), only to realize I'd basically written a buggy, worse version of something that already exists.
Thinking std::env::args() is enough for a "real" tool
The misconception is that manual argument parsing is "leaner" and more "authentic." Let me show you why that fails almost immediately. Imagine we're building a tool called rgrep that searches for a string in a file. You might start like this:
use std::env;
fn main() {
let args: Vec = env::args().collect();
if args.len() < 3 {
eprintln!("Usage: rgrep <query> <filename>");
std::process::exit(1);
}
let query = &args[1];
let filename = &args[2];
// ... logic to search the file
}
This looks fine for five minutes. But what happens when you want to add a --case-insensitive flag? Or a -v for verbose mode? Suddenly, you're writing a loop to iterate through the Vec, manually tracking indices, and handling edge cases where a user puts the flag at the end instead of the beginning. It's tedious, error-prone, and—most importantly—it provides a terrible user experience because you have to manually write every single help message and error string.
Leveraging clap for type-safe, declarative interfaces
In the professional world, we use clap. It's the gold standard for a reason. Instead of treating arguments as a list of strings that you have to guess the meaning of, clap allows you to define your interface as a Rust struct. This turns your CLI definition into a single source of truth.
Here is how I would actually structure the rgrep tool using the derive feature of clap:
use clap::Parser;
use std::fs;
#[derive(Parser, Debug)]
#[command(author, version, about = "A simple grep clone in Rust")]
struct Args {
/// The string to search for
query: String,
/// The path to the file to read
path: std::path::PathBuf,
/// Search case-insensitively
#[arg(short, long)]
ignore_case: bool,
}
fn main() {
let args = Args::parse();
let content = fs::read_to_string(&args.path)
.expect("Should have been able to read the file");
for line in content.lines() {
let matches = if args.ignore_case {
line.to_lowercase().contains(&args.query.to_lowercase())
} else {
line.contains(&args.query)
};
if matches {
println!("{}", line);
}
}
}
Notice the difference. I didn't write a single line of code to handle --help or versioning; clap generates those automatically based on the doc comments. I'm using PathBuf instead of a String for the path, which is the correct type for filesystem operations. The logic is now decoupled from the parsing.
Bridging the gap between arguments and the filesystem
When you build a CLI tool, your biggest enemy isn't the logic—it's the "outside world." Files aren't always there, permissions are often denied, and users type things you don't expect. I always recommend two things when moving from parsing to execution:
- Use
eprintln!for errors: Standard output (stdout) is for the actual data your tool produces. Error messages should go to standard error (stderr). This allows users to pipe your tool's output to another file without mixing in the error messages. - Fail fast: Don't wrap your entire
mainfunction in a giantmatch. Use the?operator by changing yourmainreturn type toResult<(), Box<dyn std::error::Error>>. This makes the flow much cleaner.
Building a CLI tool in Rust is incredibly satisfying because the type system ensures that once you get past the argument parsing, the actual "work" of the program is remarkably stable. You stop worrying about null pointers or index-out-of-bounds and start focusing on the actual utility of the tool.
📋 Practical Task
Build the "Log-File Line Counter" Utility
Your task is to create a CLI tool called log-count. This tool should take a path to a log file and an optional "filter" string. If the filter is provided, the tool should only count lines that contain that specific string. If no filter is provided, it should count all lines in the file.
Requirements:
- Use the
clapcrate with thederivefeature for argument parsing. - The tool must accept one mandatory positional argument:
path(as aPathBuf). - The tool must accept one optional flag/option:
--filter(or-f) which takes aString. - The program should print the final count to
stdout. - If the file cannot be read, print a descriptive error message to
stderrand exit with a non-zero status code.
There are no comments for now.