Read & Write
Read a whole file into a string
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
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
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
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
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
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
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
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
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
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!
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(())
}