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}