How to read a file line by line
Short answer: Wrap the file in a BufReader and iterate lines(): for line in BufReader::new(file).lines().
Each item is an io::Result<String> with the newline already stripped. A bare File can’t do this —
lines() is a method on the BufRead trait, and File does not implement it.
From a fresh project — cargo new read-demo, then replace the contents of src/main.rs with each block
below and run it with cargo run. The project name shows up only in cargo’s own output; nothing else on
this page depends on it. Each block writes the file it reads before reading it, so it runs on its own; in
your own code the file is already there and you delete that line.
Read it line by line
Section titled “Read it line by line”use std::fs;use std::io::{self, BufRead, BufReader};
fn main() -> io::Result<()> { fs::write("poem.txt", "Leaves are falling\nsoftly on the path\n")?; let file = fs::File::open("poem.txt")?; for line in BufReader::new(file).lines() { println!("{}", line?); } Ok(())}Leaves are fallingsoftly on the pathlines() yields one io::Result<String> per line, newline stripped, and stops at the end of the file.
The ? inside the loop turns a read error into an early return from main, the same way the whole-file
version does it; BufRead has to be in scope for the method to resolve. The path is relative to the
directory you run the program from, which under cargo run is the project root.
Why the BufReader
Section titled “Why the BufReader”File implements Read, but read_line and lines are methods on BufRead, and File does not
implement that trait. Calling one directly is a compile error, not a runtime surprise:
use std::fs::File;
fn main() { let mut file = File::open("poem.txt").unwrap(); let mut line = String::new(); file.read_line(&mut line).unwrap();}error[E0599]: no method named `read_line` found for struct `File` in the current scope --> src/main.rs:6:10 | 6 | file.read_line(&mut line).unwrap(); | ^^^^^^^^^ |help: there is a method `read_at` with a similar name, but with different arguments --> /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/os/unix/fs.rs:61:5 |61 | fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize>; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |For more information about this error, try `rustc --explain E0599`.The read_at suggestion is a red herring — it’s a Unix positional read, not a line reader. Importing
BufRead doesn’t fix the call either, because the trait has to be implemented for the type, not just in
scope. The reason is buffering: File is unbuffered, so every read is a system call, and reading line
by line straight from it would mean a syscall per read. The standard library doesn’t offer it — the
line-splitting methods sit on the buffered reader instead. BufReader wraps any Read and reads ahead in
8 KiB chunks (its default capacity), keeping the bytes in memory until you consume them. A line longer
than the buffer is fine: read_line keeps refilling until it finds the newline.
The edges
Section titled “The edges”use std::fs;use std::io::{self, BufRead, BufReader};
fn main() -> io::Result<()> { // Three lines, Windows line endings, and no newline after the last one. fs::write("crlf.txt", "one\r\ntwo\r\nthree")?;
for line in BufReader::new(fs::File::open("crlf.txt")?).lines() { println!("[{}]", line?); }
let count = BufReader::new(fs::File::open("crlf.txt")?).lines().count(); println!("{count} lines"); Ok(())}[one][two][three]3 lineslines() strips \r\n as well as \n, so Windows files come out clean. A last line with no newline is
still yielded, and a trailing newline does not produce an empty line at the end — the count matches the
lines an editor shows. When you only need the number, .count() gives it without collecting them.
A line that isn’t valid UTF-8 fails with the same InvalidData kind the whole-file version gives, one
line at a time — and because the error arrives per line, the loop decides what to do with it:
use std::fs;use std::io::{self, BufRead, BufReader};
fn main() { // "café" in Latin-1: 0xE9 on its own is not valid UTF-8. fs::write("latin1.txt", [b'c', b'a', b'f', 0xE9, b'\n']).unwrap(); for line in BufReader::new(fs::File::open("latin1.txt").unwrap()).lines() { match line { Ok(text) => println!("{text}"), Err(err) => println!("{err:?}"), } }}Error { kind: InvalidData, message: "stream did not contain valid UTF-8" }? would abandon the whole loop on the first bad line; match lets you skip it, count it, or keep the
good lines. The guarantee behind the error is on the UTF-8 page.
Related
Section titled “Related”- How to read a file into a String — the whole file in one call, and the UTF-8 check behind both pages
- How to convert a Vec of bytes to a String — once the file turns out not to be text
- Reading and writing files — the rest of the chapter