Build the mental model
This project is the capstone that pulls together the Basic and Intermediate chapters — parsing a command-line argument (a directory path) from `std::env::args()` (Lesson 5's control flow), reading each file in that directory with `Result<String, io::Error>` (Lesson 11's error handling), accumulating `LessonStat` structs (Lesson 9's struct, Lesson 12's `Vec`), and finally printing a report — four layers, each designed deliberately. Error-handling strategy matters most in a real CLI tool: if one file can't be read (a permission issue, corruption), you don't want the whole program to crash — skipping just that file and logging the error is the practical application of Lesson 20's "external input error → handle gracefully" strategy; a `main` function full of `.unwrap()` calls would let one bad file abort the entire batch. Managing ownership efficiently means reading file content as an owned `String` and then passing `count_words(&content)` (Lesson 7's borrow) — moving the content into that function would make it impossible to also extract an excerpt from the same content afterward. Only by actually running this project do Lessons 6 through 13's theory click into place inside one production-shaped tool.
Connect it to a real scenario
In `fn main()`, parse the directory path argument with `std::env::args().nth(1)` — if no argument is given, print a usage message and exit via `std::process::exit(1)`. Get the file list with `std::fs::read_dir(path)?`, and for each file use the pattern `match std::fs::read_to_string(entry.path()) { Ok(content) => { ... }, Err(e) => eprintln!("skip {path}: {e}") }`, so the rest of the files keep processing even if one fails. Build a `LessonStat::new(file_name, count_words(&content))` struct per file and push it into a `Vec<LessonStat>` — once done, build a `HashMap<String, u32>` (difficulty label to count) and print it as a summary report.
Try the working example
use std::collections::HashMap;
use std::fs;
struct LessonStat { name: String, word_count: u32 }
fn count_words(text: &str) -> u32 {
text.split_whitespace().count() as u32
}
fn main() {
let dir = std::env::args().nth(1).unwrap_or_else(|| ".".to_string());
let mut stats: Vec<LessonStat> = Vec::new();
if let Ok(entries) = fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
match fs::read_to_string(&path) {
Ok(content) => {
let name = path.display().to_string();
stats.push(LessonStat { name, word_count: count_words(&content) });
}
Err(e) => eprintln!("skip {}: {e}", path.display()),
}
}
}
let mut total_by_bucket: HashMap<&str, u32> = HashMap::new();
for stat in &stats {
let bucket = if stat.word_count < 500 { "short" } else { "long" };
*total_by_bucket.entry(bucket).or_insert(0) += 1;
}
println!("{total_by_bucket:?}");
}Given a directory argument, you print a word-count bucket summary (`{"short": N, "long": M}`) across every file in it.5-minute try-it
Extend the analyzer above to also track each file's longest line and add "longest line overall" to the final report — also report how many files failed to read.
One important caution
Using `fs::read_to_string(&path).unwrap()` inside the file-reading loop — one bad file (a permission issue, corrupted encoding) can crash the entire batch; the Err arm needs graceful handling.
Assuming the directory argument will always be provided and calling `args().nth(1).unwrap()` directly — missing the argument panics instead of giving the user a helpful error message.