Reading a file or stdin with Rust
Sometimes I need a Rust program to accept a filename to read, or default to reading from standard input. Here’s how I’ve ended up doing that:
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
trait DefaultToStdin {
fn open(&self) -> Box<dyn BufRead>;
}
That’s defining the shape of what I’m after: whatever happens I’m going to get back a BufRead—which is a trait I’m interested in as it allows me to read lines() from a file.
The dyn BufRead part means we don’t know the concrete type at compile time, but we know it’ll implement the BufRead trait so we can call lines() on the result. It’s dynamic dispatch, if that’s something you’re familiar with.
This is wrapped in a Box, which is a pointer to heap memory. In Rust, we need to know the size of everything at compile time, and as the size of the BufRead implementation could be anything, we stuff it behind a fixed-size pointer. In the future, I might be able to write the return type as -> impl BufRead, but not yet.
The last step is to provide an implementation. In my case, I’m maybe given a filename, which is represented as an optional path:
impl DefaultToStdin for Option<PathBuf> {
fn open(&self) -> Box<dyn BufRead> {
match self {
None => Box::new(BufReader::new(std::io::stdin())),
Some(filename) => {
let file = File::open(filename)
.expect("Cannot open source file");
Box::new(BufReader::new(file))
}
}
}
}Inside the function I construct a reader from stdin or the file, depending on what we’re given (remember the self is an Option<PathBuf>).
With that in place, assuming source is an optional path, this reads the file or standard input:
for line in source.open().lines() {
// do something with the line here
}Ok, it blows up given a file that doesn’t exist. That’s not great, but the usage here is command-line argument parsing, so that’s probably ok.