Reading Arguments and Files

Take a path from argv, read the file, and count words — with ownership showing up for the first time.

Part 2 of 3Updated

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.