I am porting some Python to Rust as a learning exercise and need to take input from either a file or stdin. I keep a handle to my input in a struct so I thought I'd just make a Box<io::Read>
but I ran into a situation where I need to seek on the input, and seek
isn't part of the Read
trait. I know you can't seek in pipes, so I'm going ahead and assuming for now that this method only gets called when the input is a file, but my problem is that I can't check that and downcast in Rust.
I know that I could use an enum for the two input types, but it seems like there should be a more elegant way to do this. And that's my question, how do you do this and not make a mess?
Is it possible to wrap stdin or a file in the same sort of buffer so I could just use that type and not worry about the type of IO?
I know, you said you wanted something more elegant and without enums, but I think the enum-solution is fairly elegant. So here is one attempt:
use std::fs;
use std::io::{self, Read, Seek, SeekFrom};
enum Input {
File(fs::File),
Stdin(io::Stdin),
}
impl Read for Input {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
Input::File(ref mut file) => file.read(buf),
Input::Stdin(ref mut stdin) => stdin.read(buf),
}
}
}
impl Seek for Input {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match *self {
Input::File(ref mut file) => file.seek(pos),
Input::Stdin(_) => {
Err(io::Error::new(
io::ErrorKind::Other,
"not supported by stdin-input",
))
},
}
}
}
Put code like that in some sub module of yours and don't worry about it too much anymore. You can use an object of type Input
just like you would use a File
: you have to handle seek errors anyway, so handling the inability to seek by stdin should be super easy. An example:
let arg = std::env::args().nth(1).unwrap();
let mut input = if arg == "--" {
Input::Stdin(io::stdin())
} else {
Input::File(fs::File::open(&arg).expect("I should handle that.."))
};
let mut v = Vec::new();
let _idc = input.read_to_end(&mut v);
match input.seek(SeekFrom::End(0)) {
Err(_) => println!("oh noes :("),
Ok(bytes) => println!("yeah, input is {} long", bytes),
}
Is it possible to wrap stdin or a file in the same sort of buffer so I could just use that type and not worry about the type of io?
That's exactly what the trait Read
does. It seems that what you want is an abstraction (trait) for Stdin
and File
that has optional support for seek
and allows to query about this support. In the following code, OptionalSeekRead
trait is used to fulfill this intend:
use std::io::{Read, Seek, SeekFrom, Stdin};
use std::fs::File;
// define a trait alias
pub trait SeekRead: Seek + Read {}
impl<T: Seek + Read> SeekRead for T {}
pub trait OptionSeekRead: Read {
fn get_seek_read(&mut self) -> Option<&mut SeekRead>;
}
impl OptionSeekRead for File {
fn get_seek_read(&mut self) -> Option<&mut SeekRead> {
Some(self)
}
}
impl OptionSeekRead for Stdin {
fn get_seek_read(&mut self) -> Option<&mut SeekRead> {
None
}
}
struct Handle {
read: Box<OptionSeekRead>,
}
impl Handle {
fn f(&mut self) {
if let Some(h) = self.read.get_seek_read() {
// h is Seek + Read
h.seek(SeekFrom::Start(42));
} else {
// without Seek
}
}
}