Every file this course has ever touched — hello.txt, username.txt, count.txt — has had its name written directly into the source code, a string literal sitting right there in fs::read_to_string(...). That's about to change. Two new sources of input today — the command line and the environment — plus the half of file I/O that hasn't come up yet: writing.
Command-line arguments: std::env::args
std::env::args() returns every argument the program was started with, as an iterator of Strings. And the very first thing this lesson does with that iterator is the very last thing the previous one taught:
env::args() — an iterator — straight into .collect(), exactly where the iterators lesson left off, producing a Vec<String>. Run this with cargo run -- count.txt, and the printed vector has two elements: the path to your compiled binary, always present as element 0, and "count.txt" right after it. Whatever you type after -- on the command line shows up here, in order, as plain strings — yours to do with as you like.
A real path, from outside the program
Here's read_to_string again — but this time, the path isn't a literal. It's args[1], and the rest of the function is exactly the shape the propagating-errors lesson and the traits lesson already built:
main() -> Result<(), Box<dyn Error>> — the exact signature from two lessons you've already finished — is what lets that ? after read_to_string(path) work at all. Run this without a second argument, and args[1] panics: index out of bounds, the exact same panic as the very first one this course ever showed you, back in the panic-vs-Result lesson. Same Vec, same indexing operator, same panic — except this time the index came from how someone ran your program, not from a number you typed into the source yourself.
Environment variables: std::env::var
Arguments are typed fresh every run. Environment variables are ambient — set once in your shell, read by every program that asks. std::env::var asks:
env::var(name) returns a Result — Ok(value) if a variable by that name is set, Err(...) if it isn't. Nothing here is new: it's match on a Result, exactly like every Ok/Err pair since the panic-vs-Result lesson, with Err(_) using the wildcard pattern from the pattern-matching lesson because — for this purpose — why the variable isn't set doesn't matter, only that it isn't.
Writing files: fs::write
Every fs call so far has been a read. Here's the other half — and notice it's shaped like a mirror image:
fs::write(path, contents) creates the file if it doesn't exist, overwrites it completely if it does, and writes contents in one shot — no separate "open" step. std::io::Result<()> is the same io::Error from the custom-error-types lesson, just spelled as the type alias the standard library itself uses; ? works on it for the same reason it always has. Two calls — write, then read the same path right back — are enough to prove to yourself that what landed on disk is exactly what you sent.
A safer default, the Option way
Indexing args[1] panics if it's missing. The vectors lesson's answer to that was .get(), returning an Option instead of panicking — and Option has a method that should look very familiar after last lesson:
args.get(1) is Option<&String> — Some if a second argument was given, None if not. .map(String::as_str) is Iterator::map's sibling on Option: where Iterator::map transforms each item a sequence produces, Option::map transforms the one item a container holds — if it holds one — and leaves None alone. String::as_str, passed by name, is the same "function used as a value" trick as or_insert_with(Vec::new) and .map_err(CountError::Io) from a few lessons back. And .unwrap_or("count.txt") is .unwrap_or_else(|| String::from("Anonymous"))'s plainer cousin from the closures lesson: unwrap_or_else takes a closure because building its default took real work; unwrap_or takes the value directly, because "count.txt" is just a literal — there's nothing to delay.
Quick exercise
- Run the first code block in this lesson with
cargo run -- hello world. Count the elements in the printed vector, and confirm element0is your binary's path, not"hello". - Take
read_count_from_filefrom the custom-error-types lesson. Instead of always reading"count.txt", collectstd::env::args()into aVec<String>and use the exact pattern from the last code block —args.get(1).map(String::as_str).unwrap_or("count.txt")— to pick the path: whatever was passed on the command line, or"count.txt"if nothing was.
Six lessons ago, or_insert_with(Vec::new) and .map_err(CountError::Io) were "functions passed as values." Two lessons ago, they became closures' quiet cousins. Today, String::as_str joined them, and Option::map turned out to be Iterator::map wearing a smaller hat — every container in this course, from Vec to Option to Result, keeps turning out to share the same handful of moves. env::args, env::var, and fs::write are three new functions; nothing about using them was new at all.
There's one more shape of "code someone else wrote" left to meet. Lesson 4 added rand to Cargo.toml and called rand::thread_rng().gen_range(...) — one external crate, used once, before derive, traits, or modules meant anything yet. The next lesson adds a second one: serde, and the realization that derive — the same #[derive(Debug)] you've written since the structs lesson — extends to crates you didn't write at all. A couple of attributes, and any struct in this course can turn itself into JSON and back — reading and writing not just text files, but structured ones: serde and JSON.