Skip to content

Commit 9ce14d0

Browse files
Merge pull request #3 from thepacketgeek/file-read
Adding Read support to FileOrStdin
2 parents da3676e + a384675 commit 9ce14d0

File tree

7 files changed

+130
-92
lines changed

7 files changed

+130
-92
lines changed

README.md

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,26 @@ use clap_stdin::FileOrStdin;
5858
5959
#[derive(Debug, Parser)]
6060
struct Args {
61-
contents: FileOrStdin,
61+
input: FileOrStdin,
6262
}
6363
64+
# fn main() -> anyhow::Result<()> {
6465
let args = Args::parse();
65-
println!("contents={}", args.contents);
66+
println!("input={}", args.input.contents()?);
67+
# Ok(())
68+
# }
6669
```
6770

6871
Calling this CLI:
6972
```sh
7073
# using stdin for positional arg value
7174
$ echo "testing" | cargo run -- -
72-
contents=testing
75+
input=testing
7376

7477
# using filename for positional arg value
75-
$ echo "testing" > contents.txt
76-
$ cargo run -- contents.txt
77-
contents=testing
78+
$ echo "testing" > input.txt
79+
$ cargo run -- input.txt
80+
input=testing
7881
```
7982

8083
## Compatible Types
@@ -100,6 +103,39 @@ $ cat myfile.txt
100103
$ .example myfile.txt
101104
```
102105

106+
## Reading from Stdin without special characters
107+
When using [`MaybeStdin`] or [`FileOrStdin`], you can allow your users to omit the "-" character to read from `stdin` by providing a `default_value` to clap. This works with positional and optional args:
108+
109+
```rust,no_run
110+
use clap::Parser;
111+
112+
use clap_stdin::FileOrStdin;
113+
114+
#[derive(Debug, Parser)]
115+
struct Args {
116+
#[clap(default_value = "-")]
117+
input: FileOrStdin,
118+
}
119+
120+
# fn main() -> anyhow::Result<()> {
121+
let args = Args::parse();
122+
println!("input={}", args.input.contents()?);
123+
# Ok(())
124+
# }
125+
```
126+
127+
Calling this CLI:
128+
```sh
129+
# using stdin for positional arg value
130+
$ echo "testing" | cargo run
131+
input=testing
132+
133+
# using filename for positional arg value
134+
$ echo "testing" > input.txt
135+
$ cargo run -- input.txt
136+
input=testing
137+
```
138+
103139
## Using `MaybeStdin` or `FileOrStdin` multiple times
104140
Both [`MaybeStdin`] and [`FileOrStdin`] will check at runtime if `stdin` is being read from multiple times. You can use this
105141
as a feature if you have mutually exclusive args that should both be able to read from stdin, but know

examples/parse_with_serde.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ struct Args {
4141

4242
fn main() -> anyhow::Result<()> {
4343
let args = Args::parse();
44-
eprintln!("{:?}", args.user);
44+
eprintln!("{:?}", args.user.contents());
4545
Ok(())
4646
}

src/file_or_stdin.rs

Lines changed: 62 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::fs;
22
use std::io::{self, Read};
3+
use std::marker::PhantomData;
34
use std::str::FromStr;
45

56
use super::{Source, StdinError};
@@ -14,90 +15,86 @@ use super::{Source, StdinError};
1415
///
1516
/// #[derive(Debug, Parser)]
1617
/// struct Args {
17-
/// contents: FileOrStdin,
18+
/// input: FileOrStdin,
1819
/// }
1920
///
21+
/// # fn main() -> anyhow::Result<()> {
2022
/// if let Ok(args) = Args::try_parse() {
21-
/// println!("contents={}", args.contents);
23+
/// println!("input={}", args.input.contents()?);
2224
/// }
25+
/// # Ok(())
26+
/// # }
2327
/// ```
2428
///
2529
/// ```sh
26-
/// $ cat <filename> | ./example -
27-
/// <filename> contents
28-
/// ```
30+
/// $ echo "1 2 3 4" > input.txt
31+
/// $ cat input.txt | ./example -
32+
/// 1 2 3 4
2933
///
30-
/// ```sh
31-
/// $ ./example <filename>
32-
/// <filename> contents
34+
/// $ ./example input.txt
35+
/// 1 2 3 4
3336
/// ```
34-
#[derive(Clone)]
37+
#[derive(Debug, Clone)]
3538
pub struct FileOrStdin<T = String> {
36-
/// Source of the contents
3739
pub source: Source,
38-
inner: T,
39-
}
40-
41-
impl<T> FromStr for FileOrStdin<T>
42-
where
43-
T: FromStr,
44-
<T as FromStr>::Err: std::fmt::Display,
45-
{
46-
type Err = StdinError;
47-
48-
fn from_str(s: &str) -> Result<Self, Self::Err> {
49-
let source = Source::from_str(s)?;
50-
match &source {
51-
Source::Stdin => {
52-
let stdin = io::stdin();
53-
let mut input = String::new();
54-
stdin.lock().read_to_string(&mut input)?;
55-
Ok(T::from_str(input.trim_end())
56-
.map_err(|e| StdinError::FromStr(format!("{e}")))
57-
.map(|val| Self { source, inner: val })?)
58-
}
59-
Source::Arg(filepath) => Ok(T::from_str(&fs::read_to_string(filepath)?)
60-
.map_err(|e| StdinError::FromStr(format!("{e}")))
61-
.map(|val| FileOrStdin { source, inner: val })?),
62-
}
63-
}
40+
_type: PhantomData<T>,
6441
}
6542

6643
impl<T> FileOrStdin<T> {
67-
/// Extract the inner value from the wrapper
68-
pub fn into_inner(self) -> T {
69-
self.inner
44+
/// Read the entire contents from the input source, returning T::from_str
45+
pub fn contents(self) -> Result<T, StdinError>
46+
where
47+
T: FromStr,
48+
<T as FromStr>::Err: std::fmt::Display,
49+
{
50+
let mut reader = self.into_reader()?;
51+
let mut input = String::new();
52+
let _ = reader.read_to_string(&mut input)?;
53+
T::from_str(input.trim_end()).map_err(|e| StdinError::FromStr(format!("{e}")))
7054
}
71-
}
7255

73-
impl<T> std::fmt::Display for FileOrStdin<T>
74-
where
75-
T: std::fmt::Display,
76-
{
77-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78-
self.inner.fmt(f)
79-
}
80-
}
81-
82-
impl<T> std::fmt::Debug for FileOrStdin<T>
83-
where
84-
T: std::fmt::Debug,
85-
{
86-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87-
self.inner.fmt(f)
56+
/// Create a reader from the source, to allow user flexibility of
57+
/// how to read and parse (e.g. all at once or in chunks)
58+
///
59+
/// ```no_run
60+
/// use std::io::Read;
61+
///
62+
/// use clap_stdin::FileOrStdin;
63+
/// use clap::Parser;
64+
///
65+
/// #[derive(Parser)]
66+
/// struct Args {
67+
/// input: FileOrStdin,
68+
/// }
69+
///
70+
/// # fn main() -> anyhow::Result<()> {
71+
/// let args = Args::parse();
72+
/// let mut reader = args.input.into_reader()?;
73+
/// let mut buf = vec![0;8];
74+
/// reader.read_exact(&mut buf)?;
75+
/// # Ok(())
76+
/// # }
77+
/// ```
78+
pub fn into_reader(&self) -> Result<impl io::Read, StdinError> {
79+
let input: Box<dyn std::io::Read + 'static> = match &self.source {
80+
Source::Stdin => Box::new(std::io::stdin()),
81+
Source::Arg(filepath) => {
82+
let f = fs::File::open(filepath)?;
83+
Box::new(f)
84+
}
85+
};
86+
Ok(input)
8887
}
8988
}
9089

91-
impl<T> std::ops::Deref for FileOrStdin<T> {
92-
type Target = T;
93-
94-
fn deref(&self) -> &Self::Target {
95-
&self.inner
96-
}
97-
}
90+
impl<T> FromStr for FileOrStdin<T> {
91+
type Err = StdinError;
9892

99-
impl std::ops::DerefMut for FileOrStdin {
100-
fn deref_mut(&mut self) -> &mut Self::Target {
101-
&mut self.inner
93+
fn from_str(s: &str) -> Result<Self, Self::Err> {
94+
let source = Source::from_str(s)?;
95+
Ok(Self {
96+
source,
97+
_type: PhantomData,
98+
})
10299
}
103100
}

tests/fixtures/file_or_stdin_optional_arg.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ struct Args {
1111

1212
fn main() {
1313
let args = Args::parse();
14-
println!("{args:?}");
14+
println!(
15+
"FIRST: {}, SECOND: {:?}",
16+
args.first,
17+
args.second.map(|second| second.contents().unwrap()),
18+
);
1519
}

tests/fixtures/file_or_stdin_positional_arg.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ use clap_stdin::FileOrStdin;
44

55
#[derive(Debug, Parser)]
66
struct Args {
7+
#[clap(default_value = "-")]
78
first: FileOrStdin,
89
#[clap(short, long)]
910
second: Option<String>,
1011
}
1112

1213
fn main() {
1314
let args = Args::parse();
14-
println!("{args:?}");
15+
println!(
16+
"FIRST: {}; SECOND: {:?}",
17+
args.first.contents().unwrap(),
18+
args.second
19+
);
1520
}

tests/fixtures/file_or_stdin_twice.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ struct Args {
1010

1111
fn main() {
1212
let args = Args::parse();
13-
println!("{args:?}");
13+
println!(
14+
"FIRST: {}; SECOND: {}",
15+
args.first.contents().unwrap(),
16+
args.second
17+
);
1418
}

tests/tests.rs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -109,26 +109,24 @@ fn test_file_or_stdin_positional_arg() {
109109
.assert()
110110
.success()
111111
.stdout(predicate::str::starts_with(
112-
r#"Args { first: "FILE", second: Some("SECOND") }"#,
112+
r#"FIRST: FILE; SECOND: Some("SECOND")"#,
113113
));
114114
Command::cargo_bin("file_or_stdin_positional_arg")
115115
.unwrap()
116-
.args(["-", "--second", "SECOND"])
116+
.args(["--second", "SECOND"])
117117
.write_stdin("STDIN")
118118
.assert()
119119
.success()
120120
.stdout(predicate::str::starts_with(
121-
r#"Args { first: "STDIN", second: Some("SECOND") }"#,
121+
r#"FIRST: STDIN; SECOND: Some("SECOND")"#,
122122
));
123123
Command::cargo_bin("file_or_stdin_positional_arg")
124124
.unwrap()
125125
.args([&tmp_path])
126126
.write_stdin("TESTING")
127127
.assert()
128128
.success()
129-
.stdout(predicate::str::starts_with(
130-
r#"Args { first: "FILE", second: None }"#,
131-
));
129+
.stdout(predicate::str::starts_with(r#"FIRST: FILE; SECOND: None"#));
132130
}
133131

134132
#[test]
@@ -144,7 +142,7 @@ fn test_file_or_stdin_optional_arg() {
144142
.assert()
145143
.success()
146144
.stdout(predicate::str::starts_with(
147-
r#"Args { first: "FIRST", second: Some(2) }"#,
145+
r#"FIRST: FIRST, SECOND: Some(2)"#,
148146
));
149147
Command::cargo_bin("file_or_stdin_optional_arg")
150148
.unwrap()
@@ -153,17 +151,15 @@ fn test_file_or_stdin_optional_arg() {
153151
.assert()
154152
.success()
155153
.stdout(predicate::str::starts_with(
156-
r#"Args { first: "FIRST", second: Some(2) }"#,
154+
r#"FIRST: FIRST, SECOND: Some(2)"#,
157155
));
158156
Command::cargo_bin("file_or_stdin_optional_arg")
159157
.unwrap()
160158
.args(["FIRST"])
161159
.write_stdin("TESTING")
162160
.assert()
163161
.success()
164-
.stdout(predicate::str::starts_with(
165-
r#"Args { first: "FIRST", second: None }"#,
166-
));
162+
.stdout(predicate::str::starts_with(r#"FIRST: FIRST, SECOND: None"#));
167163
}
168164

169165
#[test]
@@ -177,18 +173,14 @@ fn test_file_or_stdin_twice() {
177173
.args([&tmp_path, "2"])
178174
.assert()
179175
.success()
180-
.stdout(predicate::str::starts_with(
181-
r#"Args { first: "FILE", second: 2 }"#,
182-
));
176+
.stdout(predicate::str::starts_with(r#"FIRST: FILE; SECOND: 2"#));
183177
Command::cargo_bin("file_or_stdin_twice")
184178
.unwrap()
185179
.write_stdin("2")
186180
.args([&tmp_path, "-"])
187181
.assert()
188182
.success()
189-
.stdout(predicate::str::starts_with(
190-
r#"Args { first: "FILE", second: 2 }"#,
191-
));
183+
.stdout(predicate::str::starts_with(r#"FIRST: FILE; SECOND: 2"#));
192184

193185
// Actually using stdin twice will fail because there's no value the second time
194186
Command::cargo_bin("file_or_stdin_twice")

0 commit comments

Comments
 (0)