Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

I/O: stdin and File Access

Goal of This Episode

Meet Rust’s I/O (input / output) methods and learn to read and write files.

Concept

We’ve learned to express file locations with Path and to work with string content — this episode covers actually reading and writing files.

stdin Revisited

In Chapter 1 we copied these three lines verbatim to read user input:

fn main() {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).expect("read failed");
    let name = input.trim();
}

You’ve since learned String, &mut, and .expect(), so this should all make sense now. What we haven’t mentioned: std::io::stdin() returns a Stdin struct, and read_line returns an io::Result<usize> — a type alias for Result<usize, io::Error> that many I/O functions in the standard library use.

The Simplest File Reading and Writing

fs::read_to_string’s parameter type is impl AsRef<Path> — the AsRef from Episode 1 pays off here. You can pass a &str, String, &Path, or PathBuf, no manual conversion needed.

Reading a whole file into a string:

use std::fs;

fn main() {
    let content = fs::read_to_string("hello.txt").expect("read failed");
    println!("{}", content);
}

Writing to a file (created if missing, overwritten if present):

use std::fs;

fn main() {
    fs::write("output.txt", "Hello, file!").expect("write failed");
}

These two functions are wonderfully simple, but read_to_string reads the entire file into memory at once — not a good fit for very large files.

File + BufReader: Reading Line by Line

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

fn main() {
    let file = File::open("data.txt").expect("open failed");
    let reader = BufReader::new(file);

    for line in reader.lines() {
        let line = line.expect("failed to read line");
        println!("{}", line);
    }
}

File::open opens the file, BufReader wraps it to provide a buffer, and .lines() reads line by line — each line is an io::Result<String>.

Writing to a File

use std::fs::File;
use std::io::Write;

fn main() {
    let mut file = File::create("output.txt").expect("failed to create");
    writeln!(file, "first line").expect("write failed");
    writeln!(file, "second line").expect("write failed");
}

File::create creates (or overwrites) a file, and writeln! is a lot like println!, just with the output going to the file instead of the screen. Using writeln! requires importing the Write trait.

Read, Write, BufRead

The standard library abstracts I/O with traits:

  • Read: things bytes can be read from (File, Stdin, TcpStream, etc.).
  • Write: things bytes can be written to (File, Stdout, TcpStream, etc.).
  • BufRead: buffered reading, providing lines() and other methods. BufReader can turn any Read into a BufRead.

That’s why many functions declare parameters as impl Read or impl Write — whether you pass a file, stdin, or a network connection, anything implementing the right trait works.

Example Code

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

fn main() -> io::Result<()> {
    // write a file
    let mut file = File::create("names.txt")?;
    writeln!(file, "Alice")?;
    writeln!(file, "Bob")?;
    writeln!(file, "Charlie")?;

    // read the whole file at once
    let all = fs::read_to_string("names.txt")?;
    println!("whole file:\n{}", all);

    // read line by line
    let file = File::open("names.txt")?;
    let reader = BufReader::new(file);
    for (i, line) in reader.lines().enumerate() {
        println!("line {}: {}", i + 1, line?);
    }

    Ok(())
}

Recap

  • io::Result<T> is a type alias for Result<T, io::Error>.
  • fs::read_to_string / fs::write: the simplest one-line read/write
  • File::open + BufReader: read big files line by line
  • File::create + writeln!: write line by line.
  • Read, Write, and BufRead are the core I/O traits, giving different sources (files, stdin, the network) one shared interface.