20.xChapter 20 summary and quiz
This chapter turned your programs outward, into tools that read the world and compose with other programs. Review, then one more extension to minigrep.
Quick review
A program reads the words after its name with std::env::args (20.1), collected into a Vec<String>; index 0 is the program name, real arguments start at 1, and every argument arrives as text (use parse for numbers). Check the count before indexing, or [] panics.
Files come in two layers (20.2): fs::read_to_string and fs::write for whole files that fit in memory (both return Result), and File with BufReader/BufWriter for big files processed a piece at a time. Buffered writes must be flushed; an early exit before flushing loses them, the chapter-7 truncation warning made concrete.
Every program has three standard streams (20.3): stdin (input, io::stdin), stdout (results, println!), and stderr (diagnostics, eprintln!). Results go to stdout and everything else to stderr so output stays clean when piped or redirected. The exit code (0 success, non-zero failure) is set automatically by returning Result from main, which also prints errors to stderr.
Environment variables (20.4) configure persistent behavior and secrets, read with env::var (returns Result; pair with unwrap_or_else for a default or is_ok for a present/absent flag). Arguments are for per-run inputs; env vars are for set-once configuration.
The minigrep project (20.5, 20.6) assembled it all: arguments into a Config (with a build that returns Result), a thin main.rs over a lib.rs of logic (chapter 13), a search function written test-first (chapter 14), a case-insensitive mode from an environment variable, errors propagated with ? and Box<dyn Error> (chapter 12), and an iterator refactor (chapter 19).
Quiz time
Question #1
A program is run as ./tool --verbose input.txt. What does env::args().collect::<Vec<String>>() contain, element by element?
Show solution
["./tool", "--verbose", "input.txt"]. Index 0 is the program's invocation name/path, index 1 is "--verbose", index 2 is "input.txt". All three are Strings. Your real arguments are indices 1 and 2; interpreting --verbose as a flag and input.txt as a filename is your code's job.
Question #2
Why send results to stdout but errors to stderr? Give a concrete consequence of getting it wrong.
Show solution
So that redirecting or piping stdout captures only the real results while diagnostics still reach the terminal. Concrete failure: if a tool prints "Processing..." to stdout and a user runs tool > out.txt, that status line lands in out.txt and corrupts the data. Putting diagnostics on stderr (eprintln!) keeps out.txt clean while the message still shows on screen.
Question #3
In minigrep, the logic lives in lib.rs and main.rs is thin. Name two things that stayed in main.rs and explain why they belong there rather than in the library.
Show solution
Reading the real command-line arguments (env::args().collect()) and the error-reporting glue that calls process::exit (and eprintln!). These belong in main.rs because they're the hard-to-test interaction with the outside world, the actual process environment and termination, whereas everything testable (Config::build, search, run) lives in lib.rs where tests can call it directly (chapters 13 and 14).
Chapter extension
Extend minigrep: make it count matches and report the total to stderr, while still printing the matching lines to stdout. Running cargo run -- nobody poem.txt should print the two matching lines on stdout and a line like 2 matches on stderr.
Show solution
Modify run in src/lib.rs to collect the results, print each match to stdout, then report the count to stderr:
pub fn run(config: &Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(&config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in &results {
println!("{line}"); // matches -> stdout
}
eprintln!("{} matches", results.len()); // summary -> stderr
Ok(())
}$ cargo run -- nobody poem.txt
I'm nobody! Who are you?
Are you nobody, too?
2 matches
Running cargo run -- nobody poem.txt > out.txt puts only the two matching lines in out.txt, while 2 matches still appears in the terminal, the stdout/stderr separation (lesson 20.3) doing its job: the data is on stdout, the summary diagnostic on stderr. Borrowing &results in the loop lets results.len() still work afterward (chapter 9). This is exactly how a real tool reports a summary without polluting its output.
You can now write programs that read arguments, files, input, and configuration, and behave like first-class command-line citizens. That closes the "core Rust" arc of the course. Chapter 21 begins the advanced material with smart pointers, the types that let you reach for the heap deliberately, build recursive data structures, and bend the ownership rules when you genuinely need to, starting with the Box you've seen named in passing since chapter 12.