Phase 5Practical project

#17 File I/O

std::fs, read, write

Reading files

The simplest way to read a file is std::fs::read_to_string(). It reads the entire file into a String:

Reading a whole file
use std::fs;

fn main() {
    match fs::read_to_string("hello.txt") {
        Ok(content) => println!("File content: {}", content),
        Err(e) => println!("Error reading file: {}", e),
    }
}

For more control, use File::open() with a BufReader to read line by line:

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

fn read_lines(path: &str) -> io::Result<Vec<String>> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let mut lines = Vec::new();

    for line in reader.lines() {
        lines.push(line?);
    }

    Ok(lines)
}

fn main() {
    match read_lines("data.txt") {
        Ok(lines) => {
            println!("Lines in file: {}", lines.len());
            for line in &lines {
                println!("  {}", line);
            }
        }
        Err(e) => println!("Error: {}", e),
    }
}

Writing files

Use fs::write() for a quick write, or File::create() for more control. Both will create the file if it does not exist and overwrite it if it does:

Writing to a file
use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    // Simple: write entire content at once
    fs::write("output.txt", "Hello, Rust!\nThis is line 2.")?;
    println!("Wrote to output.txt");

    // More control: use File::create
    let mut file = fs::File::create("formatted.txt")?;
    writeln!(file, "Name: {}", "Rust")?;
    writeln!(file, "Version: {}", "1.75")?;
    writeln!(file, "Year: {}", 2024)?;

    Ok(())
}

Appending to files

To add content to an existing file without overwriting, use OpenOptions:

Appending to a file
use std::fs::OpenOptions;
use std::io::Write;

fn append_log(message: &str) -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .create(true)    // Create if it does not exist
        .append(true)    // Append instead of overwrite
        .open("log.txt")?;

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

fn main() -> std::io::Result<()> {
    append_log("Application started")?;
    append_log("Processing data")?;
    append_log("Done")?;
    println!("Appended to log.txt");
    Ok(())
}

Working with paths

The std::path module provides Path and PathBuf for cross-platform path manipulation:

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

fn main() {
    let path = Path::new("src/main.rs");

    println!("Exists: {}", path.exists());
    println!("Is file: {}", path.is_file());
    println!("Extension: {:?}", path.extension());
    println!("File name: {:?}", path.file_name());
    println!("Parent: {:?}", path.parent());

    // Build paths dynamically with PathBuf
    let mut config_path = PathBuf::from("/home/user");
    config_path.push(".config");
    config_path.push("app");
    config_path.push("settings.toml");
    println!("Config: {}", config_path.display());
    // /home/user/.config/app/settings.toml
}

You can also create directories and check if paths exist:

Directory operations
use std::fs;

fn main() -> std::io::Result<()> {
    // Create a single directory
    fs::create_dir("output")?;

    // Create nested directories (like mkdir -p)
    fs::create_dir_all("data/raw/2024")?;

    // List directory contents
    for entry in fs::read_dir("src")? {
        let entry = entry?;
        println!("{}", entry.path().display());
    }

    // Remove a file
    fs::remove_file("temp.txt")?;

    // Remove a directory (must be empty)
    fs::remove_dir("output")?;

    Ok(())
}

Error handling for I/O

All I/O operations return io::Result<T>, which is an alias for Result<T, io::Error>. You can match on the error kind for fine-grained handling:

Handling I/O errors by kind
use std::fs;
use std::io::ErrorKind;

fn main() {
    match fs::read_to_string("config.toml") {
        Ok(content) => println!("Config: {}", content),
        Err(e) => match e.kind() {
            ErrorKind::NotFound => {
                println!("Config file not found, using defaults");
            }
            ErrorKind::PermissionDenied => {
                println!("Permission denied! Check file permissions.");
            }
            _ => {
                println!("Unexpected error: {}", e);
            }
        },
    }
}

Summary

Quick reference
use std::fs;
use std::io::Write;
use std::path::Path;

// Reading
fs::read_to_string("file.txt")?    // Read whole file
BufReader::new(file).lines()       // Read line by line

// Writing
fs::write("file.txt", content)?    // Write whole file
File::create("file.txt")?          // Create/overwrite
writeln!(file, "{}", data)?        // Write formatted

// Appending
OpenOptions::new().append(true).open("f")?

// Paths
Path::new("dir/file.rs")           // Create a path
PathBuf::from("/home").push("dir") // Build a path
path.exists() / is_file() / extension()

Your turn

Try running the file I/O demo with cargo run and verify with cargo check:

terminal — cargo
user@stemlegacy:~/file-io-demo$