clap_io/
lib.rs

1// Copyright (c) 2023 Swift Navigation
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy of
4// this software and associated documentation files (the "Software"), to deal in
5// the Software without restriction, including without limitation the rights to
6// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7// the Software, and to permit persons to whom the Software is furnished to do so,
8// subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
20//! Add optional `--input` and `--output` flags to a clap command. If `--input` is not specified,
21//! it defaults to (locked) stdin. If `--output` is not specified, it defaults to (locked) stdout.
22//!
23//! # Examples
24//!
25//! Add get `--input` and `--output` flags to your program:
26//!
27//! ```rust,no_run
28//! use clap::Parser;
29//! use clap_io::InputOutput;
30//!
31//! #[derive(Parser)]
32//! struct Cli {
33//!     #[clap(flatten)]
34//!     io: InputOutput,
35//! }
36//!
37//! let cli = Cli::parse();
38//! let mut input = cli.io.input.open().unwrap();
39//! let mut output = cli.io.output.open().unwrap();
40//! std::io::copy(&mut input, &mut output).unwrap();
41//! ```
42//!
43//! Add just one:
44//!
45//! ```rust,no_run
46//! use clap::Parser;
47//! use clap_io::Input;
48//!
49//! #[derive(Parser)]
50//! struct Cli {
51//!    #[clap(long = "in")]
52//!     input: Input,
53//! }
54//!
55//! let cli = Cli::parse();
56//! eprintln!("is tty? {}", cli.input.is_tty());
57//! eprintln!("path? {:?}", cli.input.path());
58//! ```
59
60use std::{
61    ffi::{OsStr, OsString},
62    fmt,
63    fs::File,
64    io::{self, Read, Write},
65    path::{Path, PathBuf},
66    str::FromStr,
67};
68
69use clap::{Args, ValueHint};
70
71const STDIO: &str = "-";
72const STDIN: &str = "<stdin>";
73const STDOUT: &str = "<stdout>";
74
75/// Combined input and output options.
76#[derive(Debug, Args)]
77pub struct InputOutput {
78    /// Input file path
79    #[arg(
80        long,
81        default_value_os_t,
82        value_hint = ValueHint::FilePath,
83    )]
84    pub input: Input,
85
86    /// Output file path
87    #[arg(
88        long,
89        default_value_os_t,
90        value_hint = ValueHint::FilePath,
91    )]
92    pub output: Output,
93}
94
95/// Either a file or stdin.
96#[derive(Debug, Clone)]
97pub struct Input(Stream);
98
99impl Input {
100    /// Open the input stream.
101    pub fn open(self) -> io::Result<Box<dyn Read + 'static>> {
102        match self.0 {
103            Stream::File(_) => {
104                let file = self.open_file().unwrap()?;
105                Ok(Box::new(file))
106            }
107            Stream::Stdin { .. } => {
108                let stdin = self.open_stdin().unwrap();
109                Ok(Box::new(stdin))
110            }
111            Stream::Stdout { .. } => unreachable!("stdout is an output"),
112        }
113    }
114
115    /// Open the input as stdin.
116    pub fn open_stdin(self) -> Result<io::StdinLock<'static>, Self> {
117        match self.0 {
118            Stream::Stdin { .. } => {
119                let stdin = Box::leak(Box::new(io::stdin()));
120                Ok(stdin.lock())
121            }
122            _ => Err(self),
123        }
124    }
125
126    /// Open the input as a file.
127    pub fn open_file(&self) -> Option<io::Result<File>> {
128        match &self.0 {
129            Stream::File(path) => match File::open(&path) {
130                Ok(file) => Some(Ok(file)),
131                Err(e) => Some(Err(io::Error::new(
132                    e.kind(),
133                    format!(
134                        "Failed to open input file `{}`. Cause: {}",
135                        path.display(),
136                        e
137                    ),
138                ))),
139            },
140            _ => None,
141        }
142    }
143
144    /// Is this input a TTY?
145    pub fn is_tty(&self) -> bool {
146        self.0.is_tty()
147    }
148
149    /// If the input is a file get the path.
150    pub fn path(&self) -> Option<&Path> {
151        self.0.path()
152    }
153}
154
155impl Default for Input {
156    fn default() -> Self {
157        Self(Stream::stdin())
158    }
159}
160
161impl fmt::Display for Input {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        self.0.fmt(f)
164    }
165}
166
167impl FromStr for Input {
168    type Err = std::convert::Infallible;
169
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        Ok(Self::from(s.as_ref()))
172    }
173}
174
175impl From<&OsStr> for Input {
176    fn from(s: &OsStr) -> Self {
177        if s == STDIO || s == STDIN {
178            Self(Stream::stdin())
179        } else {
180            Self(Stream::file(s))
181        }
182    }
183}
184
185impl From<Input> for OsString {
186    fn from(input: Input) -> Self {
187        input.0.into()
188    }
189}
190
191/// Either a file or stdout.
192#[derive(Debug, Clone)]
193pub struct Output(Stream);
194
195impl Output {
196    /// Open the output stream.
197    pub fn open(self) -> io::Result<Box<dyn Write + 'static>> {
198        match self.0 {
199            Stream::File(_) => {
200                let file = self.open_file().unwrap()?;
201                Ok(Box::new(file))
202            }
203            Stream::Stdout { .. } => {
204                let stdout = self.open_stdout().unwrap();
205                Ok(Box::new(stdout))
206            }
207            Stream::Stdin { .. } => unreachable!("stdin is an input"),
208        }
209    }
210
211    /// Open the output as stdout.
212    pub fn open_stdout(self) -> Result<io::StdoutLock<'static>, Self> {
213        match self.0 {
214            Stream::Stdout { .. } => {
215                let stdout = Box::leak(Box::new(io::stdout()));
216                Ok(stdout.lock())
217            }
218            _ => Err(self),
219        }
220    }
221
222    /// Open the output as a file.
223    pub fn open_file(&self) -> Option<io::Result<File>> {
224        match &self.0 {
225            Stream::File(path) => match File::create(&path) {
226                Ok(file) => Some(Ok(file)),
227                Err(e) => Some(Err(io::Error::new(
228                    e.kind(),
229                    format!(
230                        "Failed to open output file `{}`. Cause: {}",
231                        path.display(),
232                        e
233                    ),
234                ))),
235            },
236            _ => None,
237        }
238    }
239
240    /// Is this output a TTY?
241    pub fn is_tty(&self) -> bool {
242        self.0.is_tty()
243    }
244
245    /// If the output is a file get the path.
246    pub fn path(&self) -> Option<&Path> {
247        self.0.path()
248    }
249}
250
251impl Default for Output {
252    fn default() -> Self {
253        Self(Stream::stdout())
254    }
255}
256
257impl fmt::Display for Output {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        self.0.fmt(f)
260    }
261}
262
263impl FromStr for Output {
264    type Err = std::convert::Infallible;
265
266    fn from_str(s: &str) -> Result<Self, Self::Err> {
267        Ok(Self::from(s.as_ref()))
268    }
269}
270
271impl From<&OsStr> for Output {
272    fn from(s: &OsStr) -> Self {
273        if s == STDIO || s == STDOUT {
274            Self(Stream::stdout())
275        } else {
276            Self(Stream::file(s))
277        }
278    }
279}
280
281impl From<Output> for OsString {
282    fn from(output: Output) -> Self {
283        output.0.into()
284    }
285}
286
287#[derive(Debug, Clone)]
288enum Stream {
289    File(PathBuf),
290    Stdin { tty: bool },
291    Stdout { tty: bool },
292}
293
294impl Stream {
295    fn file(path: &OsStr) -> Self {
296        Self::File(path.into())
297    }
298
299    fn stdin() -> Self {
300        Self::Stdin {
301            tty: atty::is(atty::Stream::Stdin),
302        }
303    }
304
305    fn stdout() -> Self {
306        Self::Stdout {
307            tty: atty::is(atty::Stream::Stdout),
308        }
309    }
310
311    fn is_tty(&self) -> bool {
312        matches!(self, Self::Stdin { tty } | Self::Stdout { tty } if *tty)
313    }
314
315    fn path(&self) -> Option<&Path> {
316        if let Self::File(v) = self {
317            Some(v)
318        } else {
319            None
320        }
321    }
322}
323
324impl fmt::Display for Stream {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        match self {
327            Self::File(path) => path.display().fmt(f),
328            Self::Stdin { .. } => STDIN.fmt(f),
329            Self::Stdout { .. } => STDOUT.fmt(f),
330        }
331    }
332}
333
334impl From<Stream> for OsString {
335    fn from(s: Stream) -> OsString {
336        match s {
337            Stream::File(path) => path.into(),
338            Stream::Stdin { .. } => STDIN.into(),
339            Stream::Stdout { .. } => STDOUT.into(),
340        }
341    }
342}