Reading Arguments and Files
Take a path from argv, read the file, and count words — with ownership showing up for the first time.
Now we make the program do something. It takes a file path, reads it, and prints counts.
Arguments
std::env::args gives you an iterator over the command-line arguments. The first one is
always the program name.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let Some(path) = args.get(1) else {
eprintln!("usage: wordcount <file>");
std::process::exit(1);
};
println!("counting {path}");
}
let ... else is the tool for “bind this or bail.” The else block must diverge — return,
break, panic, or exit — so after it, path is guaranteed to exist.
Note eprintln! for the usage message: errors belong on stderr so they don’t pollute a pipe.
Reading the file
use std::fs;
let contents = fs::read_to_string(path).expect("failed to read file");
read_to_string returns Result<String, io::Error>. .expect(...) unwraps it and panics
with your message on failure. That’s fine for now — part 2 replaces it with real error
handling.
Counting
fn count(contents: &str) -> (usize, usize, usize) {
let lines = contents.lines().count();
let words = contents.split_whitespace().count();
let bytes = contents.len();
(lines, words, bytes)
}
Look at the signature: &str, not String. The function only needs to read the text, so
it borrows. If it took String it would take ownership and the caller couldn’t use the value
afterward. Borrow by default; take ownership only when you need to keep something.
split_whitespace handles runs of spaces, tabs, and newlines correctly — don’t hand-roll it.
Putting it together
use std::{env, fs};
fn main() {
let args: Vec<String> = env::args().collect();
let Some(path) = args.get(1) else {
eprintln!("usage: wordcount <file>");
std::process::exit(1);
};
let contents = fs::read_to_string(path).expect("failed to read file");
let (lines, words, bytes) = count(&contents);
println!("{lines:>8} {words:>8} {bytes:>8} {path}");
}
fn count(contents: &str) -> (usize, usize, usize) {
(
contents.lines().count(),
contents.split_whitespace().count(),
contents.len(),
)
}
Try it:
cargo run -- src/main.rs
# 24 61 612 src/main.rs
The -- separates cargo’s own flags from your program’s arguments.
{lines:>8} right-aligns in an 8-character field, which is why the output lines up like
real wc.
A wrinkle worth noticing
contents.len() returns bytes, not characters. Rust strings are UTF-8, so a file with
emoji or accented letters reports more bytes than characters. If you want characters, use
contents.chars().count(). This distinction is deliberate — Rust refuses to let you pretend
that indexing a string by “character” is cheap.
Next: handling errors properly, and supporting more than one file.