#File I/O

Rust provides safe, cross-platform file I/O through the std::fs and std::io modules. This chapter covers common patterns for reading, writing, and manipulating files.

#Reading Files

The simplest way to read an entire file:

use std::fs;

fn main() -> std::io::Result<()> {
    let contents = fs::read_to_string("input.txt")?;
    println!("File has {} lines", contents.lines().count());
    Ok(())
}

For binary files, use fs::read:

use std::fs;

let bytes = fs::read("image.png")?;
println!("File size: {} bytes", bytes.len());

#Reading Line by Line

For large files, read line by line to avoid loading everything into memory:

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

fn count_matching_lines(path: &str, pattern: &str) -> io::Result<usize> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let count = reader
        .lines()
        .filter_map(|line| line.ok())
        .filter(|line| line.contains(pattern))
        .count();
    Ok(count)
}

#Writing Files

Write a string to a file (creates or overwrites):

use std::fs;

fs::write("output.txt", "Hello, world!\n")?;

For more control, use File::create with a writer:

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

fn write_report(path: &str, data: &[(String, f64)]) -> io::Result<()> {
    let file = File::create(path)?;
    let mut writer = BufWriter::new(file);

    writeln!(writer, "Name,Score")?;
    for (name, score) in data {
        writeln!(writer, "{name},{score:.2}")?;
    }

    writer.flush()?;
    Ok(())
}

#Appending to Files

Open a file in append mode:

use std::fs::OpenOptions;
use std::io::Write;

fn append_log(path: &str, message: &str) -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)?;

    writeln!(file, "{message}")?;
    Ok(())
}

#Working with Paths

Use Path and PathBuf for cross-platform path manipulation:

use std::path::{Path, PathBuf};

let base = Path::new("/home/user/documents");
let file_path = base.join("reports").join("2024-q1.csv");

println!("Extension: {:?}", file_path.extension());
println!("Filename: {:?}", file_path.file_name());
println!("Parent: {:?}", file_path.parent());
println!("Exists: {}", file_path.exists());

#Directory Operations

List files in a directory:

use std::fs;

fn list_rust_files(directory: &str) -> std::io::Result<Vec<String>> {
    let mut files = Vec::new();

    for entry in fs::read_dir(directory)? {
        let entry = entry?;
        let path = entry.path();

        if path.extension().map_or(false, |extension| extension == "rs") {
            if let Some(name) = path.file_name() {
                files.push(name.to_string_lossy().to_string());
            }
        }
    }

    files.sort();
    Ok(files)
}

#Temporary Files

Use the tempfile crate for safe temporary files:

use std::io::Write;
use tempfile::NamedTempFile;

fn process_with_tempfile(data: &[u8]) -> std::io::Result<()> {
    let mut temp = NamedTempFile::new()?;
    temp.write_all(data)?;

    println!("Temp file at: {:?}", temp.path());

    // File is automatically deleted when `temp` is dropped
    Ok(())
}

#Summary

Use fs::read_to_string and fs::write for simple cases. For large files or streaming, use BufReader and BufWriter. Always prefer Path/PathBuf over raw strings for file paths to ensure cross-platform compatibility.