patharg/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! Most CLI commands that take file paths as arguments follow the convention
3//! of treating a path of `-` (a single hyphen/dash) as referring to either
4//! standard input or standard output (depending on whether the path is read
5//! from or written to).  The `patharg` crate lets your programs follow this
6//! convention too: it provides [`InputArg`] and [`OutputArg`] types that wrap
7//! command-line arguments, with methods for reading from/writing to either the
8//! given path or — if the argument is just a hyphen — the appropriate standard
9//! stream.
10//!
11//! `InputArg` and `OutputArg` implement `From<OsString>`, `From<String>`, and
12//! `FromStr`, so you can use them seamlessly with your favorite Rust source of
13//! command-line arguments, be it [`clap`][], [`lexopt`][], plain old
14//! [`std::env::args`]/[`std::env::args_os`], or whatever else is out there.
15//! The source repository contains examples of two of these:
16//!
17//! - [`flipcase`][] and [`tokio-flipcase`][] show how to use this crate with
18//!   `clap`.
19//!
20//! - [`revchars`][] and [`tokio-revchars`][] show how to use this crate with
21//!   `lexopt`.
22//!
23//! [`clap`]: https://crates.io/crates/clap
24//! [`lexopt`]: https://crates.io/crates/lexopt
25//! [`flipcase`]: https://github.com/jwodder/patharg/tree/master/examples/flipcase/
26//! [`tokio-flipcase`]: https://github.com/jwodder/patharg/tree/master/examples/tokio-flipcase/
27//! [`revchars`]: https://github.com/jwodder/patharg/tree/master/examples/revchars/
28//! [`tokio-revchars`]: https://github.com/jwodder/patharg/tree/master/examples/tokio-revchars/
29//!
30//! Features
31//! ========
32//!
33//! The `patharg` crate has the following optional features.  None of them are
34//! enabled by default.
35//!
36//! - `serde` — Enables serialization & deserialization of `InputArg` and
37//!   `OutputArg` values with [`serde`]
38//!
39//! - `tokio` — Enables using `InputArg` and `OutputArg` values for
40//!   asynchronous I/O with [`tokio`]
41//!
42//! Comparison with clio
43//! ====================
44//!
45//! The only other library I am aware of that provides similar functionality to
46//! `patharg` is [`clio`][].  Compared to `clio`, `patharg` aims to be a much
47//! simpler, smaller library that doesn't try to be too clever.  Major
48//! differences between the libraries include:
49//!
50//! - When a `clio` path instance is created, `clio` will either (depending on
51//!   the type used) open the path immediately — which can lead to empty files
52//!   being needlessly left behind if an output file is constructed during
53//!   argument processing but an error occurs before the file is actually used
54//!   — or else check that the path can be opened — which is vulnerable to
55//!   TOCTTOU bugs.  `patharg` does no such thing.
56//!
57//! - `clio` supports reading from & writing to HTTP(S) URLs and has special
58//!   treatment for FIFOs.  `patharg` sees no need for such excesses.
59//!
60//! - `patharg` has a feature for allowing async I/O with [`tokio`].  `clio`
61//!   does not.
62//!
63//! - `patharg` has optional support for [`serde`].  `clio` does not.
64//!
65//! [`clio`]: https://crates.io/crates/clio
66
67use cfg_if::cfg_if;
68use either::Either;
69use std::ffi::OsString;
70use std::fmt;
71use std::fs;
72use std::io::{self, BufRead, BufReader, Read, StdinLock, StdoutLock, Write};
73use std::path::{Path, PathBuf};
74use std::str::FromStr;
75
76cfg_if! {
77    if #[cfg(feature = "serde")] {
78        use serde::de::Deserializer;
79        use serde::ser::Serializer;
80        use serde::{Deserialize, Serialize};
81    }
82}
83
84cfg_if! {
85    if #[cfg(feature = "tokio")] {
86        use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt};
87        use tokio_util::either::Either as AsyncEither;
88        use tokio_stream::wrappers::LinesStream;
89    }
90}
91
92/// An input path that can refer to either standard input or a file system path
93#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
94pub enum InputArg {
95    /// Refers to standard input.
96    ///
97    /// This is the variant returned by `InputArg::default()`.
98    #[default]
99    Stdin,
100
101    /// Refers to a file system path (stored in `.0`)
102    Path(PathBuf),
103}
104
105impl InputArg {
106    /// Construct an `InputArg` from a string, usually one taken from
107    /// command-line arguments.  If the string equals `"-"` (i.e., it contains
108    /// only a single hyphen/dash), [`InputArg::Stdin`] is returned; otherwise,
109    /// an [`InputArg::Path`] is returned.
110    ///
111    /// # Example
112    ///
113    /// ```
114    /// use patharg::InputArg;
115    /// use std::path::PathBuf;
116    ///
117    /// let p1 = InputArg::from_arg("-");
118    /// assert_eq!(p1, InputArg::Stdin);
119    ///
120    /// let p2 = InputArg::from_arg("./-");
121    /// assert_eq!(p2, InputArg::Path(PathBuf::from("./-")));
122    /// ```
123    pub fn from_arg<S: Into<PathBuf>>(arg: S) -> InputArg {
124        let arg = arg.into();
125        if arg == Path::new("-") {
126            InputArg::Stdin
127        } else {
128            InputArg::Path(arg)
129        }
130    }
131
132    /// Returns true if the input arg is the `Stdin` variant of `InputArg`.
133    ///
134    /// # Example
135    ///
136    /// ```
137    /// use patharg::InputArg;
138    ///
139    /// let p1 = InputArg::from_arg("-");
140    /// assert!(p1.is_stdin());
141    ///
142    /// let p2 = InputArg::from_arg("file.txt");
143    /// assert!(!p2.is_stdin());
144    /// ```
145    pub fn is_stdin(&self) -> bool {
146        self == &InputArg::Stdin
147    }
148
149    /// Returns true if the input arg is the `Path` variant of `InputArg`.
150    ///
151    /// # Example
152    ///
153    /// ```
154    /// use patharg::InputArg;
155    ///
156    /// let p1 = InputArg::from_arg("-");
157    /// assert!(!p1.is_path());
158    ///
159    /// let p2 = InputArg::from_arg("file.txt");
160    /// assert!(p2.is_path());
161    /// ```
162    pub fn is_path(&self) -> bool {
163        matches!(self, InputArg::Path(_))
164    }
165
166    /// Retrieve a reference to the inner [`PathBuf`].  If the input arg is
167    /// the `Stdin` variant, this returns `None`.
168    ///
169    /// # Example
170    ///
171    /// ```
172    /// use patharg::InputArg;
173    /// use std::path::PathBuf;
174    ///
175    /// let p1 = InputArg::from_arg("-");
176    /// assert_eq!(p1.path_ref(), None);
177    ///
178    /// let p2 = InputArg::from_arg("file.txt");
179    /// assert_eq!(p2.path_ref(), Some(&PathBuf::from("file.txt")));
180    /// ```
181    pub fn path_ref(&self) -> Option<&PathBuf> {
182        match self {
183            InputArg::Stdin => None,
184            InputArg::Path(p) => Some(p),
185        }
186    }
187
188    /// Retrieve a mutable reference to the inner [`PathBuf`].  If the input
189    /// arg is the `Stdin` variant, this returns `None`.
190    ///
191    /// # Example
192    ///
193    /// ```
194    /// use patharg::InputArg;
195    /// use std::path::PathBuf;
196    ///
197    /// let mut p1 = InputArg::from_arg("-");
198    /// assert_eq!(p1.path_mut(), None);
199    ///
200    /// let mut p2 = InputArg::from_arg("file.txt");
201    /// assert_eq!(p2.path_mut(), Some(&mut PathBuf::from("file.txt")));
202    /// ```
203    pub fn path_mut(&mut self) -> Option<&mut PathBuf> {
204        match self {
205            InputArg::Stdin => None,
206            InputArg::Path(p) => Some(p),
207        }
208    }
209
210    /// Consume the input arg and return the inner [`PathBuf`].  If the input
211    /// arg is the `Stdin` variant, this returns `None`.
212    ///
213    /// # Example
214    ///
215    /// ```
216    /// use patharg::InputArg;
217    /// use std::path::PathBuf;
218    ///
219    /// let p1 = InputArg::from_arg("-");
220    /// assert_eq!(p1.into_path(), None);
221    ///
222    /// let p2 = InputArg::from_arg("file.txt");
223    /// assert_eq!(p2.into_path(), Some(PathBuf::from("file.txt")));
224    /// ```
225    pub fn into_path(self) -> Option<PathBuf> {
226        match self {
227            InputArg::Stdin => None,
228            InputArg::Path(p) => Some(p),
229        }
230    }
231
232    /// Open the input arg for reading.
233    ///
234    /// If the input arg is the `Stdin` variant, this returns a locked
235    /// reference to stdin.  Otherwise, if the path arg is a `Path` variant,
236    /// the given path is opened for reading.
237    ///
238    /// The returned reader implements [`std::io::BufRead`].
239    ///
240    /// # Errors
241    ///
242    /// Has the same error conditions as [`std::fs::File::open`].
243    ///
244    /// # Example
245    ///
246    /// ```no_run
247    /// use patharg::InputArg;
248    /// use std::env::args_os;
249    /// use std::io::{self, Read};
250    ///
251    /// fn main() -> io::Result<()> {
252    ///     let infile = args_os().nth(1)
253    ///                           .map(InputArg::from_arg)
254    ///                           .unwrap_or_default();
255    ///     let mut f = infile.open()?;
256    ///     let mut buffer = [0; 16];
257    ///     let n = f.read(&mut buffer)?;
258    ///     println!("First {} bytes: {:?}", n, &buffer[..n]);
259    ///     Ok(())
260    /// }
261    /// ```
262    pub fn open(&self) -> io::Result<InputArgReader> {
263        Ok(match self {
264            InputArg::Stdin => Either::Left(io::stdin().lock()),
265            InputArg::Path(p) => Either::Right(BufReader::new(fs::File::open(p)?)),
266        })
267    }
268
269    /// Read the entire contents of the input arg into a bytes vector.
270    ///
271    /// If the input arg is the `Stdin` variant, the entire contents of stdin
272    /// are read.  Otherwise, if the input arg is a `Path` variant, the
273    /// contents of the given path are read.
274    ///
275    /// # Errors
276    ///
277    /// Has the same error conditions as [`std::io::Read::read_to_end`] and
278    /// [`std::fs::read`].
279    ///
280    /// # Example
281    ///
282    /// ```no_run
283    /// use patharg::InputArg;
284    /// use std::env::args_os;
285    /// use std::io;
286    ///
287    /// fn main() -> io::Result<()> {
288    ///     let infile = args_os().nth(1)
289    ///                           .map(InputArg::from_arg)
290    ///                           .unwrap_or_default();
291    ///     let input = infile.read()?;
292    ///     println!("Read {} bytes from input", input.len());
293    ///     Ok(())
294    /// }
295    /// ```
296    pub fn read(&self) -> io::Result<Vec<u8>> {
297        match self {
298            InputArg::Stdin => {
299                let mut vec = Vec::new();
300                io::stdin().lock().read_to_end(&mut vec)?;
301                Ok(vec)
302            }
303            InputArg::Path(p) => fs::read(p),
304        }
305    }
306
307    /// Read the entire contents of the input arg into a string.
308    ///
309    /// If the input arg is the `Stdin` variant, the entire contents of stdin
310    /// are read.  Otherwise, if the input arg is a `Path` variant, the
311    /// contents of the given path are read.
312    ///
313    /// # Errors
314    ///
315    /// Has the same error conditions as [`std::io::read_to_string`] and
316    /// [`std::fs::read_to_string`].
317    ///
318    /// # Example
319    ///
320    /// ```no_run
321    /// use patharg::InputArg;
322    /// use std::env::args_os;
323    /// use std::io;
324    ///
325    /// fn main() -> io::Result<()> {
326    ///     let infile = args_os().nth(1)
327    ///                           .map(InputArg::from_arg)
328    ///                           .unwrap_or_default();
329    ///     let input = infile.read_to_string()?;
330    ///     println!("Read {} characters from input", input.len());
331    ///     Ok(())
332    /// }
333    /// ```
334    pub fn read_to_string(&self) -> io::Result<String> {
335        match self {
336            InputArg::Stdin => io::read_to_string(io::stdin().lock()),
337            InputArg::Path(p) => fs::read_to_string(p),
338        }
339    }
340
341    /// Return an iterator over the lines of the input arg.
342    ///
343    /// If the input arg is the `Stdin` variant, this locks stdin and returns
344    /// an iterator over its lines; the lock is released once the iterator is
345    /// dropped.  Otherwise, if the input arg is a `Path` variant, the given
346    /// path is opened for reading, and an iterator over its lines is returned.
347    ///
348    /// The returned iterator yields instances of `std::io::Result<String>`,
349    /// where each individual item has the same error conditions as
350    /// [`std::io::BufRead::read_line()`].
351    ///
352    /// # Errors
353    ///
354    /// Has the same error conditions as [`InputArg::open()`].
355    ///
356    /// # Example
357    ///
358    /// ```no_run
359    /// use patharg::InputArg;
360    /// use std::env::args_os;
361    /// use std::io;
362    ///
363    /// fn main() -> io::Result<()> {
364    ///     let infile = args_os().nth(1)
365    ///                           .map(InputArg::from_arg)
366    ///                           .unwrap_or_default();
367    ///     for (i, r) in infile.lines()?.enumerate() {
368    ///         let line = r?;
369    ///         println!("Line {} is {} characters long.", i + 1, line.len());
370    ///     }
371    ///     Ok(())
372    /// }
373    /// ```
374    pub fn lines(&self) -> io::Result<Lines> {
375        Ok(self.open()?.lines())
376    }
377}
378
379#[cfg(feature = "tokio")]
380#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
381impl InputArg {
382    /// Asynchronously open the input arg for reading.
383    ///
384    /// If the input arg is the `Stdin` variant, this returns a reference to
385    /// stdin.  Otherwise, if the path arg is a `Path` variant, the given path
386    /// is opened for reading.
387    ///
388    /// The returned reader implements [`tokio::io::AsyncRead`].
389    ///
390    /// # Errors
391    ///
392    /// Has the same error conditions as [`tokio::fs::File::open`].
393    ///
394    /// # Example
395    ///
396    /// ```no_run
397    /// use patharg::InputArg;
398    /// use std::env::args_os;
399    /// use tokio::io::AsyncReadExt;
400    ///
401    /// #[tokio::main]
402    /// async fn main() -> std::io::Result<()> {
403    ///     let infile = args_os().nth(1)
404    ///                           .map(InputArg::from_arg)
405    ///                           .unwrap_or_default();
406    ///     let mut f = infile.async_open().await?;
407    ///     let mut buffer = [0; 16];
408    ///     let n = f.read(&mut buffer).await?;
409    ///     println!("First {} bytes: {:?}", n, &buffer[..n]);
410    ///     Ok(())
411    /// }
412    /// ```
413    pub async fn async_open(&self) -> io::Result<AsyncInputArgReader> {
414        Ok(match self {
415            InputArg::Stdin => AsyncEither::Left(tokio::io::stdin()),
416            InputArg::Path(p) => AsyncEither::Right(tokio::fs::File::open(p).await?),
417        })
418    }
419
420    /// Asynchronously read the entire contents of the input arg into a bytes
421    /// vector.
422    ///
423    /// If the input arg is the `Stdin` variant, the entire contents of stdin
424    /// are read.  Otherwise, if the input arg is a `Path` variant, the
425    /// contents of the given path are read.
426    ///
427    /// # Errors
428    ///
429    /// Has the same error conditions as
430    /// [`tokio::io::AsyncReadExt::read_to_end`] and [`tokio::fs::read`].
431    ///
432    /// # Example
433    ///
434    /// ```no_run
435    /// use patharg::InputArg;
436    /// use std::env::args_os;
437    ///
438    /// #[tokio::main]
439    /// async fn main() -> std::io::Result<()> {
440    ///     let infile = args_os().nth(1)
441    ///                           .map(InputArg::from_arg)
442    ///                           .unwrap_or_default();
443    ///     let input = infile.async_read().await?;
444    ///     println!("Read {} bytes from input", input.len());
445    ///     Ok(())
446    /// }
447    /// ```
448    pub async fn async_read(&self) -> io::Result<Vec<u8>> {
449        match self {
450            InputArg::Stdin => {
451                let mut vec = Vec::new();
452                tokio::io::stdin().read_to_end(&mut vec).await?;
453                Ok(vec)
454            }
455            InputArg::Path(p) => tokio::fs::read(p).await,
456        }
457    }
458
459    /// Asynchronously read the entire contents of the input arg into a string.
460    ///
461    /// If the input arg is the `Stdin` variant, the entire contents of stdin
462    /// are read.  Otherwise, if the input arg is a `Path` variant, the
463    /// contents of the given path are read.
464    ///
465    /// # Errors
466    ///
467    /// Has the same error conditions as
468    /// [`tokio::io::AsyncReadExt::read_to_string`] and
469    /// [`tokio::fs::read_to_string`].
470    ///
471    /// # Example
472    ///
473    /// ```no_run
474    /// use patharg::InputArg;
475    /// use std::env::args_os;
476    ///
477    /// #[tokio::main]
478    /// async fn main() -> std::io::Result<()> {
479    ///     let infile = args_os().nth(1)
480    ///                           .map(InputArg::from_arg)
481    ///                           .unwrap_or_default();
482    ///     let input = infile.async_read_to_string().await?;
483    ///     println!("Read {} characters from input", input.len());
484    ///     Ok(())
485    /// }
486    /// ```
487    pub async fn async_read_to_string(&self) -> io::Result<String> {
488        match self {
489            InputArg::Stdin => {
490                let mut s = String::new();
491                tokio::io::stdin().read_to_string(&mut s).await?;
492                Ok(s)
493            }
494            InputArg::Path(p) => tokio::fs::read_to_string(p).await,
495        }
496    }
497
498    /// Return a stream over the lines of the input arg.
499    ///
500    /// If the input arg is the `Stdin` variant, this returns a stream over the
501    /// lines of stdin.  Otherwise, if the input arg is a `Path` variant, the
502    /// given path is opened for reading, and a stream over its lines is
503    /// returned.
504    ///
505    /// The returned stream yields instances of `std::io::Result<String>`,
506    /// where each individual item has the same error conditions as
507    /// [`tokio::io::AsyncBufReadExt::read_line()`].
508    ///
509    /// # Errors
510    ///
511    /// Has the same error conditions as [`InputArg::async_open()`].
512    ///
513    /// # Example
514    ///
515    /// ```no_run
516    /// use patharg::InputArg;
517    /// use std::env::args_os;
518    /// use tokio_stream::StreamExt;
519    ///
520    /// #[tokio::main]
521    /// async fn main() -> std::io::Result<()> {
522    ///     let infile = args_os().nth(1)
523    ///                           .map(InputArg::from_arg)
524    ///                           .unwrap_or_default();
525    ///     let mut i = 1;
526    ///     let mut stream = infile.async_lines().await?;
527    ///     while let Some(r) = stream.next().await {
528    ///         let line = r?;
529    ///         println!("Line {} is {} characters long.", i, line.len());
530    ///         i += 1;
531    ///     }
532    ///     Ok(())
533    /// }
534    /// ```
535    pub async fn async_lines(&self) -> io::Result<AsyncLines> {
536        Ok(LinesStream::new(
537            tokio::io::BufReader::new(self.async_open().await?).lines(),
538        ))
539    }
540}
541
542impl fmt::Display for InputArg {
543    /// Displays [`InputArg::Stdin`] as `-` (a single hyphen/dash) or as
544    /// `<stdin>` if the `{:#}` format is used.  Always displays
545    /// [`InputArg::Path`] using [`std::path::Path::display()`].
546    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547        match self {
548            // IMPORTANT: The default Display of Stdin has to round-trip back
549            // to Stdin so that InputArg will work properly when used with
550            // clap's `default_value_t`.
551            InputArg::Stdin => {
552                if f.alternate() {
553                    write!(f, "<stdin>")
554                } else {
555                    write!(f, "-")
556                }
557            }
558            InputArg::Path(p) => write!(f, "{}", p.display()),
559        }
560    }
561}
562
563impl<S: Into<PathBuf>> From<S> for InputArg {
564    /// Convert a string to an [`InputArg`] using [`InputArg::from_arg()`].
565    fn from(s: S) -> InputArg {
566        InputArg::from_arg(s)
567    }
568}
569
570impl FromStr for InputArg {
571    type Err = std::convert::Infallible;
572
573    /// Convert a string to an [`InputArg`] using [`InputArg::from_arg()`].
574    fn from_str(s: &str) -> Result<InputArg, Self::Err> {
575        Ok(InputArg::from_arg(s))
576    }
577}
578
579impl From<InputArg> for OsString {
580    /// Convert an [`InputArg`] back to an `OsString`: `InputArg::Stdin`
581    /// becomes `"-"`, and `InputArg::Path(p)` becomes `p.into()`.
582    fn from(arg: InputArg) -> OsString {
583        match arg {
584            InputArg::Stdin => OsString::from("-"),
585            InputArg::Path(p) => p.into(),
586        }
587    }
588}
589
590#[cfg(feature = "serde")]
591#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
592impl Serialize for InputArg {
593    /// Serializes [`InputArg::Stdin`] as `"-"` (a string containing a single
594    /// hyphen/dash).  Serializes [`InputArg::Path`] as the inner [`PathBuf`];
595    /// this will fail if the path is not valid UTF-8.
596    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
597        match self {
598            InputArg::Stdin => "-".serialize(serializer),
599            InputArg::Path(p) => p.serialize(serializer),
600        }
601    }
602}
603
604#[cfg(feature = "serde")]
605#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
606impl<'de> Deserialize<'de> for InputArg {
607    /// Deserializes a [`PathBuf`] and converts it to an `InputArg` with
608    /// [`InputArg::from_arg()`].
609    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
610    where
611        D: Deserializer<'de>,
612    {
613        PathBuf::deserialize(deserializer).map(InputArg::from_arg)
614    }
615}
616
617/// An output path that can refer to either standard output or a file system
618/// path
619#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
620pub enum OutputArg {
621    /// Refers to standard output.
622    ///
623    /// This is the variant returned by `OutputArg::default()`.
624    #[default]
625    Stdout,
626
627    /// Refers to a file system path (stored in `.0`)
628    Path(PathBuf),
629}
630
631impl OutputArg {
632    /// Construct a `OutputArg` from a string, usually one taken from
633    /// command-line arguments.  If the string equals `"-"` (i.e., it contains
634    /// only a single hyphen/dash), [`OutputArg::Stdout`] is returned;
635    /// otherwise, an [`OutputArg::Path`] is returned.
636    ///
637    /// # Example
638    ///
639    /// ```
640    /// use patharg::OutputArg;
641    /// use std::path::PathBuf;
642    ///
643    /// let p1 = OutputArg::from_arg("-");
644    /// assert_eq!(p1, OutputArg::Stdout);
645    ///
646    /// let p2 = OutputArg::from_arg("./-");
647    /// assert_eq!(p2, OutputArg::Path(PathBuf::from("./-")));
648    /// ```
649    pub fn from_arg<S: Into<PathBuf>>(arg: S) -> OutputArg {
650        let arg = arg.into();
651        if arg == Path::new("-") {
652            OutputArg::Stdout
653        } else {
654            OutputArg::Path(arg)
655        }
656    }
657
658    /// Returns true if the output arg is the `Stdout` variant of `OutputArg`.
659    ///
660    /// # Example
661    ///
662    /// ```
663    /// use patharg::OutputArg;
664    ///
665    /// let p1 = OutputArg::from_arg("-");
666    /// assert!(p1.is_stdout());
667    ///
668    /// let p2 = OutputArg::from_arg("file.txt");
669    /// assert!(!p2.is_stdout());
670    /// ```
671    pub fn is_stdout(&self) -> bool {
672        self == &OutputArg::Stdout
673    }
674
675    /// Returns true if the output arg is the `Path` variant of `OutputArg`.
676    ///
677    /// # Example
678    ///
679    /// ```
680    /// use patharg::OutputArg;
681    ///
682    /// let p1 = OutputArg::from_arg("-");
683    /// assert!(!p1.is_path());
684    ///
685    /// let p2 = OutputArg::from_arg("file.txt");
686    /// assert!(p2.is_path());
687    /// ```
688    pub fn is_path(&self) -> bool {
689        matches!(self, OutputArg::Path(_))
690    }
691
692    /// Retrieve a reference to the inner [`PathBuf`].  If the output arg is
693    /// the `Stdout` variant, this returns `None`.
694    ///
695    /// # Example
696    ///
697    /// ```
698    /// use patharg::OutputArg;
699    /// use std::path::PathBuf;
700    ///
701    /// let p1 = OutputArg::from_arg("-");
702    /// assert_eq!(p1.path_ref(), None);
703    ///
704    /// let p2 = OutputArg::from_arg("file.txt");
705    /// assert_eq!(p2.path_ref(), Some(&PathBuf::from("file.txt")));
706    /// ```
707    pub fn path_ref(&self) -> Option<&PathBuf> {
708        match self {
709            OutputArg::Stdout => None,
710            OutputArg::Path(p) => Some(p),
711        }
712    }
713
714    /// Retrieve a mutable reference to the inner [`PathBuf`].  If the output
715    /// arg is the `Stdout` variant, this returns `None`.
716    ///
717    /// # Example
718    ///
719    /// ```
720    /// use patharg::OutputArg;
721    /// use std::path::PathBuf;
722    ///
723    /// let mut p1 = OutputArg::from_arg("-");
724    /// assert_eq!(p1.path_mut(), None);
725    ///
726    /// let mut p2 = OutputArg::from_arg("file.txt");
727    /// assert_eq!(p2.path_mut(), Some(&mut PathBuf::from("file.txt")));
728    /// ```
729    pub fn path_mut(&mut self) -> Option<&mut PathBuf> {
730        match self {
731            OutputArg::Stdout => None,
732            OutputArg::Path(p) => Some(p),
733        }
734    }
735
736    /// Consume the output arg and return the inner [`PathBuf`].  If the output
737    /// arg is the `Stdout` variant, this returns `None`.
738    ///
739    /// # Example
740    ///
741    /// ```
742    /// use patharg::OutputArg;
743    /// use std::path::PathBuf;
744    ///
745    /// let p1 = OutputArg::from_arg("-");
746    /// assert_eq!(p1.into_path(), None);
747    ///
748    /// let p2 = OutputArg::from_arg("file.txt");
749    /// assert_eq!(p2.into_path(), Some(PathBuf::from("file.txt")));
750    /// ```
751    pub fn into_path(self) -> Option<PathBuf> {
752        match self {
753            OutputArg::Stdout => None,
754            OutputArg::Path(p) => Some(p),
755        }
756    }
757
758    /// Open the output arg for writing.
759    ///
760    /// If the output arg is the `Stdout` variant, this returns a locked
761    /// reference to stdout.  Otherwise, if the output arg is a `Path` variant,
762    /// the given path is opened for writing; if the path does not exist, it is
763    /// created.
764    ///
765    /// The returned writer implements [`std::io::Write`].
766    ///
767    /// # Errors
768    ///
769    /// Has the same error conditions as [`std::fs::File::create`].
770    ///
771    /// # Example
772    ///
773    /// ```no_run
774    /// use patharg::OutputArg;
775    /// use std::env::args_os;
776    /// use std::io::{self, Write};
777    ///
778    /// fn main() -> io::Result<()> {
779    ///     let outfile = args_os().nth(1)
780    ///                            .map(OutputArg::from_arg)
781    ///                            .unwrap_or_default();
782    ///     let mut f = outfile.create()?;
783    ///     // The "{}" is replaced by either the output filepath or a hyphen.
784    ///     write!(&mut f, "I am writing to {}.", outfile)?;
785    ///     Ok(())
786    /// }
787    /// ```
788    pub fn create(&self) -> io::Result<OutputArgWriter> {
789        Ok(match self {
790            OutputArg::Stdout => Either::Left(io::stdout().lock()),
791            OutputArg::Path(p) => Either::Right(fs::File::create(p)?),
792        })
793    }
794
795    /// Write a slice as the entire contents of the output arg.
796    ///
797    /// If the output arg is the `Stdout` variant, the given data is written to
798    /// stdout.  Otherwise, if the output arg is a `Path` variant, the contents
799    /// of the given path are replaced with the given data; if the path does
800    /// not exist, it is created first.
801    ///
802    /// # Errors
803    ///
804    /// Has the same error conditions as [`std::io::Write::write_all`] and
805    /// [`std::fs::write`].
806    ///
807    /// # Example
808    ///
809    /// ```no_run
810    /// use patharg::OutputArg;
811    /// use std::env::args_os;
812    /// use std::io;
813    ///
814    /// fn main() -> io::Result<()> {
815    ///     let outfile = args_os().nth(1)
816    ///                            .map(OutputArg::from_arg)
817    ///                            .unwrap_or_default();
818    ///     outfile.write("This is the output arg's new content.\n")?;
819    ///     Ok(())
820    /// }
821    /// ```
822    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> io::Result<()> {
823        match self {
824            OutputArg::Stdout => io::stdout().lock().write_all(contents.as_ref()),
825            OutputArg::Path(p) => fs::write(p, contents),
826        }
827    }
828}
829
830#[cfg(feature = "tokio")]
831#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
832impl OutputArg {
833    /// Asynchronously open the output arg for writing.
834    ///
835    /// If the output arg is the `Stdout` variant, this returns a reference to
836    /// stdout.  Otherwise, if the output arg is a `Path` variant, the given
837    /// path is opened for writing; if the path does not exist, it is created.
838    ///
839    /// The returned writer implements [`tokio::io::AsyncWrite`].
840    ///
841    /// # Errors
842    ///
843    /// Has the same error conditions as [`tokio::fs::File::create`].
844    ///
845    /// # Example
846    ///
847    /// ```no_run
848    /// use patharg::OutputArg;
849    /// use std::env::args_os;
850    /// use tokio::io::AsyncWriteExt;
851    ///
852    /// #[tokio::main]
853    /// async fn main() -> std::io::Result<()> {
854    ///     let outfile = args_os().nth(1)
855    ///                            .map(OutputArg::from_arg)
856    ///                            .unwrap_or_default();
857    ///     let mut f = outfile.async_create().await?;
858    ///     // The "{}" is replaced by either the output filepath or a hyphen.
859    ///     let msg = format!("I am writing to {}.\n", outfile);
860    ///     f.write_all(msg.as_ref()).await?;
861    ///     Ok(())
862    /// }
863    /// ```
864    pub async fn async_create(&self) -> io::Result<AsyncOutputArgWriter> {
865        Ok(match self {
866            OutputArg::Stdout => AsyncEither::Left(tokio::io::stdout()),
867            OutputArg::Path(p) => AsyncEither::Right(tokio::fs::File::create(p).await?),
868        })
869    }
870
871    /// Asynchronously write a slice as the entire contents of the output arg.
872    ///
873    /// If the output arg is the `Stdout` variant, the given data is written to
874    /// stdout.  Otherwise, if the output arg is a `Path` variant, the contents
875    /// of the given path are replaced with the given data; if the path does
876    /// not exist, it is created first.
877    ///
878    /// # Errors
879    ///
880    /// Has the same error conditions as
881    /// [`tokio::io::AsyncWriteExt::write_all`] and [`tokio::fs::write`].
882    ///
883    /// # Example
884    ///
885    /// ```no_run
886    /// use patharg::OutputArg;
887    /// use std::env::args_os;
888    ///
889    /// #[tokio::main]
890    /// async fn main() -> std::io::Result<()> {
891    ///     let outfile = args_os().nth(1)
892    ///                            .map(OutputArg::from_arg)
893    ///                            .unwrap_or_default();
894    ///     outfile.async_write("This is the output arg's new content.\n").await?;
895    ///     Ok(())
896    /// }
897    /// ```
898    #[allow(clippy::future_not_send)] // The Future is Send if C is Send
899    pub async fn async_write<C: AsRef<[u8]>>(&self, contents: C) -> io::Result<()> {
900        match self {
901            OutputArg::Stdout => {
902                let mut stdout = tokio::io::stdout();
903                stdout.write_all(contents.as_ref()).await?;
904                stdout.flush().await
905            }
906            OutputArg::Path(p) => tokio::fs::write(p, contents).await,
907        }
908    }
909}
910
911impl fmt::Display for OutputArg {
912    /// Displays [`OutputArg::Stdout`] as `-` (a single hyphen/dash) or as
913    /// `<stdout>` if the `{:#}` format is used.  Always displays
914    /// [`OutputArg::Path`] using [`std::path::Path::display()`].
915    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
916        match self {
917            // IMPORTANT: The default Display of Stdout has to round-trip back
918            // to Stdout so that OutputArg will work properly when used with
919            // clap's `default_value_t`.
920            OutputArg::Stdout => {
921                if f.alternate() {
922                    write!(f, "<stdout>")
923                } else {
924                    write!(f, "-")
925                }
926            }
927            OutputArg::Path(p) => write!(f, "{}", p.display()),
928        }
929    }
930}
931
932impl<S: Into<PathBuf>> From<S> for OutputArg {
933    /// Convert an string to an [`OutputArg`] using [`OutputArg::from_arg()`].
934    fn from(s: S) -> OutputArg {
935        OutputArg::from_arg(s)
936    }
937}
938
939impl FromStr for OutputArg {
940    type Err = std::convert::Infallible;
941
942    /// Convert a string to an [`OutputArg`] using [`OutputArg::from_arg()`].
943    fn from_str(s: &str) -> Result<OutputArg, Self::Err> {
944        Ok(OutputArg::from_arg(s))
945    }
946}
947
948impl From<OutputArg> for OsString {
949    /// Convert an [`OutputArg`] back to an `OsString`: `OutputArg::Stdout`
950    /// becomes `"-"`, and `OutputArg::Path(p)` becomes `p.into()`.
951    fn from(arg: OutputArg) -> OsString {
952        match arg {
953            OutputArg::Stdout => OsString::from("-"),
954            OutputArg::Path(p) => p.into(),
955        }
956    }
957}
958
959#[cfg(feature = "serde")]
960#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
961impl Serialize for OutputArg {
962    /// Serializes [`OutputArg::Stdout`] as `"-"` (a string containing a single
963    /// hyphen/dash).  Serializes [`OutputArg::Path`] as the inner [`PathBuf`];
964    /// this will fail if the path is not valid UTF-8.
965    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
966        match self {
967            OutputArg::Stdout => "-".serialize(serializer),
968            OutputArg::Path(p) => p.serialize(serializer),
969        }
970    }
971}
972
973#[cfg(feature = "serde")]
974#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
975impl<'de> Deserialize<'de> for OutputArg {
976    /// Deserializes a [`PathBuf`] and converts it to an `OutputArg` with
977    /// [`OutputArg::from_arg()`].
978    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
979    where
980        D: Deserializer<'de>,
981    {
982        PathBuf::deserialize(deserializer).map(OutputArg::from_arg)
983    }
984}
985
986/// The type of the readers returned by [`InputArg::open()`].
987///
988/// This type implements [`std::io::BufRead`].
989pub type InputArgReader = Either<StdinLock<'static>, BufReader<fs::File>>;
990
991/// The type of the writers returned by [`OutputArg::create()`].
992///
993/// This type implements [`std::io::Write`].
994pub type OutputArgWriter = Either<StdoutLock<'static>, fs::File>;
995
996/// The type of the iterators returned by [`InputArg::lines()`].
997///
998/// This iterator yields instances of `std::io::Result<String>`.
999pub type Lines = io::Lines<InputArgReader>;
1000
1001cfg_if! {
1002    if #[cfg(feature = "tokio")] {
1003       /// The type of the asynchronous readers returned by
1004       /// [`InputArg::async_open()`].
1005       ///
1006       /// This type implements [`tokio::io::AsyncRead`].
1007       #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
1008       pub type AsyncInputArgReader = AsyncEither<tokio::io::Stdin, tokio::fs::File>;
1009
1010       /// The type of the asynchronous writers returned by
1011       /// [`OutputArg::async_create()`].
1012       ///
1013       /// This type implements [`tokio::io::AsyncWrite`].
1014       #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
1015       pub type AsyncOutputArgWriter = AsyncEither<tokio::io::Stdout, tokio::fs::File>;
1016
1017       /// The type of the streams returned by [`InputArg::async_lines()`].
1018       ///
1019       /// This stream yields instances of `std::io::Result<String>`.
1020       #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
1021       pub type AsyncLines = LinesStream<tokio::io::BufReader<AsyncInputArgReader>>;
1022    }
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027    use super::*;
1028    use std::ffi::OsStr;
1029
1030    mod inputarg {
1031        use super::*;
1032
1033        #[test]
1034        fn test_assert_stdin_from_osstring() {
1035            let s = OsString::from("-");
1036            let p = InputArg::from(s);
1037            assert!(p.is_stdin());
1038            assert!(!p.is_path());
1039        }
1040
1041        #[test]
1042        fn test_assert_path_from_osstring() {
1043            let s = OsString::from("./-");
1044            let p = InputArg::from(s);
1045            assert!(!p.is_stdin());
1046            assert!(p.is_path());
1047        }
1048
1049        #[test]
1050        fn test_assert_stdin_from_osstr() {
1051            let s = OsStr::new("-");
1052            let p = InputArg::from(s);
1053            assert!(p.is_stdin());
1054            assert!(!p.is_path());
1055        }
1056
1057        #[test]
1058        fn test_assert_path_from_osstr() {
1059            let s = OsStr::new("./-");
1060            let p = InputArg::from(s);
1061            assert!(!p.is_stdin());
1062            assert!(p.is_path());
1063        }
1064
1065        #[test]
1066        fn test_assert_stdin_from_pathbuf() {
1067            let s = PathBuf::from("-");
1068            let p = InputArg::from(s);
1069            assert!(p.is_stdin());
1070            assert!(!p.is_path());
1071        }
1072
1073        #[test]
1074        fn test_assert_path_from_pathbuf() {
1075            let s = PathBuf::from("./-");
1076            let p = InputArg::from(s);
1077            assert!(!p.is_stdin());
1078            assert!(p.is_path());
1079        }
1080
1081        #[test]
1082        fn test_assert_stdin_from_path() {
1083            let s = Path::new("-");
1084            let p = InputArg::from(s);
1085            assert!(p.is_stdin());
1086            assert!(!p.is_path());
1087        }
1088
1089        #[test]
1090        fn test_assert_path_from_path() {
1091            let s = Path::new("./-");
1092            let p = InputArg::from(s);
1093            assert!(!p.is_stdin());
1094            assert!(p.is_path());
1095        }
1096
1097        #[test]
1098        fn test_assert_stdin_from_string() {
1099            let s = String::from("-");
1100            let p = InputArg::from(s);
1101            assert!(p.is_stdin());
1102            assert!(!p.is_path());
1103        }
1104
1105        #[test]
1106        fn test_assert_path_from_string() {
1107            let s = String::from("./-");
1108            let p = InputArg::from(s);
1109            assert!(!p.is_stdin());
1110            assert!(p.is_path());
1111        }
1112
1113        #[test]
1114        fn test_assert_stdin_from_str() {
1115            let p = InputArg::from("-");
1116            assert!(p.is_stdin());
1117            assert!(!p.is_path());
1118        }
1119
1120        #[test]
1121        fn test_assert_path_from_str() {
1122            let p = InputArg::from("./-");
1123            assert!(!p.is_stdin());
1124            assert!(p.is_path());
1125        }
1126
1127        #[test]
1128        fn test_assert_parse_stdin() {
1129            let p = "-".parse::<InputArg>().unwrap();
1130            assert!(p.is_stdin());
1131            assert!(!p.is_path());
1132        }
1133
1134        #[test]
1135        fn test_assert_parse_path() {
1136            let p = "./-".parse::<InputArg>().unwrap();
1137            assert!(!p.is_stdin());
1138            assert!(p.is_path());
1139        }
1140
1141        #[test]
1142        fn test_default() {
1143            assert_eq!(InputArg::default(), InputArg::Stdin);
1144        }
1145
1146        #[test]
1147        fn test_stdin_path_ref() {
1148            let p = InputArg::Stdin;
1149            assert_eq!(p.path_ref(), None);
1150        }
1151
1152        #[test]
1153        fn test_path_path_ref() {
1154            let p = InputArg::Path(PathBuf::from("-"));
1155            assert_eq!(p.path_ref(), Some(&PathBuf::from("-")));
1156        }
1157
1158        #[test]
1159        fn test_stdin_path_mut() {
1160            let mut p = InputArg::Stdin;
1161            assert_eq!(p.path_mut(), None);
1162        }
1163
1164        #[test]
1165        fn test_path_path_mut() {
1166            let mut p = InputArg::Path(PathBuf::from("-"));
1167            assert_eq!(p.path_mut(), Some(&mut PathBuf::from("-")));
1168        }
1169
1170        #[test]
1171        fn test_stdin_into_path() {
1172            let p = InputArg::Stdin;
1173            assert_eq!(p.into_path(), None);
1174        }
1175
1176        #[test]
1177        fn test_path_into_path() {
1178            let p = InputArg::Path(PathBuf::from("-"));
1179            assert_eq!(p.into_path(), Some(PathBuf::from("-")));
1180        }
1181
1182        #[test]
1183        fn test_display_stdin() {
1184            let p = InputArg::Stdin;
1185            assert_eq!(p.to_string(), "-");
1186        }
1187
1188        #[test]
1189        fn test_display_alternate_stdin() {
1190            let p = InputArg::Stdin;
1191            assert_eq!(format!("{p:#}"), "<stdin>");
1192        }
1193
1194        #[test]
1195        fn test_display_path() {
1196            let p = InputArg::from_arg("./-");
1197            assert_eq!(p.to_string(), "./-");
1198        }
1199
1200        #[test]
1201        fn test_stdin_into_osstring() {
1202            let p = InputArg::Stdin;
1203            assert_eq!(OsString::from(p), OsString::from("-"));
1204        }
1205
1206        #[test]
1207        fn test_path_into_osstring() {
1208            let p = InputArg::Path(PathBuf::from("./-"));
1209            assert_eq!(OsString::from(p), OsString::from("./-"));
1210        }
1211
1212        #[cfg(feature = "serde")]
1213        mod serding {
1214            use super::*;
1215
1216            #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
1217            struct Input {
1218                path: InputArg,
1219            }
1220
1221            #[test]
1222            fn test_stdin_to_json() {
1223                let val = Input {
1224                    path: InputArg::Stdin,
1225                };
1226                assert_eq!(serde_json::to_string(&val).unwrap(), r#"{"path":"-"}"#);
1227            }
1228
1229            #[test]
1230            fn test_path_to_json() {
1231                let val = Input {
1232                    path: InputArg::Path(PathBuf::from("foo.txt")),
1233                };
1234                assert_eq!(
1235                    serde_json::to_string(&val).unwrap(),
1236                    r#"{"path":"foo.txt"}"#
1237                );
1238            }
1239
1240            #[test]
1241            fn test_stdin_from_json() {
1242                let s = r#"{"path": "-"}"#;
1243                assert_eq!(
1244                    serde_json::from_str::<Input>(s).unwrap(),
1245                    Input {
1246                        path: InputArg::Stdin
1247                    }
1248                );
1249            }
1250
1251            #[test]
1252            fn test_path_from_json() {
1253                let s = r#"{"path": "./-"}"#;
1254                assert_eq!(
1255                    serde_json::from_str::<Input>(s).unwrap(),
1256                    Input {
1257                        path: InputArg::Path(PathBuf::from("./-"))
1258                    }
1259                );
1260            }
1261        }
1262    }
1263
1264    mod outputarg {
1265        use super::*;
1266
1267        #[test]
1268        fn test_assert_stdout_from_osstring() {
1269            let s = OsString::from("-");
1270            let p = OutputArg::from(s);
1271            assert!(p.is_stdout());
1272            assert!(!p.is_path());
1273        }
1274
1275        #[test]
1276        fn test_assert_path_from_osstring() {
1277            let s = OsString::from("./-");
1278            let p = OutputArg::from(s);
1279            assert!(!p.is_stdout());
1280            assert!(p.is_path());
1281        }
1282
1283        #[test]
1284        fn test_assert_stdout_from_osstr() {
1285            let s = OsStr::new("-");
1286            let p = OutputArg::from(s);
1287            assert!(p.is_stdout());
1288            assert!(!p.is_path());
1289        }
1290
1291        #[test]
1292        fn test_assert_path_from_osstr() {
1293            let s = OsStr::new("./-");
1294            let p = OutputArg::from(s);
1295            assert!(!p.is_stdout());
1296            assert!(p.is_path());
1297        }
1298
1299        #[test]
1300        fn test_assert_stdout_from_pathbuf() {
1301            let s = PathBuf::from("-");
1302            let p = OutputArg::from(s);
1303            assert!(p.is_stdout());
1304            assert!(!p.is_path());
1305        }
1306
1307        #[test]
1308        fn test_assert_path_from_pathbuf() {
1309            let s = PathBuf::from("./-");
1310            let p = OutputArg::from(s);
1311            assert!(!p.is_stdout());
1312            assert!(p.is_path());
1313        }
1314
1315        #[test]
1316        fn test_assert_stdout_from_path() {
1317            let s = Path::new("-");
1318            let p = OutputArg::from(s);
1319            assert!(p.is_stdout());
1320            assert!(!p.is_path());
1321        }
1322
1323        #[test]
1324        fn test_assert_path_from_path() {
1325            let s = Path::new("./-");
1326            let p = OutputArg::from(s);
1327            assert!(!p.is_stdout());
1328            assert!(p.is_path());
1329        }
1330
1331        #[test]
1332        fn test_assert_stdout_from_string() {
1333            let s = String::from("-");
1334            let p = OutputArg::from(s);
1335            assert!(p.is_stdout());
1336            assert!(!p.is_path());
1337        }
1338
1339        #[test]
1340        fn test_assert_path_from_string() {
1341            let s = String::from("./-");
1342            let p = OutputArg::from(s);
1343            assert!(!p.is_stdout());
1344            assert!(p.is_path());
1345        }
1346
1347        #[test]
1348        fn test_assert_stdout_from_str() {
1349            let p = OutputArg::from("-");
1350            assert!(p.is_stdout());
1351            assert!(!p.is_path());
1352        }
1353
1354        #[test]
1355        fn test_assert_path_from_str() {
1356            let p = OutputArg::from("./-");
1357            assert!(!p.is_stdout());
1358            assert!(p.is_path());
1359        }
1360
1361        #[test]
1362        fn test_assert_parse_stdin() {
1363            let p = "-".parse::<OutputArg>().unwrap();
1364            assert!(p.is_stdout());
1365            assert!(!p.is_path());
1366        }
1367
1368        #[test]
1369        fn test_assert_parse_path() {
1370            let p = "./-".parse::<OutputArg>().unwrap();
1371            assert!(!p.is_stdout());
1372            assert!(p.is_path());
1373        }
1374
1375        #[test]
1376        fn test_default() {
1377            assert_eq!(OutputArg::default(), OutputArg::Stdout);
1378        }
1379
1380        #[test]
1381        fn test_stdout_path_ref() {
1382            let p = OutputArg::Stdout;
1383            assert_eq!(p.path_ref(), None);
1384        }
1385
1386        #[test]
1387        fn test_path_path_ref() {
1388            let p = OutputArg::Path(PathBuf::from("-"));
1389            assert_eq!(p.path_ref(), Some(&PathBuf::from("-")));
1390        }
1391
1392        #[test]
1393        fn test_stdout_path_mut() {
1394            let mut p = OutputArg::Stdout;
1395            assert_eq!(p.path_mut(), None);
1396        }
1397
1398        #[test]
1399        fn test_path_path_mut() {
1400            let mut p = OutputArg::Path(PathBuf::from("-"));
1401            assert_eq!(p.path_mut(), Some(&mut PathBuf::from("-")));
1402        }
1403
1404        #[test]
1405        fn test_stdout_into_path() {
1406            let p = OutputArg::Stdout;
1407            assert_eq!(p.into_path(), None);
1408        }
1409
1410        #[test]
1411        fn test_path_into_path() {
1412            let p = OutputArg::Path(PathBuf::from("-"));
1413            assert_eq!(p.into_path(), Some(PathBuf::from("-")));
1414        }
1415
1416        #[test]
1417        fn test_display_stdout() {
1418            let p = OutputArg::Stdout;
1419            assert_eq!(p.to_string(), "-");
1420        }
1421
1422        #[test]
1423        fn test_display_alternate_stdout() {
1424            let p = OutputArg::Stdout;
1425            assert_eq!(format!("{p:#}"), "<stdout>");
1426        }
1427
1428        #[test]
1429        fn test_display_path() {
1430            let p = OutputArg::from_arg("./-");
1431            assert_eq!(p.to_string(), "./-");
1432        }
1433
1434        #[test]
1435        fn test_stdout_into_osstring() {
1436            let p = OutputArg::Stdout;
1437            assert_eq!(OsString::from(p), OsString::from("-"));
1438        }
1439
1440        #[test]
1441        fn test_path_into_osstring() {
1442            let p = OutputArg::Path(PathBuf::from("./-"));
1443            assert_eq!(OsString::from(p), OsString::from("./-"));
1444        }
1445
1446        #[cfg(feature = "tokio")]
1447        #[test]
1448        fn test_async_write_is_send_if_content_is_send() {
1449            fn require_send<T: Send>(_t: T) {}
1450            let p = OutputArg::default();
1451            let fut = p.async_write(b"This arg is Send.");
1452            require_send(fut);
1453        }
1454
1455        #[cfg(feature = "serde")]
1456        mod serding {
1457            use super::*;
1458
1459            #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
1460            struct Output {
1461                path: OutputArg,
1462            }
1463
1464            #[test]
1465            fn test_stdout_to_json() {
1466                let val = Output {
1467                    path: OutputArg::Stdout,
1468                };
1469                assert_eq!(serde_json::to_string(&val).unwrap(), r#"{"path":"-"}"#);
1470            }
1471
1472            #[test]
1473            fn test_path_to_json() {
1474                let val = Output {
1475                    path: OutputArg::Path(PathBuf::from("foo.txt")),
1476                };
1477                assert_eq!(
1478                    serde_json::to_string(&val).unwrap(),
1479                    r#"{"path":"foo.txt"}"#
1480                );
1481            }
1482
1483            #[test]
1484            fn test_stdout_from_json() {
1485                let s = r#"{"path": "-"}"#;
1486                assert_eq!(
1487                    serde_json::from_str::<Output>(s).unwrap(),
1488                    Output {
1489                        path: OutputArg::Stdout
1490                    }
1491                );
1492            }
1493
1494            #[test]
1495            fn test_path_from_json() {
1496                let s = r#"{"path": "./-"}"#;
1497                assert_eq!(
1498                    serde_json::from_str::<Output>(s).unwrap(),
1499                    Output {
1500                        path: OutputArg::Path(PathBuf::from("./-"))
1501                    }
1502                );
1503            }
1504        }
1505    }
1506}