Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/parse_with_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ struct Args {

fn main() -> anyhow::Result<()> {
let args = Args::parse();
eprintln!("{:?}", args.user);
eprintln!("{:?}", args.user.contents());
Ok(())
}
127 changes: 62 additions & 65 deletions src/file_or_stdin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fs;
use std::io::{self, Read};
use std::marker::PhantomData;
use std::str::FromStr;

use super::{Source, StdinError};
Expand All @@ -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 <filename> | ./example -
/// <filename> contents
/// ```
/// $ echo "1 2 3 4" > input.txt
/// $ cat input.txt | ./example -
/// 1 2 3 4
///
/// ```sh
/// $ ./example <filename>
/// <filename> contents
/// $ ./example input.txt
/// 1 2 3 4
/// ```
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct FileOrStdin<T = String> {
/// Source of the contents
pub source: Source,
inner: T,
}

impl<T> FromStr for FileOrStdin<T>
where
T: FromStr,
<T as FromStr>::Err: std::fmt::Display,
{
type Err = StdinError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
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<T>,
}

impl<T> FileOrStdin<T> {
/// 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<T, StdinError>
where
T: FromStr,
<T as 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<T> std::fmt::Display for FileOrStdin<T>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.inner.fmt(f)
}
}

impl<T> std::fmt::Debug for FileOrStdin<T>
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<impl io::Read, StdinError> {
let input: Box<dyn std::io::Read + 'static> = 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<T> std::ops::Deref for FileOrStdin<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> FromStr for FileOrStdin<T> {
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<Self, Self::Err> {
let source = Source::from_str(s)?;
Ok(Self {
source,
_type: PhantomData,
})
}
}
6 changes: 5 additions & 1 deletion tests/fixtures/file_or_stdin_optional_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
);
}
7 changes: 6 additions & 1 deletion tests/fixtures/file_or_stdin_positional_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ use clap_stdin::FileOrStdin;

#[derive(Debug, Parser)]
struct Args {
#[clap(default_value = "-")]
first: FileOrStdin,
#[clap(short, long)]
second: Option<String>,
}

fn main() {
let args = Args::parse();
println!("{args:?}");
println!(
"FIRST: {}; SECOND: {:?}",
args.first.contents().unwrap(),
args.second
);
}
6 changes: 5 additions & 1 deletion tests/fixtures/file_or_stdin_twice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ struct Args {

fn main() {
let args = Args::parse();
println!("{args:?}");
println!(
"FIRST: {}; SECOND: {}",
args.first.contents().unwrap(),
args.second
);
}
26 changes: 9 additions & 17 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,26 +109,24 @@ 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()
.args([&tmp_path])
.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]
Expand All @@ -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()
Expand All @@ -153,17 +151,15 @@ 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()
.args(["FIRST"])
.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]
Expand All @@ -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")
Expand Down