diff --git a/README.md b/README.md index 1da8b45..22172db 100644 --- a/README.md +++ b/README.md @@ -58,23 +58,26 @@ use clap_stdin::FileOrStdin; #[derive(Debug, Parser)] struct Args { - contents: FileOrStdin, + input: FileOrStdin, } +# fn main() -> anyhow::Result<()> { let args = Args::parse(); -println!("contents={}", args.contents); +println!("input={}", args.input.contents()?); +# Ok(()) +# } ``` Calling this CLI: ```sh # using stdin for positional arg value $ echo "testing" | cargo run -- - -contents=testing +input=testing # using filename for positional arg value -$ echo "testing" > contents.txt -$ cargo run -- contents.txt -contents=testing +$ echo "testing" > input.txt +$ cargo run -- input.txt +input=testing ``` ## Compatible Types @@ -100,6 +103,39 @@ $ cat myfile.txt $ .example myfile.txt ``` +## Reading from Stdin without special characters +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: + +```rust,no_run +use clap::Parser; + +use clap_stdin::FileOrStdin; + +#[derive(Debug, Parser)] +struct Args { + #[clap(default_value = "-")] + input: FileOrStdin, +} + +# fn main() -> anyhow::Result<()> { +let args = Args::parse(); +println!("input={}", args.input.contents()?); +# Ok(()) +# } +``` + +Calling this CLI: +```sh +# using stdin for positional arg value +$ echo "testing" | cargo run +input=testing + +# using filename for positional arg value +$ echo "testing" > input.txt +$ cargo run -- input.txt +input=testing +``` + ## Using `MaybeStdin` or `FileOrStdin` multiple times Both [`MaybeStdin`] and [`FileOrStdin`] will check at runtime if `stdin` is being read from multiple times. You can use this as a feature if you have mutually exclusive args that should both be able to read from stdin, but know diff --git a/examples/parse_with_serde.rs b/examples/parse_with_serde.rs index 614a176..efae8ae 100644 --- a/examples/parse_with_serde.rs +++ b/examples/parse_with_serde.rs @@ -41,6 +41,6 @@ struct Args { fn main() -> anyhow::Result<()> { let args = Args::parse(); - eprintln!("{:?}", args.user); + eprintln!("{:?}", args.user.contents()); Ok(()) } diff --git a/src/file_or_stdin.rs b/src/file_or_stdin.rs index bfca65d..db2c29b 100644 --- a/src/file_or_stdin.rs +++ b/src/file_or_stdin.rs @@ -1,5 +1,6 @@ use std::fs; use std::io::{self, Read}; +use std::marker::PhantomData; use std::str::FromStr; use super::{Source, StdinError}; @@ -14,90 +15,86 @@ use super::{Source, StdinError}; /// /// #[derive(Debug, Parser)] /// struct Args { -/// contents: FileOrStdin, +/// input: FileOrStdin, /// } /// +/// # fn main() -> anyhow::Result<()> { /// if let Ok(args) = Args::try_parse() { -/// println!("contents={}", args.contents); +/// println!("input={}", args.input.contents()?); /// } +/// # Ok(()) +/// # } /// ``` /// /// ```sh -/// $ cat | ./example - -/// contents -/// ``` +/// $ echo "1 2 3 4" > input.txt +/// $ cat input.txt | ./example - +/// 1 2 3 4 /// -/// ```sh -/// $ ./example -/// contents +/// $ ./example input.txt +/// 1 2 3 4 /// ``` -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct FileOrStdin { - /// Source of the contents pub source: Source, - inner: T, -} - -impl FromStr for FileOrStdin -where - T: FromStr, - ::Err: std::fmt::Display, -{ - type Err = StdinError; - - fn from_str(s: &str) -> Result { - let source = Source::from_str(s)?; - match &source { - Source::Stdin => { - let stdin = io::stdin(); - let mut input = String::new(); - stdin.lock().read_to_string(&mut input)?; - Ok(T::from_str(input.trim_end()) - .map_err(|e| StdinError::FromStr(format!("{e}"))) - .map(|val| Self { source, inner: val })?) - } - Source::Arg(filepath) => Ok(T::from_str(&fs::read_to_string(filepath)?) - .map_err(|e| StdinError::FromStr(format!("{e}"))) - .map(|val| FileOrStdin { source, inner: val })?), - } - } + _type: PhantomData, } impl FileOrStdin { - /// Extract the inner value from the wrapper - pub fn into_inner(self) -> T { - self.inner + /// Read the entire contents from the input source, returning T::from_str + pub fn contents(self) -> Result + where + T: FromStr, + ::Err: std::fmt::Display, + { + let mut reader = self.into_reader()?; + let mut input = String::new(); + let _ = reader.read_to_string(&mut input)?; + T::from_str(input.trim_end()).map_err(|e| StdinError::FromStr(format!("{e}"))) } -} -impl std::fmt::Display for FileOrStdin -where - T: std::fmt::Display, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.fmt(f) - } -} - -impl std::fmt::Debug for FileOrStdin -where - T: std::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.fmt(f) + /// Create a reader from the source, to allow user flexibility of + /// how to read and parse (e.g. all at once or in chunks) + /// + /// ```no_run + /// use std::io::Read; + /// + /// use clap_stdin::FileOrStdin; + /// use clap::Parser; + /// + /// #[derive(Parser)] + /// struct Args { + /// input: FileOrStdin, + /// } + /// + /// # fn main() -> anyhow::Result<()> { + /// let args = Args::parse(); + /// let mut reader = args.input.into_reader()?; + /// let mut buf = vec![0;8]; + /// reader.read_exact(&mut buf)?; + /// # Ok(()) + /// # } + /// ``` + pub fn into_reader(&self) -> Result { + let input: Box = match &self.source { + Source::Stdin => Box::new(std::io::stdin()), + Source::Arg(filepath) => { + let f = fs::File::open(filepath)?; + Box::new(f) + } + }; + Ok(input) } } -impl std::ops::Deref for FileOrStdin { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} +impl FromStr for FileOrStdin { + type Err = StdinError; -impl std::ops::DerefMut for FileOrStdin { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner + fn from_str(s: &str) -> Result { + let source = Source::from_str(s)?; + Ok(Self { + source, + _type: PhantomData, + }) } } diff --git a/tests/fixtures/file_or_stdin_optional_arg.rs b/tests/fixtures/file_or_stdin_optional_arg.rs index 1e3ec83..10afd46 100644 --- a/tests/fixtures/file_or_stdin_optional_arg.rs +++ b/tests/fixtures/file_or_stdin_optional_arg.rs @@ -11,5 +11,9 @@ struct Args { fn main() { let args = Args::parse(); - println!("{args:?}"); + println!( + "FIRST: {}, SECOND: {:?}", + args.first, + args.second.map(|second| second.contents().unwrap()), + ); } diff --git a/tests/fixtures/file_or_stdin_positional_arg.rs b/tests/fixtures/file_or_stdin_positional_arg.rs index 5a39da1..5c60851 100644 --- a/tests/fixtures/file_or_stdin_positional_arg.rs +++ b/tests/fixtures/file_or_stdin_positional_arg.rs @@ -4,6 +4,7 @@ use clap_stdin::FileOrStdin; #[derive(Debug, Parser)] struct Args { + #[clap(default_value = "-")] first: FileOrStdin, #[clap(short, long)] second: Option, @@ -11,5 +12,9 @@ struct Args { fn main() { let args = Args::parse(); - println!("{args:?}"); + println!( + "FIRST: {}; SECOND: {:?}", + args.first.contents().unwrap(), + args.second + ); } diff --git a/tests/fixtures/file_or_stdin_twice.rs b/tests/fixtures/file_or_stdin_twice.rs index 5fd6ba8..95a21d7 100644 --- a/tests/fixtures/file_or_stdin_twice.rs +++ b/tests/fixtures/file_or_stdin_twice.rs @@ -10,5 +10,9 @@ struct Args { fn main() { let args = Args::parse(); - println!("{args:?}"); + println!( + "FIRST: {}; SECOND: {}", + args.first.contents().unwrap(), + args.second + ); } diff --git a/tests/tests.rs b/tests/tests.rs index 6b17350..a034233 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -109,16 +109,16 @@ fn test_file_or_stdin_positional_arg() { .assert() .success() .stdout(predicate::str::starts_with( - r#"Args { first: "FILE", second: Some("SECOND") }"#, + r#"FIRST: FILE; SECOND: Some("SECOND")"#, )); Command::cargo_bin("file_or_stdin_positional_arg") .unwrap() - .args(["-", "--second", "SECOND"]) + .args(["--second", "SECOND"]) .write_stdin("STDIN") .assert() .success() .stdout(predicate::str::starts_with( - r#"Args { first: "STDIN", second: Some("SECOND") }"#, + r#"FIRST: STDIN; SECOND: Some("SECOND")"#, )); Command::cargo_bin("file_or_stdin_positional_arg") .unwrap() @@ -126,9 +126,7 @@ fn test_file_or_stdin_positional_arg() { .write_stdin("TESTING") .assert() .success() - .stdout(predicate::str::starts_with( - r#"Args { first: "FILE", second: None }"#, - )); + .stdout(predicate::str::starts_with(r#"FIRST: FILE; SECOND: None"#)); } #[test] @@ -144,7 +142,7 @@ fn test_file_or_stdin_optional_arg() { .assert() .success() .stdout(predicate::str::starts_with( - r#"Args { first: "FIRST", second: Some(2) }"#, + r#"FIRST: FIRST, SECOND: Some(2)"#, )); Command::cargo_bin("file_or_stdin_optional_arg") .unwrap() @@ -153,7 +151,7 @@ fn test_file_or_stdin_optional_arg() { .assert() .success() .stdout(predicate::str::starts_with( - r#"Args { first: "FIRST", second: Some(2) }"#, + r#"FIRST: FIRST, SECOND: Some(2)"#, )); Command::cargo_bin("file_or_stdin_optional_arg") .unwrap() @@ -161,9 +159,7 @@ fn test_file_or_stdin_optional_arg() { .write_stdin("TESTING") .assert() .success() - .stdout(predicate::str::starts_with( - r#"Args { first: "FIRST", second: None }"#, - )); + .stdout(predicate::str::starts_with(r#"FIRST: FIRST, SECOND: None"#)); } #[test] @@ -177,18 +173,14 @@ fn test_file_or_stdin_twice() { .args([&tmp_path, "2"]) .assert() .success() - .stdout(predicate::str::starts_with( - r#"Args { first: "FILE", second: 2 }"#, - )); + .stdout(predicate::str::starts_with(r#"FIRST: FILE; SECOND: 2"#)); Command::cargo_bin("file_or_stdin_twice") .unwrap() .write_stdin("2") .args([&tmp_path, "-"]) .assert() .success() - .stdout(predicate::str::starts_with( - r#"Args { first: "FILE", second: 2 }"#, - )); + .stdout(predicate::str::starts_with(r#"FIRST: FILE; SECOND: 2"#)); // Actually using stdin twice will fail because there's no value the second time Command::cargo_bin("file_or_stdin_twice")