Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I take input from either stdin or a file if I cannot seek stdin?

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?

like image 751
Camden Narzt Avatar asked May 14 '16 07:05

Camden Narzt


2 Answers

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),
}
like image 162
Lukas Kalbertodt Avatar answered Sep 30 '22 18:09

Lukas Kalbertodt


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
        }
    }
}
like image 31
malbarbo Avatar answered Sep 30 '22 16:09

malbarbo