arg_input/
lib.rs

1//! Inspired by Ruby's [`ARGF`](https://ruby-doc.org/core-1.9.3/ARGF.html).
2//! Treat files and `stdin` as if they were a big long concatenated stream.
3//!
4//! [`argf()`](fn.argf.html) will pull input from your command line arguments,
5//! no frills, no questions asked, and [`argf_lines()`](fn.argf_lines.html) will
6//! give you an iterator over all *lines* of command line input.
7//!
8//! `argf()` and `argf_lines()` assume that the command line arguments contain **only**
9//! file arguments. If you need a little more control (for example, you're using `docopt`
10//! to parse command line arguments instead), use [`input()`](fn.input.html) or
11//! [`input_lines()`](fn.input_lines.html)
12
13use std::env::args_os;
14use std::iter::ExactSizeIterator;
15use std::io::{self, Read};
16use std::io::{BufReader, BufRead};
17use std::fs::File;
18use std::path::Path;
19use std::error::Error;
20use std::fmt::{self, Display, Formatter};
21use std::convert::From;
22
23#[derive(Debug)]
24pub struct FailReadFileError {
25  pub inner: io::Error,
26  pub filename: String
27}
28
29impl Display for FailReadFileError {
30  fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
31    writeln!(f, "could not read file {}", self.filename)?;
32    write!(f, "caused by: {}", self.inner)?;
33    Ok(())
34  }
35}
36
37impl Error for FailReadFileError {
38  fn description(&self) -> &str {
39    "failed to read file"
40  }
41
42  fn cause(&self) -> Option<&Error> {
43    Some(&self.inner)
44  }
45}
46
47#[derive(Debug)]
48pub struct InputError {
49  pub badfiles: Vec<FailReadFileError>
50}
51
52impl Display for InputError {
53  fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
54    for e in &self.badfiles {
55      writeln!(f, "{}", e)?;
56    }
57    Ok(())
58  }
59}
60
61impl Error for InputError {
62  fn description(&self) -> &str {
63    "failed to read one or more files"
64  }
65
66  fn cause(&self) -> Option<&Error> {
67    let first = self.badfiles.first();
68
69    // There's some weird casting that I have to do here
70    // that I don't fully understand.
71    match first {
72      Some(err) => Some(err),
73      None => None
74    }
75  }
76}
77
78impl From<Vec<FailReadFileError>> for InputError {
79  fn from(err: Vec<FailReadFileError>) -> Self {
80    InputError { badfiles: err }
81  }
82}
83
84/// Add the attempt_map() function to all iterators.
85trait TryIterator {
86  type Item;
87  type JIter: ExactSizeIterator<Item=Self::Item>;
88
89  /// Attempt to map the function over the given iterator, which might fail.
90  /// If all attempts succeed, give back all the success. Otherwise, give
91  /// back all the errors.
92  fn attempt_map<F, T, E>(self, mapper: F) -> Result<Vec<T>, Vec<E>> where
93    F: Fn(Self::Item) -> Result<T, E>;
94}
95
96impl<I> TryIterator for I where
97  I: ExactSizeIterator 
98{
99  type Item = I::Item;
100  type JIter = I;
101
102  fn attempt_map<F, T, E>(self, mapper: F) -> Result<Vec<T>, Vec<E>> where
103    F: Fn(Self::Item) -> Result<T, E>
104  {
105    let mut any_failure = false;
106    let mut successes = Vec::new();
107    let mut failures = Vec::new();
108
109    for obj in self {
110      match mapper(obj) {
111        Ok(output) => {
112          if !any_failure {
113            successes.push(output);
114          }
115        },
116        Err(err) => {
117          any_failure = true;
118          failures.push(err);
119        }
120      };
121    }
122
123    if any_failure { Err(failures) } else { Ok(successes) }
124  }
125}
126
127pub type Lines = io::Lines<BufReader<Box<Read>>>;
128
129/// Act like [`input_lines()`](fn.input_lines.html), but automatically
130/// pull arguments from the command line. 
131///
132/// See [`argf()`](fn.argf.html) for caveats.
133pub fn argf_lines() -> Result<Lines, InputError> {
134  let chained = argf()?;
135  let buffered = BufReader::new(chained);
136
137  Ok(buffered.lines())
138}
139
140/// Act like [`input()`](fn.input.html), but automatically pull arguments
141/// from the command line.
142///
143/// Assumes that the command line arguments are undisturbed (i.e., the first
144/// argument is the executable name) and that all other arguments should be
145/// treated like file names. If this is not the case and you need more fine-grained
146/// control (e.g. you're using `docopt` to parse command-line arguments instead),
147/// use `input()`.
148pub fn argf() -> Result<Box<Read>, InputError> {
149  let args = args_os().skip(1);
150  input(args)
151}
152
153/// Return an iterator over all lines of input. 
154///
155/// See [`input()`](fn.input.html) for how this handles its arguments/errors.
156pub fn input_lines<I, J, S>(inputs: I) -> Result<Lines, InputError> where
157  I: IntoIterator<Item=S, IntoIter=J>,
158  J: ExactSizeIterator<Item=S>,
159  S: AsRef<Path>
160{
161  let chained = input(inputs)?;
162  let buffered = BufReader::new(chained);
163
164  Ok(buffered.lines())
165}
166
167/// Return a `Read` instance with all the input files/`stdin` chained together.
168///
169/// If any of the files fail to open, returns a `Vec` of all the IO errors
170/// instead.
171///
172/// If *no* files are specified as inputs, this reads solely from `stdin`.
173/// Otherwise, ignores `stdin` and concatenates the contents of all files
174/// specified as arguments.
175/// The argument "-" is special, and is an alias for `stdin`; this can be
176/// used to reinsert `stdin` into the contents returned, if so desired.
177pub fn input<I, J, S>(inputs: I) -> Result<Box<Read>, InputError> where
178  I: IntoIterator<Item=S, IntoIter=J>,
179  J: ExactSizeIterator<Item=S>,
180  S: AsRef<Path>
181{
182  let iter = inputs.into_iter();
183
184  if iter.len() == 0 {
185    Ok(Box::new(io::stdin()))
186  } else {
187    let reads = iter.attempt_map(|path| from_arg(path.as_ref()))?;
188
189    Ok(chain_all_reads(reads))
190  }
191}
192
193fn chain_all_reads<I>(reads: I) -> Box<Read> where
194  I: IntoIterator<Item=Box<Read>>
195{
196  reads.into_iter().fold(Box::new(io::empty()), |read, next| {
197    Box::new(read.chain(next))
198  })
199}
200
201fn from_arg<'a>(arg: &'a Path) -> Result<Box<Read>, FailReadFileError> {
202  let str_repr = arg.to_string_lossy();
203  if str_repr == "-" {
204    Ok(Box::new(io::stdin()))
205  } else {
206    let file = File::open(arg).map_err(|err| {
207      FailReadFileError {
208        inner: err,
209        filename: arg.to_string_lossy().to_string()
210      }
211    })?;
212    Ok(Box::new(file))
213  }
214}