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 }