clap_stdin/file_or_stdout.rs
1use std::str::FromStr;
2
3use super::Dest;
4
5/// `FileOrStdout` can be used as a proxy output writer to write to whichever destination
6/// was specified by the CLI args, a file or `stdout`.
7///
8/// ```rust
9/// use std::path::PathBuf;
10/// use std::io::Write;
11/// use clap::Parser;
12/// use clap_stdin::FileOrStdout;
13///
14/// #[derive(Debug, Parser)]
15/// struct Args {
16/// output: FileOrStdout,
17/// }
18///
19/// # fn main() -> anyhow::Result<()> {
20/// if let Ok(args) = Args::try_parse() {
21/// let mut writer = args.output.into_writer()?;
22/// write!(&mut writer, "1 2 3 4");
23/// }
24/// # Ok(())
25/// # }
26/// ```
27///
28/// ```sh
29/// $ ./example output.txt
30/// 1 2 3 4
31/// $ cat output.txt | ./example -
32/// 1 2 3 4
33/// ```
34#[derive(Debug, Clone)]
35pub struct FileOrStdout {
36 dest: Dest,
37}
38
39impl FileOrStdout {
40 /// Was this value read from stdout
41 pub fn is_stdout(&self) -> bool {
42 matches!(self.dest, Dest::Stdout)
43 }
44
45 /// Was this value read from a file (path passed in from argument values)
46 pub fn is_file(&self) -> bool {
47 !self.is_stdout()
48 }
49
50 /// The value passed to this arg (Either "-" for stdout or a filepath)
51 pub fn filename(&self) -> &str {
52 match &self.dest {
53 Dest::Stdout => "-",
54 Dest::Arg(path) => path,
55 }
56 }
57
58 /// Create a writer for the dest, to allow user flexibility of
59 /// how to write output (e.g. all at once or in chunks)
60 ///
61 /// ```no_run
62 /// use std::io::Write;
63 ///
64 /// use clap_stdin::FileOrStdout;
65 /// use clap::Parser;
66 ///
67 /// #[derive(Parser)]
68 /// struct Args {
69 /// output: FileOrStdout,
70 /// }
71 ///
72 /// # fn main() -> anyhow::Result<()> {
73 /// let args = Args::parse();
74 /// let mut writer = args.output.into_writer()?;
75 /// let mut buf = vec![0;8];
76 /// writer.write_all(&mut buf)?;
77 /// # Ok(())
78 /// # }
79 /// ```
80 pub fn into_writer(self) -> Result<impl std::io::Write, std::io::Error> {
81 self.dest.into_writer()
82 }
83
84 #[cfg(feature = "tokio")]
85 /// Create a reader from the source, to allow user flexibility of
86 /// how to read and parse (e.g. all at once or in chunks)
87 ///
88 /// ```no_run
89 /// use std::io::Write;
90 /// use tokio::io::AsyncWriteExt;
91 ///
92 /// use clap_stdin::FileOrStdout;
93 /// use clap::Parser;
94 ///
95 /// #[derive(Parser)]
96 /// struct Args {
97 /// output: FileOrStdout,
98 /// }
99 ///
100 /// # #[tokio::main(flavor = "current_thread")]
101 /// # async fn main() -> anyhow::Result<()> {
102 /// let args = Args::parse();
103 /// let mut writer = args.output.into_async_writer().await?;
104 /// let mut buf = vec![0;8];
105 /// writer.write_all(&mut buf).await?;
106 /// # Ok(())
107 /// # }
108 /// ```
109 pub async fn into_async_writer(&self) -> std::io::Result<impl tokio::io::AsyncWrite> {
110 let output: std::pin::Pin<Box<dyn tokio::io::AsyncWrite + 'static>> = match &self.dest {
111 Dest::Stdout => Box::pin(tokio::io::stdout()),
112 Dest::Arg(filepath) => {
113 let f = tokio::fs::File::open(filepath).await?;
114 Box::pin(f)
115 }
116 };
117 Ok(output)
118 }
119}
120
121impl FromStr for FileOrStdout {
122 type Err = std::io::Error;
123
124 fn from_str(s: &str) -> Result<Self, Self::Err> {
125 let dest = Dest::from_str(s)?;
126 Ok(Self { dest })
127 }
128}
129
130#[test]
131fn test_source_methods() {
132 let val: FileOrStdout = "-".parse().unwrap();
133 assert!(val.is_stdout());
134 assert!(!val.is_file());
135 assert_eq!(val.filename(), "-");
136
137 let val: FileOrStdout = "/path/to/something".parse().unwrap();
138 assert!(val.is_file());
139 assert!(!val.is_stdout());
140 assert_eq!(val.filename(), "/path/to/something");
141}