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()

  1. It APPENDS to buf (does not clear buf!): If you call read_line(&mut line) in a loop without calling line.clear() at the end of each iteration, every new line is concatenated onto the end of all previous lines!
  2. It retains the trailing newline (\n on Unix, \r\n on Windows): Calling line.parse::<i32>() or comparing line == "quit" will fail unless you strip the trailing newline with line.trim_end() (or line.trim()).
  3. Return value Ok(0) signals End-of-File (EOF): As long as at least one character (even an empty newline "\n", which is 1 byte!) is read, read_line returns Ok(n) where n >= 1. It returns Ok(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 (\n or \r\n), preserving any intentional trailing spaces on the line.