File I/O
Prerequisites: Shared state: Arc & Mutex
File operations live in std::fs (whole-file helpers and the File type) and
std::io (reading/writing traits and buffering). Every operation that touches
the file system can fail, so they all return io::Result<T>, which is short
for Result<T, io::Error>. That makes ? the natural way to write file code.
Syntax
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
fn demo() -> io::Result<()> {
fs::write("notes.txt", "first line\n")?; // create or overwrite
let text = fs::read_to_string("notes.txt")?; // whole file as a String
let mut file = OpenOptions::new().append(true).open("notes.txt")?;
writeln!(file, "second line")?; // append with formatting
let reader = BufReader::new(File::open("notes.txt")?);
for line in reader.lines() { // one line at a time
let line = line?; // each line is an io::Result<String>
println!("{line}");
}
print!("{text}");
fs::remove_file("notes.txt")?;
Ok(())
}
fn main() {
demo().unwrap();
}
fs::read_to_stringandfs::writeare the simplest choice for small files.- For large files, wrap a
Filein aBufReaderand read line by line, so the whole file never sits in memory at once..lines()strips the line endings. File::create(path)opens a file for writing (truncating it);OpenOptionslets you choose append, create, truncate and so on.- Writing with
write!/writeln!needs theWritetrait in scope; reading lines needsBufRead. - Files are closed automatically when the
Filevalue is dropped.
Example
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, BufWriter, ErrorKind, Write};
fn write_report(path: &str) -> io::Result<()> {
let file = File::create(path)?;
let mut out = BufWriter::new(file); // batches many small writes into few system calls
writeln!(out, "name,score")?;
for (name, score) in [("ada", 92), ("grace", 88), ("linus", 75)] {
writeln!(out, "{name},{score}")?;
}
out.flush()?; // push anything still buffered to the file
Ok(())
}
fn average_score(path: &str) -> io::Result<f64> {
let reader = BufReader::new(File::open(path)?);
let mut total = 0.0;
let mut count = 0;
for line in reader.lines().skip(1) {
let line = line?;
if let Some((_, score)) = line.split_once(',') {
let score: f64 = score
.trim()
.parse()
.map_err(|_| io::Error::new(ErrorKind::InvalidData, format!("bad score in '{line}'")))?;
total += score;
count += 1;
}
}
Ok(total / count as f64)
}
fn main() -> io::Result<()> {
let path = "scores.csv";
write_report(path)?;
let contents = fs::read_to_string(path)?;
println!("file has {} lines", contents.lines().count());
println!("average: {:.1}", average_score(path)?);
match fs::read_to_string("does-not-exist.txt") {
Ok(_) => println!("unexpectedly found it"),
Err(e) if e.kind() == ErrorKind::NotFound => println!("missing file handled"),
Err(e) => return Err(e),
}
fs::remove_file(path)?;
println!("cleaned up: {}", !fs::exists(path)?);
Ok(())
}
Expected output
file has 4 lines
average: 85.0
missing file handled
cleaned up: true
Relative paths such as "scores.csv" are resolved against the current
working directory — the directory you ran cargo run from, not the location
of the source file. For portable paths built from parts, use
std::path::Path and PathBuf (Path::new("data").join("scores.csv")).
Common mistakes
- Calling
writeln!on a file without importingstd::io::Write:error[E0599]: cannot write into `File`with a hint to adduse std::io::Write;. The same applies toBufReadfor.lines(). - Using
?in amainthat returns(). Declarefn main() -> io::Result<()>(orResult<(), Box<dyn Error>>) and end withOk(()). - Forgetting that
File::createtruncates an existing file. UseOpenOptions::new().append(true)to add to the end instead. - Forgetting to handle each line's
Result:for line in reader.lines()yieldsio::Result<String>, so writelet line = line?;before using it. - Reading a huge file with
read_to_string. Stream it withBufReaderinstead. - Assuming a
BufWriterhas written everything before you read the file back. Callflush()(or drop the writer) first.
Practice
Write a program that saves three lines of text to todo.txt, appends a
fourth line with OpenOptions, then reads the file back with a BufReader
and prints each line numbered from 1. Delete the file at the end.
Show solution
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
fn main() -> io::Result<()> {
let path = "todo.txt";
fs::write(path, "buy milk\nwrite tests\nread chapter 16\n")?;
let mut file = OpenOptions::new().append(true).open(path)?;
writeln!(file, "go for a walk")?;
drop(file); // close before reading
let reader = BufReader::new(File::open(path)?);
for (i, line) in reader.lines().enumerate() {
println!("{}. {}", i + 1, line?);
}
fs::remove_file(path)?;
Ok(())
}
1. buy milk
2. write tests
3. read chapter 16
4. go for a walk