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}