/commands/rust/read-file-line-by-line
rust read a file line by line (BufReader::lines and read_to_string)
- tool
- Rust
- versions
- Rust 1.0+, fs::read_to_string: Rust 1.26+
- os
- any
To read a file line by line in Rust, open it with File::open, wrap it in BufReader, and iterate over .lines(): each item is an io::Result<String> without the line ending. For small files, std::fs::read_to_string(path)? reads everything into one String, and .lines() on that string iterates without further I/O errors.
Both approaches
printf 'alpha\r\nbeta\n\ngamma' > input.txt
cat > lines.rs <<'EOF'
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader};
fn main() -> io::Result<()> {
// Streaming: constant memory, works for large files
let reader = BufReader::new(File::open("input.txt")?);
for (number, line) in reader.lines().enumerate() {
let line = line?;
println!("{:>2}: {:?}", number + 1, line);
}
// Whole file in memory: simplest for small files
let text = fs::read_to_string("input.txt")?;
let non_empty: Vec<&str> = text.lines().filter(|l| !l.is_empty()).collect();
println!("non-empty lines: {:?}", non_empty);
// A missing file is an io::Error, propagated by ?
match File::open("missing.txt") {
Ok(_) => println!("unexpected"),
Err(e) => println!("error kind: {:?}", e.kind()),
}
Ok(())
}
EOF
rustc --edition 2024 lines.rs && ./linesOutput
1: "alpha" 2: "beta" 3: "" 4: "gamma" non-empty lines: ["alpha", "beta", "gamma"] error kind: NotFound
Both methods strip \n and \r\n, so Windows line endings need no extra handling; the last line is returned even without a final newline, and an empty line comes back as an empty string.
Choosing a method
| Method | Item type | When to use |
|---|---|---|
| BufReader::new(file).lines() | io::Result<String> | Large files or streams; one line in memory at a time |
| fs::read_to_string(path)?.lines() | &str borrowed from one String | Small files; simplest code, a single error check |
| reader.read_line(&mut buf) | usize (bytes read) | Reuse one String buffer, keep the line ending |
| reader.split(b'\n') | io::Result<Vec<u8>> | Files that are not valid UTF-8 |
| io::stdin().lock().lines() | io::Result<String> | Read standard input line by line |
Pitfalls
?needs a Result-returning function. Using?insidefn main()without-> io::Result<()>(or anotherResult) fails to compile with E0277. Declare the return type as in the example.- Unwrapping each line.
for line in reader.lines()yieldsResults; printinglinedirectly printsOk("..."). Uselet line = line?;or handle the error. - Borrowed lines. Lines from
read_to_string(...).lines()borrow from theString; keep theStringalive while you use them, or convert with.to_string(). See String vs &str. - Numbers in lines must be parsed: see parse a string to an int.
Confirm it worked
Print the line count and compare it with wc -l (which counts newline characters, so it is one lower when the file has no trailing newline, as in the example).
FAQ
Does lines() keep the newline characters?
No. Each String returned by BufRead::lines has the trailing \n, or \r\n, removed. Use read_line in a loop if you need the line endings.
What happens with a file that is not valid UTF-8?
lines() and read_to_string return an error of kind InvalidData for the first invalid sequence. Read bytes with BufRead::split(b'\n') or fs::read, then convert with String::from_utf8_lossy.
Should I wrap the File in a BufReader?
Yes. lines() is a method of the BufRead trait, which File does not implement. BufReader adds a buffer, so the file is read in large chunks instead of one system call per line.