RustArena

Step 4 of 6

Reading files

Most tools work on files. std::fs has the functions you'll use every day:

use std::fs;
use std::io::ErrorKind;

fn main() {
    fs::write("notes.txt", "buy milk\nfeed the crab\n").expect("could not write notes.txt");
    let text = fs::read_to_string("notes.txt").expect("could not read notes.txt");
    println!("{} lines", text.lines().count());

    match fs::read_to_string("missing.txt") {
        Ok(t) => println!("read {} bytes", t.len()),
        Err(e) if e.kind() == ErrorKind::NotFound => println!("missing.txt does not exist"),
        Err(e) => println!("error: {e}"),
    }
}
2 lines
missing.txt does not exist

Piece by piece:

Big files: line by line

read_to_string loads the whole file into memory. That's fine for configs and most inputs; for a 20 GB log, read it line by line with a BufReader, which keeps only a buffer in memory:

use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() -> std::io::Result<()> {
    std::fs::write("app.log", "INFO start\nERROR disk full\nINFO stop\nERROR timeout\n")?;
    let reader = BufReader::new(File::open("app.log")?);
    for (i, line) in reader.lines().enumerate() {
        let line = line?;
        if line.starts_with("ERROR") {
            println!("{}: {line}", i + 1);
        }
    }
    Ok(())
}
2: ERROR disk full
4: ERROR timeout

Common mistakes

Your task

Write wc. For each file named in the arguments print <lines> <words> <bytes> <name> (words are whitespace-separated; bytes is the length of the text). If more than one file was given, end with a <lines> <words> <bytes> total line summing the files that could be read. A file that can't be read prints wc: <name>: <error> to stderr, the tool carries on with the other files, and exits with code 1 at the end. No arguments: usage: wc <file>... to stderr, exit code 2.

Challenge 2

Write head [-n N] <file>: print the first N lines of the file (default 10). A bad or missing N (-n x, -n at the end) or a missing file name prints a usage line to stderr and exits 2; a file that can't be read prints head: <name>: <error> to stderr and exits 1.

Challenge 3

Write colsum <file> <column>, a tiny CSV summarizer. The file's first line is a header of comma-separated column names; every other non-empty line has the same number of comma-separated fields. Print sum of <column>: <total> with two decimals. Errors go to stderr: wrong number of arguments exits 2; a file that can't be read, an unknown column (unknown column "x") or a value that isn't a number (line 3: "abc" is not a number) exits 1.

Previous: Exit codes and stderr Next: Reading stdin when no file is given