Beginner Projects

File Reader

A command-line file reader utility (modeled after Unix cat, head, and nl) streams one or more input sources through a BufRead pipeline, optionally numbering lines, squeezing consecutive blank lines, and bounding output to a line limit (head -n) without loading entire gigabyte files into memory at once.

Streaming File Reader Pipeline (`BufRead` -> `Write`):

  Input Source (`File` or `StdinLock`)
        |
        v  `BufReader` (Reuses a single `line_buf: String` via `.clear()`)
  [ Line Filter & Formatter ]
    ├── Squeeze repeated blank lines (`squeeze_blank`)
    ├── Prepend 1-based line numbers (`number_lines`: `"{line_no:>6}\t{line}"`)
    └── Stop early after `max_lines` (`Some(limit)`)
        |
        v  `BufWriter` (`StdoutLock` or output buffer)

Configuration Options for the Streaming Reader

Option Field Type Equivalent Unix Flag Behavior
number_lines bool cat -n / nl Prefixes every output line with a right-aligned 6-digit line number and tab.
squeeze_blank bool cat -s Collapses multiple consecutive blank lines into a single blank line.
max_lines Option<usize> head -n <K> Stops reading as soon as K output lines have been emitted.

Complete Runnable Implementation

Parameterizing the reader over R: BufRead and W: Write allows the exact same function to stream from BufReader<File>, StdinLock, or an in-memory &[u8] slice in unit tests while reusing a single String line buffer across the entire file:

use std::env;
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Write};

#[derive(Debug, Clone, Copy, Default)]
pub struct ReaderOptions {
    pub number_lines: bool,
    pub squeeze_blank: bool,
    pub max_lines: Option<usize>,
}

/// Streams lines from `reader` to `writer` using a single reused `String` allocation!
pub fn stream_with_options<R: BufRead, W: Write>(
    mut reader: R,
    mut writer: W,
    opts: ReaderOptions,
) -> io::Result<usize> {
    let mut line_buf = String::with_capacity(256);
    let mut emitted_lines = 0usize;
    let mut prev_was_blank = false;

    loop {
        if let Some(limit) = opts.max_lines {
            if emitted_lines >= limit {
                break;
            }
        }

        line_buf.clear();
        let bytes_read = reader.read_line(&mut line_buf)?;
        if bytes_read == 0 {
            break; // Reached EOF
        }

        let trimmed_newline = line_buf.trim_end_matches(['\r', '\n']);
        let is_blank = trimmed_newline.trim().is_empty();

        if opts.squeeze_blank && is_blank && prev_was_blank {
            continue;
        }
        prev_was_blank = is_blank;
        emitted_lines += 1;

        if opts.number_lines {
            writeln!(writer, "{:>6}\t{}", emitted_lines, trimmed_newline)?;
        } else {
            writeln!(writer, "{}", trimmed_newline)?;
        }
    }

    Ok(emitted_lines)
}

fn main() -> io::Result<()> {
    let temp_path = env::temp_dir().join("rust_ch66_reader_demo.txt");
    fs::write(
        &temp_path,
        "fn main() {\n\n\n    println!(\"hello\");\n}\n// trailing line\n",
    )?;

    let file = File::open(&temp_path)?;
    let reader = BufReader::new(file);
    let mut output = Vec::new();

    let opts = ReaderOptions {
        number_lines: true,
        squeeze_blank: true,
        max_lines: Some(4),
    };

    let count = stream_with_options(reader, &mut output, opts)?;
    assert_eq!(count, 4);

    let rendered = String::from_utf8(output).unwrap();
    print!("{rendered}");

    fs::remove_file(&temp_path)?;
    Ok(())
}
     1	fn main() {
     2	
     3	    println!("hello");
     4	}