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

Read & Write

Read a whole file into a string

std-badge tempfile-badge cat-filesystem-badge

fs::read_to_string opens the file, reads it to end, closes it, and returns a String — one call for the common case of loading a small text file in full. It sizes the buffer from the file’s length up front, so the read is a single allocation plus a single syscall loop.

For files large enough that loading them whole would waste memory, switch to a BufReader over File::open and stream lines or bytes. The cutoff is workload-specific; past a few megabytes, the streaming form is usually the right choice.

use std::fs;
use std::io;
use tempfile::tempdir;

fn main() -> io::Result<()> {
    let dir = tempdir()?;
    let path = dir.path().join("config.toml");
    fs::write(&path, "name = \"cookbook\"\nedition = 2021\n")?;

    // One call: open, read, close.
    let contents = fs::read_to_string(&path)?;
    assert!(contents.contains("cookbook"));
    println!("{contents}");
    Ok(())
}

Write a string to a file

std-badge tempfile-badge cat-filesystem-badge

fs::write is the mirror of fs::read_to_string: it opens the path, writes the bytes, and closes the file in one call. The semantics are create-or-truncate — if the file exists, its previous contents are discarded; if it doesn’t, it’s created with default permissions.

To append instead of replacing, build a File through OpenOptions with append set. To refuse to overwrite an existing file — useful when a stale file would be a bug — use create_new instead of create.

use std::fs;
use std::io;
use tempfile::tempdir;

fn main() -> io::Result<()> {
    let dir = tempdir()?;
    let path = dir.path().join("report.txt");

    // Creates the file if missing.
    fs::write(&path, "build ok\n")?;

    // Calling write again replaces — does not append to — the previous contents.
    fs::write(&path, "build ok\nemitted 12 warnings\n")?;

    let back = fs::read_to_string(&path)?;
    assert_eq!(back, "build ok\nemitted 12 warnings\n");
    Ok(())
}

Read lines of strings from a file

std-badge cat-filesystem-badge

Writes a three-line message to a file, then reads it back a line at a time with the Lines iterator created by BufRead::lines. File implements Read which provides BufReader trait. File::create opens a File for writing, File::open for reading.

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

fn main() -> Result<(), Error> {
    let path = "lines.txt";

    let mut output = File::create(path)?;
    write!(output, "Rust\n💖\nFun")?;

    let input = File::open(path)?;
    let buffered = BufReader::new(input);

    for line in buffered.lines() {
        println!("{}", line?);
    }

    Ok(())
}

Rename or atomically replace a file

std-badge tempfile-badge cat-filesystem-badge

fs::rename is the atomic-replace primitive when source and destination live on the same filesystem: on Unix it’s a single rename(2) syscall, on Windows it’s MoveFileExW with MOVEFILE_REPLACE_EXISTING. A concurrent reader either opens the old contents or the new — never a half-written file, never a window where the path is missing.

The write-to-temp-then-rename pattern below is how editors, package managers, and config writers avoid leaving a torn file behind if the process dies mid-write. The staging file and the final path must live on the same filesystem; across devices the kernel has no atomic move, and rename fails instead of silently degrading to a copy.

use std::fs;
use std::io;
use tempfile::tempdir;

fn main() -> io::Result<()> {
    let dir = tempdir()?;
    let final_path = dir.path().join("config.toml");
    let staging = dir.path().join("config.toml.new");

    // Write the new contents to a sibling path first...
    fs::write(&staging, "version = 2\n")?;

    // ...then swap it into place. Readers see either the old file or the
    // new one, never a partial write.
    fs::rename(&staging, &final_path)?;

    assert_eq!(fs::read_to_string(&final_path)?, "version = 2\n");
    assert!(!staging.exists());
    Ok(())
}

Avoid writing and reading from a same file

same_file-badge cat-filesystem-badge

Use same_file::Handle to a file that can be tested for equality with other handles. In this example, the handles of file to be read from and to be written to are tested for equality.

use same_file::Handle;
use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
use std::fs::File;
use std::path::Path;

fn main() -> Result<(), Error> {
    // Create a test file
    let mut file = File::create("new.txt")?;
    writeln!(file, "test content")?;
    
    let path_to_read = Path::new("new.txt");

    let stdout_handle = Handle::stdout()?;
    let handle = Handle::from_path(path_to_read)?;

    if stdout_handle == handle {
        return Err(Error::new(
            ErrorKind::Other,
            "You are reading and writing to the same file",
        ));
    } else {
        let file = File::open(&path_to_read)?;
        let file = BufReader::new(file);
        for (num, line) in file.lines().enumerate() {
            println!("{} : {}", num, line?.to_uppercase());
        }
    }
    Ok(())
}

Access a file randomly using a memory map

memmap-badge cat-filesystem-badge

Creates a memory map of a file using memmap and simulates some non-sequential reads from the file. Using a memory map means you just index into a slice rather than dealing with seeking around in a File.

The Mmap::map function assumes the file behind the memory map is not being modified at the same time by another process or else a race condition occurs.

use memmap::Mmap;
use std::fs::File;
use std::io::{Write, Error};

fn main() -> Result<(), Error> {
    write!(File::create("content.txt")?, "My hovercraft is full of eels!")?;

    let file = File::open("content.txt")?;
    let map = unsafe { Mmap::map(&file)? };

    let random_indexes = [0, 1, 2, 19, 22, 10, 11, 29];
    assert_eq!(&map[3..13], b"hovercraft");
    let random_bytes: Vec<u8> = random_indexes.iter()
        .map(|&idx| map[idx])
        .collect();
    assert_eq!(&random_bytes[..], b"My loaf!");
    Ok(())
}

Read a file line by line with BufReader

std-badge tempfile-badge cat-filesystem-badge

For files too large to load into memory at once, wrap a File in a BufReader and iterate with BufRead::lines. Unlike fs::read_to_string, this streams one kernel-buffered chunk at a time — typically 8 KB — so memory usage stays flat regardless of file size.

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

fn main() -> io::Result<()> {
    let dir = tempdir()?;
    let path = dir.path().join("large.txt");

    // Write some lines so the example is self-contained.
    let mut file = File::create(&path)?;
    writeln!(file, "line one")?;
    writeln!(file, "line two")?;
    writeln!(file, "line three")?;

    // BufReader wraps the File and adds an internal 8 KB buffer.
    let reader = BufReader::new(File::open(&path)?);
    for line in reader.lines() {
        println!("{}", line?);
    }
    Ok(())
}

Write to a file with BufWriter

std-badge tempfile-badge cat-filesystem-badge

BufWriter wraps any io::Write and batches small writes into a single in-memory buffer — typically 8 KB — before flushing to the OS. This reduces syscall overhead when making many small writes. Always call Write::flush explicitly when you are done; the buffer is also flushed when the BufWriter is dropped, but any error from that implicit flush is silently discarded.

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

fn main() -> io::Result<()> {
    let dir = tempdir()?;
    let path = dir.path().join("report.txt");

    let file = File::create(&path)?;
    let mut writer = BufWriter::new(file);

    // These writes go into an in-memory buffer, not directly to disk.
    writeln!(writer, "line one")?;
    writeln!(writer, "line two")?;
    writeln!(writer, "line three")?;

    // Explicit flush drains the buffer to the OS in one syscall.
    writer.flush()?;
    Ok(())
}

Read a line from stdin

std-badge cat-filesystem-badge

io::stdin returns a handle to the standard input stream. Calling Stdin::lock acquires an exclusive lock and gives access to BufRead::read_line, which appends characters — including the trailing newline — into the supplied String buffer. The lock is released when it goes out of scope.

use std::io::{self, BufRead};

fn main() -> io::Result<()> {
    let stdin = io::stdin();
    let mut line = String::new();

    // lock() gives exclusive BufRead access to stdin.
    stdin.lock().read_line(&mut line)?;

    // The buffer includes the trailing newline; trim before using.
    print!("you typed: {}", line.trim_end());
    Ok(())
}

Use Cursor as an in-memory buffer

std-badge cat-filesystem-badge

Cursor wraps an in-memory buffer and implements io::Read, io::Write, and io::Seek, making it a drop-in replacement for a File in tests or whenever bytes need to be assembled before deciding where to send them. The internal position advances with each read or write, so call Cursor::set_position to rewind before reading back what you wrote.

use std::io::{self, Cursor, Read, Write};

fn main() -> io::Result<()> {
    let mut buf = Cursor::new(Vec::<u8>::new());

    // Write into the buffer exactly as you would a File.
    write!(buf, "hello, ")?;
    write!(buf, "world")?;

    // Rewind to the start before reading back.
    buf.set_position(0);
    let mut out = String::new();
    buf.read_to_string(&mut out)?;

    assert_eq!(out, "hello, world");
    println!("{out}");
    Ok(())
}

Format text into a String with write!

std-badge cat-filesystem-badge

String implements fmt::Write, so the write! macro can push formatted text into it with no I/O and no file handle. Import fmt::Write — not io::Write — to bring the required trait into scope; the two traits share the macro name but operate on different targets. The String implementation never actually errors, but the signature uses ? for consistency with other writers.

use std::fmt::Write;

fn main() -> std::fmt::Result {
    let mut s = String::new();

    // write! on a String uses fmt::Write — no I/O, no file handle needed.
    write!(s, "Hello, {}!", "world")?;
    write!(s, " The answer is {}.", 42)?;

    assert_eq!(s, "Hello, world! The answer is 42.");
    println!("{s}");
    Ok(())
}