clap_stdin/
file_or_stdin.rs

1use std::marker::PhantomData;
2use std::str::FromStr;
3
4#[cfg(feature = "tokio")]
5use tokio::io::AsyncReadExt;
6
7use super::{Source, StdinError};
8
9/// Wrapper struct to either read in a file or contents from `stdin`
10///
11/// `FileOrStdin` can wrap any type that matches the trait bounds for `Arg`: `FromStr` and `Clone`
12/// ```rust
13/// use std::path::PathBuf;
14/// use clap::Parser;
15/// use clap_stdin::FileOrStdin;
16///
17/// #[derive(Debug, Parser)]
18/// struct Args {
19///     input: FileOrStdin,
20/// }
21///
22/// # fn main() -> anyhow::Result<()> {
23/// if let Ok(args) = Args::try_parse() {
24///     println!("input={}", args.input.contents()?);
25/// }
26/// # Ok(())
27/// # }
28/// ```
29///
30/// ```sh
31/// $ echo "1 2 3 4" > input.txt
32/// $ cat input.txt | ./example -
33/// 1 2 3 4
34///
35/// $ ./example input.txt
36/// 1 2 3 4
37/// ```
38#[derive(Debug, Clone)]
39pub struct FileOrStdin<T = String> {
40    source: Source,
41    _type: PhantomData<T>,
42}
43
44impl<T> FileOrStdin<T> {
45    /// Was this value read from stdin
46    pub fn is_stdin(&self) -> bool {
47        matches!(self.source, Source::Stdin)
48    }
49
50    /// Was this value read from a file (path passed in from argument values)
51    pub fn is_file(&self) -> bool {
52        !self.is_stdin()
53    }
54
55    /// The value passed to this arg (Either "-" for stdin or a filepath)
56    pub fn filename(&self) -> &str {
57        match &self.source {
58            Source::Stdin => "-",
59            Source::Arg(path) => path,
60        }
61    }
62
63    /// Read the entire contents from the input source, returning T::from_str
64    pub fn contents(self) -> Result<T, StdinError>
65    where
66        T: FromStr,
67        <T as FromStr>::Err: std::fmt::Display,
68    {
69        use std::io::Read;
70        let mut reader = self.into_reader()?;
71        let mut input = String::new();
72        let _ = reader.read_to_string(&mut input)?;
73        T::from_str(input.trim_end()).map_err(|e| StdinError::FromStr(format!("{e}")))
74    }
75
76    /// Create a reader from the source, to allow user flexibility of
77    /// how to read and parse (e.g. all at once or in chunks)
78    ///
79    /// ```no_run
80    /// use std::io::Read;
81    ///
82    /// use clap_stdin::FileOrStdin;
83    /// use clap::Parser;
84    ///
85    /// #[derive(Parser)]
86    /// struct Args {
87    ///   input: FileOrStdin,
88    /// }
89    ///
90    /// # fn main() -> anyhow::Result<()> {
91    /// let args = Args::parse();
92    /// let mut reader = args.input.into_reader()?;
93    /// let mut buf = vec![0;8];
94    /// reader.read_exact(&mut buf)?;
95    /// # Ok(())
96    /// # }
97    /// ```
98    pub fn into_reader(self) -> Result<impl std::io::Read, StdinError> {
99        self.source.into_reader()
100    }
101
102    #[cfg(feature = "tokio")]
103    /// Read the entire contents from the input source, returning T::from_str
104    /// ```rust,no_run
105    /// use clap::Parser;
106    /// use clap_stdin::FileOrStdin;
107    ///
108    /// #[derive(Debug, Parser)]
109    /// struct Args {
110    ///     input: FileOrStdin,
111    /// }
112    ///
113    /// # #[tokio::main(flavor = "current_thread")]
114    /// # async fn main() -> anyhow::Result<()> {
115    /// let args = Args::parse();
116    /// println!("input={}", args.input.contents_async().await?);
117    /// # Ok(())
118    /// # }
119    /// ```
120    pub async fn contents_async(self) -> Result<T, StdinError>
121    where
122        T: FromStr,
123        <T as FromStr>::Err: std::fmt::Display,
124    {
125        let mut reader = self.into_async_reader().await?;
126        let mut input = String::new();
127        let _ = reader.read_to_string(&mut input).await?;
128        T::from_str(input.trim_end()).map_err(|e| StdinError::FromStr(format!("{e}")))
129    }
130
131    #[cfg(feature = "tokio")]
132    /// Create a reader from the source, to allow user flexibility of
133    /// how to read and parse (e.g. all at once or in chunks)
134    ///
135    /// ```no_run
136    /// use std::io::Read;
137    /// use tokio::io::AsyncReadExt;
138    ///
139    /// use clap_stdin::FileOrStdin;
140    /// use clap::Parser;
141    ///
142    /// #[derive(Parser)]
143    /// struct Args {
144    ///   input: FileOrStdin,
145    /// }
146    ///
147    /// # #[tokio::main(flavor = "current_thread")]
148    /// # async fn main() -> anyhow::Result<()> {
149    /// let args = Args::parse();
150    /// let mut reader = args.input.into_async_reader().await?;
151    /// let mut buf = vec![0;8];
152    /// reader.read_exact(&mut buf).await?;
153    /// # Ok(())
154    /// # }
155    /// ```
156    pub async fn into_async_reader(&self) -> Result<impl tokio::io::AsyncRead, StdinError> {
157        let input: std::pin::Pin<Box<dyn tokio::io::AsyncRead + 'static>> = match &self.source {
158            Source::Stdin => Box::pin(tokio::io::stdin()),
159            Source::Arg(filepath) => {
160                let f = tokio::fs::File::open(filepath).await?;
161                Box::pin(f)
162            }
163        };
164        Ok(input)
165    }
166}
167
168impl<T> FromStr for FileOrStdin<T> {
169    type Err = StdinError;
170
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        let source = Source::from_str(s)?;
173        Ok(Self {
174            source,
175            _type: PhantomData,
176        })
177    }
178}
179
180#[test]
181fn test_source_methods() {
182    let val: FileOrStdin<String> = "-".parse().unwrap();
183    assert!(val.is_stdin());
184    assert!(!val.is_file());
185    assert_eq!(val.filename(), "-");
186
187    let val: FileOrStdin<String> = "/path/to/something".parse().unwrap();
188    assert!(val.is_file());
189    assert!(!val.is_stdin());
190    assert_eq!(val.filename(), "/path/to/something");
191}