Standard Input and Output
read_line()
read_line(&mut self, buf: &mut String) -> io::Result<usize> is a method provided on std::io::BufRead (and directly as a convenience method on std::io::Stdin) that reads bytes from an input stream until a newline delimiter (0x0A / \n) or End-of-File (EOF) is reached, validates that the bytes are UTF-8, appends them (including the trailing \n or \r\n) onto buf, and returns the number of bytes appended.
Three Critical Behaviors of read_line()
- It APPENDS to
buf(does not clearbuf!): If you callread_line(&mut line)in a loop without callingline.clear()at the end of each iteration, every new line is concatenated onto the end of all previous lines! - It retains the trailing newline (
\non Unix,\r\non Windows): Callingline.parse::<i32>()or comparingline == "quit"will fail unless you strip the trailing newline withline.trim_end()(orline.trim()). - Return value
Ok(0)signals End-of-File (EOF): As long as at least one character (even an empty newline"\n", which is1byte!) is read,read_linereturnsOk(n)wheren >= 1. It returnsOk(0)if and only if the stream has reached EOF.
What `reader.read_line(&mut buf)` Appends and Returns:
Input Stream Bytes: b"hello\r\n\n"
------------------------------------------------------------------
Call 1: `read_line(&mut buf)` -> Appends `"hello\r\n"`, returns `Ok(7)`
`buf.trim_end()` -> `"hello"`
`buf.clear()` -> Resets `buf` length to 0 (retains heap capacity!)
Call 2: `read_line(&mut buf)` -> Appends `"\n"`, returns `Ok(1)` (Empty line, NOT EOF!)
Call 3: `read_line(&mut buf)` -> Appends `""`, returns `Ok(0)` (EOF reached!)
Idiomatic Zero-Allocation Loop Using read_line + buf.clear()
Unlike .lines()—which allocates a new String on the heap for every single line in a file—reusing a single String buffer with read_line(&mut buf) and buf.clear() processes millions of lines with only one heap allocation:
use std::io::{self, BufRead, Cursor};
fn process_stream_lines(mut reader: impl BufRead) -> io::Result<(usize, usize)> {
let mut line_buf = String::with_capacity(128);
let mut line_count = 0;
let mut total_trimmed_chars = 0;
// `read_line` returns `Ok(0)` only when EOF is reached:
while reader.read_line(&mut line_buf)? > 0 {
let trimmed = line_buf.trim_end_matches(&['\r', '\n'][..]);
line_count += 1;
total_trimmed_chars += trimmed.len();
// Crucial: clear the String's length to 0 while keeping its heap capacity!
line_buf.clear();
}
Ok((line_count, total_trimmed_chars))
}
fn main() -> io::Result<()> {
let mock_input = Cursor::new("alpha\n\nbeta\r\ngamma");
let (lines, chars) = process_stream_lines(mock_input)?;
println!("lines read: {}, total non-newline chars: {}", lines, chars);
Ok(())
}
lines read: 4, total non-newline chars: 14
Stripping Only \r\n / \n Without Stripping Intentional Trailing Spaces
Notice the difference between .trim_end() and .trim_end_matches(&['\r', '\n'][..]):
line.trim_end()strips all trailing whitespace (including spaces' 'and tabs'\t').line.trim_end_matches(&['\r', '\n'][..])strips only the line terminator (\nor\r\n), preserving any intentional trailing spaces on the line.