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