Accept optional file on command line, default to stdin

911 Views Asked by At

I'm building a CLI program that takes as an optional final argument the name of a file to read from, which may be left off to read from standard input instead, as with cat and similar UNIX programs.

Is there any way I could have clap populate a member of my Cli struct with something like a Box<dyn BufRead> initialized from either the file or stdin, or is this just something I'm going to have to handle manually?

Not sure why this was closed as opinion-based, but it seems that the answer to my question is "no". There's no simple way to prepopulate a struct with an open reader on the right thing using only clap's built-in parsing, and I have to do it manually. Case closed.

I wasn't looking for how best to do it manually, just asking if there was a feature I'd overlooked that would let me avoid having to.

2

There are 2 best solutions below

5
On

You can use an Option<PathBuf> and use stdin when None, everything wrapped into a method (yes, you have to implement it yourself):

use std::io::BufReader;
use clap;
use std::io::BufRead;
use std::path::PathBuf; // 3.1.6

#[derive(clap::Parser)]
struct Reader {
    input: Option<PathBuf>,
}

impl Reader {
    fn reader(&self) -> Box<dyn BufRead> {
        self.input.as_ref()
            .map(|path| {
                Box::new(BufReader::new(std::fs::File::open(path).unwrap())) as Box<dyn BufRead>
            })
            .unwrap_or_else(|| Box::new(BufReader::new(std::io::stdin())) as Box<dyn BufRead>)
    }
}

Playground

1
On

This solution: Doesn't use dynamic dispatch, is verbose, use default_value_t, require nightly see #93965 (It should be possible to not require nightly but it's was simpler for me)

use clap; // 3.1.6

use std::fmt::{self, Display, Formatter};
use std::fs::File;
use std::io::{self, stdin, BufRead, BufReader, Read, StdinLock};
use std::str::FromStr;

enum Input {
    Stdin(StdinLock<'static>),
    File(BufReader<File>),
}

impl Display for Input {
    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
        write!(fmt, "Input")
    }
}

impl Default for Input {
    fn default() -> Self {
        Self::Stdin(stdin().lock())
    }
}

impl FromStr for Input {
    type Err = io::Error;

    fn from_str(path: &str) -> Result<Self, <Self as FromStr>::Err> {
        File::open(path).map(BufReader::new).map(Input::File)
    }
}

impl BufRead for Input {
    fn fill_buf(&mut self) -> Result<&[u8], io::Error> {
        match self {
            Self::Stdin(stdin) => stdin.fill_buf(),
            Self::File(file) => file.fill_buf(),
        }
    }

    fn consume(&mut self, amt: usize) {
        match self {
            Self::Stdin(stdin) => stdin.consume(amt),
            Self::File(file) => file.consume(amt),
        }
    }
}

impl Read for Input {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
        match self {
            Self::Stdin(stdin) => stdin.read(buf),
            Self::File(file) => file.read(buf),
        }
    }
}

#[derive(clap::Parser)]
struct Reader {
    #[clap(default_value_t)]
    input: Input,
}