devx_cmd/
lib.rs

1//! `devx-cmd` provides more convenient primitives for spawning child processes
2//! than [`std::process`] targeted for use in development scripts specifically.
3//!
4//! The main entities of the crate are [`Cmd`] (builder for executable
5//! commands), and [`Child`] (represents a spawned process).
6//!
7//! There are also some convenient macros to reduce boilerplate.
8//! Here is the basic usage example:
9//!
10//! ```
11//! use devx_cmd::{read, run, cmd, Cmd};
12//!
13//! // Initialize some low-overhead logger implementation for the `log` crate
14//! simple_logger::SimpleLogger::new().init().unwrap();
15//!
16//! // Run the program, logging the invocation via [`log`] crate and waiting until it finishes
17//! // This is used only for side-effects.
18//! // Note that if the process ends with a non-zero status code, this will return an error.
19//! run!("ls", "-la")?;
20//!
21//! // Same as `run!()`, but captures the stdout and returns it as a `String`
22//! // there is also a `read_bytes!()` for non-utf8 sequences
23//! let output = read!("echo", "foo")?;
24//! assert_eq!(output.trim(), "foo");
25//!
26//! # if run!("rustfmt", "--version").is_ok() {
27//! let mut cmd = cmd!("rustfmt");
28//! cmd
29//!     // Set `trace` level for logging command invocation and output (`debug` by default)
30//!     .log_cmd(log::Level::Trace)
31//!     // Don't log error if the command fails
32//!     .log_err(None)
33//!     .stdin("fn foo () -> u32 {42}\n");
34//!
35//! // Spawn without waiting for its completion, but capturing the stdout
36//! let mut child = cmd.spawn_piped()?;
37//!
38//! // Read output line-by-line
39//! let first_line = child.stdout_lines().next().unwrap();
40//!
41//! assert_eq!(first_line.trim(), "fn foo() -> u32 {");
42//!
43//! // Dropping the child process `kill()`s it (and ignores the `Result`)
44//! // Use `.wait()/.read()` to wait until its completion.
45//! drop(child);
46//! # }
47//!
48//! # Ok::<(), devx_cmd::Error>(())
49//! ```
50
51#![warn(missing_docs)]
52#![warn(rust_2018_idioms)]
53// Makes rustc abort compilation if there are any unsafe blocks in the crate.
54// Presence of this annotation is picked up by tools such as cargo-geiger
55// and lets them ensure that there is indeed no unsafe code as opposed to
56// something they couldn't detect (e.g. unsafe added via macro expansion, etc).
57#![forbid(unsafe_code)]
58
59use std::{
60    borrow::Cow,
61    collections::HashMap,
62    env,
63    ffi::OsString,
64    fmt,
65    io::{self, Write},
66    iter,
67    path::{Path, PathBuf},
68    process::Stdio,
69    sync::Arc,
70};
71
72pub use error::*;
73use io::BufRead;
74
75/// Create a [`Cmd`] with the given binary and arguments.
76///
77/// The parameters to this macro may have completely different types.
78/// The single requirement for them is to implement [`Into`]`<`[`OsString`]`>`
79///
80/// ```
81/// # use devx_cmd::{cmd, Result};
82/// # use std::path::Path;
83/// #
84/// let path = Path::new("/foo/bar");
85///
86/// let cmd = cmd!("echo", "hi", path);
87/// cmd.run()?;
88/// #
89/// # Ok::<(), devx_cmd::Error>(())
90/// ```
91#[macro_export]
92macro_rules! cmd {
93    ($bin:expr $(, $arg:expr )* $(,)?) => {{
94        let mut cmd = $crate::Cmd::new($bin);
95        $(cmd.arg($arg);)*
96        cmd
97    }};
98}
99
100/// Shortcut for `cmd!(...).run()`.
101#[macro_export]
102macro_rules! run {
103    ($($params:tt)*) => {{ $crate::cmd!($($params)*).run() }}
104}
105
106/// Shortcut for `cmd!(...).read()`.
107#[macro_export]
108macro_rules! read {
109    ($($params:tt)*) => {{ $crate::cmd!($($params)*).read() }}
110}
111
112/// Shortcut for `cmd!(...).read_bytes()`.
113#[macro_export]
114macro_rules! read_bytes {
115    ($($params:tt)*) => {{ $crate::cmd!($($params)*).read_bytes() }}
116}
117
118mod error;
119
120#[derive(Clone)]
121enum BinOrUtf8 {
122    Bin(Vec<u8>),
123    Utf8(String),
124}
125
126impl fmt::Display for BinOrUtf8 {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            BinOrUtf8::Bin(bytes) => write!(f, "[bytes]:\n{:?}", bytes),
130            BinOrUtf8::Utf8(utf8) => write!(f, "[utf8]:\n{}", utf8),
131        }
132    }
133}
134
135impl AsRef<[u8]> for BinOrUtf8 {
136    fn as_ref(&self) -> &[u8] {
137        match self {
138            BinOrUtf8::Bin(it) => it.as_ref(),
139            BinOrUtf8::Utf8(it) => it.as_ref(),
140        }
141    }
142}
143
144/// More convenient version of [`std::process::Command`]. Allows for
145/// spawning child processes with or without capturing their stdout.
146/// It also comes with inbuilt logging of the invocations via [`log`] crate.
147///
148/// All the methods for invoking a [`Cmd`]:
149/// - [`Cmd::spawn_piped()`]
150/// - [`Cmd::spawn()`]
151/// - [`Cmd::spawn_with()`]
152/// - [`Cmd::run()`]
153/// - [`Cmd::read()`]
154/// - [`Cmd::read_bytes()`]
155///
156/// For more laconic usage see [`cmd`] and other macros.
157///
158/// Example:
159/// ```
160/// # use devx_cmd::{Cmd, Child, Result};
161/// #
162/// let mut cmd = Cmd::new("cargo");
163/// cmd
164///     // `arg*()` methods append arguments
165///     .arg("metadata")
166///     .arg2("--color", "never")
167///     .args(&["--verbose", "--no-deps", "--all-features"])
168///     .replace_arg(3, "--quiet")
169///     // These are at `debug` and `error` level by default, `None` disables logging
170///     .log_cmd(None)
171///     .log_err(log::Level::Warn)
172///     // repetated `stdin*()` calls overwrite previous ones
173///     .stdin("Hi")
174///     .stdin_bytes(vec![0, 1, 2]);
175///
176/// let () = cmd.run()?;
177/// let output: String = cmd.read()?;
178/// let output: Vec<u8> = cmd.read_bytes()?;
179/// let process: Child = cmd.spawn()?;
180/// #
181/// # Ok::<(), devx_cmd::Error>(())
182/// ```
183#[must_use = "commands are not executed until run(), read() or spawn() is called"]
184#[derive(Clone)]
185pub struct Cmd(Arc<CmdShared>);
186
187#[derive(Clone)]
188struct CmdShared {
189    bin: PathBuf,
190    args: Vec<OsString>,
191    env: HashMap<OsString, OsString>,
192    stdin: Option<BinOrUtf8>,
193    current_dir: Option<PathBuf>,
194    log_cmd: Option<log::Level>,
195    log_err: Option<log::Level>,
196}
197
198impl fmt::Debug for Cmd {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        fmt::Display::fmt(self, f)
201    }
202}
203
204impl fmt::Display for Cmd {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        write!(f, "{}", (self.0).bin.display())?;
207        for arg in &(self.0).args {
208            let arg = arg.to_string_lossy();
209            if arg.chars().any(char::is_whitespace) {
210                write!(f, " '{}'", arg)?;
211            } else {
212                write!(f, " {}", arg)?;
213            }
214        }
215        if let Some(dir) = &self.0.current_dir {
216            write!(f, "\n(at {})", dir.display())?;
217        }
218        if !self.0.env.is_empty() {
219            write!(f, "\nenv: {:#?}", self.0.env)?;
220        }
221        if let Some(stdin) = &self.0.stdin {
222            write!(f, "\nstdin <<< {}", stdin)?;
223        }
224        Ok(())
225    }
226}
227
228impl Cmd {
229    /// Returns a command builder that invokes the binary at `bin`.
230    /// You should also be able to pass the command by name if it is in `PATH`.
231    ///
232    /// Does not verify that the binary is actually available at the given path.
233    /// If it isn't, then an error will be returned when executing the command.
234    pub fn new(bin: impl Into<PathBuf>) -> Self {
235        Self(Arc::new(CmdShared {
236            bin: bin.into(),
237            args: Vec::new(),
238            env: HashMap::default(),
239            log_cmd: Some(log::Level::Debug),
240            log_err: Some(log::Level::Error),
241            stdin: None,
242            current_dir: None,
243        }))
244    }
245
246    /// Returns a command builder if there is some file available at `bin_path`.
247    /// If there is no file at the given path returns `None`.
248    /// Beware that this won't take `PATH` env variable into account.
249    /// This function expects a relative or absolute filesystem path to the binary,
250    /// and tries to check if there is some file there
251    /// (retrying with `.exe` extension on windows).
252    ///
253    /// If you want to find a binary through `PATH`, you should use
254    /// [`Cmd::lookup_in_path()`]
255    pub fn try_at(bin_path: impl Into<PathBuf>) -> Option<Self> {
256        // Compile time: reduces monomorphizations
257        Self::_try_at(bin_path.into())
258    }
259
260    fn _try_at(bin: PathBuf) -> Option<Self> {
261        let with_extension = match env::consts::EXE_EXTENSION {
262            "" => None,
263            it if bin.extension().is_none() => Some(bin.with_extension(it)),
264            _ => None,
265        };
266        iter::once(bin)
267            .chain(with_extension)
268            .find(|it| it.is_file())
269            .map(Self::new)
270    }
271
272    /// Returns a command builder for the given `bin_name` only if this
273    /// `bin_name` is accessible trough `PATH` env variable, otherwise returns `None`
274    pub fn lookup_in_path(bin_name: &str) -> Option<Self> {
275        let paths = env::var_os("PATH").unwrap_or_default();
276        env::split_paths(&paths)
277            .map(|path| path.join(bin_name))
278            .find_map(Self::try_at)
279    }
280
281    fn as_mut(&mut self) -> &mut CmdShared {
282        // Clone-on-write is so easy to do with `Arc` :D
283        Arc::make_mut(&mut self.0)
284    }
285
286    /// Set binary path, overwrites the path that was set before.
287    pub fn bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
288        self.as_mut().bin = bin.into();
289        self
290    }
291
292    /// Retuns the currently configured binary path.
293    pub fn get_bin(&self) -> &Path {
294        &self.0.bin
295    }
296
297    /// Set the current directory for the child process.
298    ///
299    /// Inherits this process current dir by default.
300    pub fn current_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
301        self.as_mut().current_dir = Some(dir.into());
302        self
303    }
304
305    /// Returns the currently configured current process directory path
306    pub fn get_current_dir(&self) -> Option<&Path> {
307        self.0.current_dir.as_deref()
308    }
309
310    /// When set to some [`log::Level`] the command with its arguments and output
311    /// will be logged via [`log`] crate.
312    ///
313    /// Note that this method is independent from [`Cmd::log_err()`].
314    ///
315    /// Default: `Some(`[`log::Level::Debug`]`)`
316    pub fn log_cmd(&mut self, level: impl Into<Option<log::Level>>) -> &mut Self {
317        self.as_mut().log_cmd = level.into();
318        self
319    }
320
321    /// When set to some [`log::Level`] the invocation error will be logged.
322    /// Set it to [`None`] or [`log::Level::Trace`] if non-zero exit code is
323    /// an expected/recoverable error which doesn't need to be logged.
324    ///
325    /// Note that this method is independent from [`Cmd::log_cmd()`].
326    ///
327    /// Default: `Some(`[`log::Level::Error`]`)`
328    pub fn log_err(&mut self, level: impl Into<Option<log::Level>>) -> &mut Self {
329        self.as_mut().log_err = level.into();
330        self
331    }
332
333    /// Sets the string input passed to child process's `stdin`.
334    /// This overwrites the previous value.
335    ///
336    /// Use [`Cmd::stdin_bytes()`] if you need to pass non-utf8 byte sequences.
337    ///
338    /// Nothing is written to `stdin` by default.
339    pub fn stdin(&mut self, stdin: impl Into<String>) -> &mut Self {
340        self.as_mut().stdin = Some(BinOrUtf8::Utf8(stdin.into()));
341        self
342    }
343
344    /// Sets the bytes input passed to child process's `stdin`.
345    /// This overwrites the previous value.
346    ///
347    /// Nothing is written to `stdin` by default.
348    pub fn stdin_bytes(&mut self, stdin: Vec<u8>) -> &mut Self {
349        self.as_mut().stdin = Some(BinOrUtf8::Bin(stdin));
350        self
351    }
352
353    /// Same as `cmd.arg(arg1).arg(arg2)`. This is just a convenient shortcut
354    /// mostly used to lexically group related arguments (for example named arguments).
355    pub fn arg2(&mut self, arg1: impl Into<OsString>, arg2: impl Into<OsString>) -> &mut Self {
356        self.arg(arg1).arg(arg2)
357    }
358
359    /// Appends a single argument to the list of arguments passed to the child process.
360    pub fn arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
361        self.as_mut().args.push(arg.into());
362        self
363    }
364
365    /// Replaces the argument at the given index with a new value.
366    ///
367    /// # Panics
368    /// Panics if the given index is out of range of the arguments already set
369    /// on this command builder.
370    pub fn replace_arg(&mut self, idx: usize, arg: impl Into<OsString>) -> &mut Self {
371        self.as_mut().args[idx] = arg.into();
372        self
373    }
374
375    /// Extends the array of arguments passed to the child process with `args`.
376    pub fn args<I>(&mut self, args: I) -> &mut Self
377    where
378        I: IntoIterator,
379        I::Item: Into<OsString>,
380    {
381        self.as_mut().args.extend(args.into_iter().map(Into::into));
382        self
383    }
384
385    /// Returns the currently configured list of command line arguments
386    pub fn get_args(&self) -> &[OsString] {
387        &self.0.args
388    }
389
390    /// Inserts or updates an environment variable mapping.
391    ///
392    /// Note that environment variable names are case-insensitive (but case-preserving) on Windows,
393    /// and case-sensitive on all other platforms.
394    pub fn env(&mut self, key: impl Into<OsString>, val: impl Into<OsString>) -> &mut Self {
395        self.as_mut().env.insert(key.into(), val.into());
396        self
397    }
398
399    /// Same as `cmd.spawn()?.wait()`
400    /// See [`Child::wait()`] for details.
401    pub fn run(&self) -> Result<()> {
402        self.spawn()?.wait()?;
403        Ok(())
404    }
405
406    /// Same as `cmd.spawn_piped()?.read()`
407    /// See [`Child::read()`] for details.
408    pub fn read(&self) -> Result<String> {
409        self.spawn_piped()?.read()
410    }
411
412    /// Same as `cmd.spawn_piped()?.read_bytes()`
413    /// See [`Child::read_bytes()`] for details.
414    pub fn read_bytes(&self) -> Result<Vec<u8>> {
415        self.spawn_piped()?.read_bytes()
416    }
417
418    /// Spawns a child process returning a handle to it.
419    /// The child inherits both `stdout` and `stderr`.
420    /// See the docs for [`Child`] for more details.
421    /// Note that reading the child process output streams will panic!
422    /// If you want to read the output, see [`Cmd::spawn_piped()`]
423    pub fn spawn(&self) -> Result<Child> {
424        self.spawn_with(Stdio::inherit(), Stdio::inherit())
425    }
426
427    /// Spawns a child process returning a handle to it.
428    /// Child's `stdout` will be piped for further reading from it, but
429    /// `stderr` will be inherited.
430    /// See the docs for [`Child`] for more details.
431    pub fn spawn_piped(&self) -> Result<Child> {
432        self.spawn_with(Stdio::piped(), Stdio::inherit())
433    }
434
435    /// More flexible version of `spawn` methods that allows you to specify
436    /// any combination of `stdout` and `stderr` conifigurations.
437    pub fn spawn_with(&self, stdout: Stdio, stderr: Stdio) -> Result<Child> {
438        let mut cmd = std::process::Command::new(&self.0.bin);
439        cmd.args(&self.0.args)
440            .envs(&self.0.env)
441            .stderr(stderr)
442            .stdout(stdout);
443
444        if let Some(dir) = &self.0.current_dir {
445            cmd.current_dir(dir);
446        }
447
448        let child = match &self.0.stdin {
449            None => cmd.stdin(Stdio::null()).spawn().cmd_context(self)?,
450            Some(_) => {
451                cmd.stdin(Stdio::piped());
452                cmd.spawn().cmd_context(self)?
453            }
454        };
455
456        let mut child = Child {
457            cmd: Cmd(Arc::clone(&self.0)),
458            child,
459        };
460
461        if let Some(level) = self.0.log_cmd {
462            log::log!(level, "{}", child);
463        }
464
465        if let Some(stdin) = &self.0.stdin {
466            child
467                .child
468                .stdin
469                .take()
470                .unwrap()
471                .write_all(stdin.as_ref())
472                .cmd_context(self)?;
473        }
474        Ok(child)
475    }
476
477    fn bin_name(&self) -> Cow<'_, str> {
478        self.0
479            .bin
480            .components()
481            .last()
482            .expect("Binary name must not be empty")
483            .as_os_str()
484            .to_string_lossy()
485    }
486}
487
488/// Wraps [`std::process::Child`], kills and waits for the process on [`Drop`].
489/// It will log the fact that [`std::process::Child::kill()`] was called in [`Drop`].
490/// You should use [`Child::wait()`] for the process to finish with any of the available
491/// methods if you want to handle the error, otherwise it will be ignored.
492///
493/// Beware that [`Child`] holds an invariant that is not propagated to the
494/// type system. The invariant is that if [`Child`] was not spawned via
495/// [`Cmd::spawn_piped()`], then any methods that read the child's `stdout` will panic.
496pub struct Child {
497    cmd: Cmd,
498    child: std::process::Child,
499}
500
501impl Drop for Child {
502    fn drop(&mut self) {
503        match self.child.try_wait() {
504            Ok(None) => {
505                log::debug!("[KILL {}] {}", self.child.id(), self.cmd.bin_name());
506                let _ = self.child.kill();
507                self.child.wait().unwrap_or_else(|err| {
508                    panic!("Failed to wait for process: {}\nProcess: {}", err, self);
509                });
510            }
511            // Already exited, no need for murder
512            Ok(Some(_status)) => {}
513            Err(err) => panic!("Failed to collect process exit status: {}", err),
514        }
515    }
516}
517
518impl fmt::Display for Child {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        let id = self.child.id();
521        write!(f, "[PID {}] {}", id, self.cmd)
522    }
523}
524
525impl Child {
526    /// Returns the [`Cmd`] that was originally used to create this [`Child`]
527    pub fn cmd(&self) -> Cmd {
528        self.cmd.clone()
529    }
530
531    /// Waits for the process to finish. Returns an error if the process has
532    /// finished with non-zero exit code.
533    ///
534    /// You should use this method for processes spawned via [`Cmd::spawn()`]
535    /// since the output of the command won't be read and returned,
536    /// but just written to this process's `stdout` (as `stdout` is inherited
537    /// with [`Cmd::spawn()`])
538    pub fn wait(&mut self) -> Result<()> {
539        let exit_status = self.child.wait().proc_context(self)?;
540
541        if !exit_status.success() {
542            return Err(Error::proc(
543                &self,
544                &format_args!("Non-zero exit code: {}", exit_status),
545            ));
546        }
547        Ok(())
548    }
549
550    /// Same as [`Child::read()`] but reads any bytes sequence from the
551    /// child process `stdout`.
552    ///
553    /// # Panics
554    /// Same as for [`Child::read()`].
555    pub fn read_bytes(mut self) -> Result<Vec<u8>> {
556        let output = self.read_bytes_no_wait(Ostream::StdOut)?;
557        self.wait()?;
558        Ok(output)
559    }
560
561    /// Waits for the process to finish and returns all that it has written
562    /// to `stdout`. Returns an error if the process has finished with
563    /// non-zero exit code. Expects a valid utf8 bytes sequence (since it returns
564    /// a Rust [`String`]), if the process is not guaranteed to output valid utf8
565    /// you might want to use [`Child::read_bytes()`] instead.
566    ///
567    /// If [`Cmd::log_cmd()`] has been set to some `log::Level` then prints captured
568    /// output via [`log`] crate.
569    ///
570    /// # Panics
571    /// Panics if the process was spawned with non-piped `stdout`.
572    /// This method is expected to be used only for processes spawned via
573    /// [`Cmd::spawn_piped()`].
574    pub fn read(mut self) -> Result<String> {
575        let output = self.read_no_wait(Ostream::StdOut)?;
576        self.wait()?;
577        Ok(output)
578    }
579
580    fn expect_ostream(&mut self, ostream: Ostream) -> &mut dyn io::Read {
581        match ostream {
582            Ostream::StdOut => {
583                if let Some(ref mut it) = self.child.stdout {
584                    return it;
585                }
586            }
587            Ostream::StdErr => {
588                if let Some(ref mut it) = self.child.stderr {
589                    return it;
590                }
591            }
592        };
593        panic!("{} wasn't piped for {}", ostream, self);
594    }
595
596    /// Same as [`Child::read()`], but doesn't wait for the process to finish
597    /// and doesn't take the exit status of the process into account.
598    pub fn read_no_wait(&mut self, ostream: Ostream) -> Result<String> {
599        let mut output = String::new();
600        self.expect_ostream(ostream)
601            .read_to_string(&mut output)
602            .map_err(|err| self.io_read_err(err, ostream, "utf8"))?;
603
604        self.log_output(ostream, &format_args!("[utf8]:\n{}", output));
605
606        Ok(output)
607    }
608
609    /// Same as [`Child::read_no_wait()`], but reads raw bytes.
610    pub fn read_bytes_no_wait(&mut self, ostream: Ostream) -> Result<Vec<u8>> {
611        let mut output = Vec::new();
612        self.expect_ostream(ostream)
613            .read_to_end(&mut output)
614            .map_err(|err| self.io_read_err(err, ostream, "bytes"))?;
615
616        self.log_output(ostream, &format_args!("[bytes]:\n{:?}", output));
617
618        Ok(output)
619    }
620
621    fn io_read_err(&self, err: io::Error, ostream: Ostream, data_kind: &str) -> Error {
622        Error::proc(
623            self,
624            &format_args!("Failed to read {} from `{}`: {}", data_kind, ostream, err),
625        )
626    }
627
628    fn log_output(&self, ostream: Ostream, output: &dyn fmt::Display) {
629        if let Some(level) = self.cmd.0.log_cmd {
630            let pid = self.child.id();
631            let bin_name = self.cmd.bin_name();
632            log::log!(level, "[{} {} {}] {}", ostream, pid, bin_name, output,);
633        }
634    }
635
636    /// Returns an iterator over the lines of data output to `stdout` by the child process.
637    /// Beware that the iterator buffers the output, thus when the it is
638    /// dropped the buffered data will be discarded and following reads
639    /// won't restore it.
640    /// The returned line of output is logged via [`log`] crate according to
641    /// [`Cmd::log_cmd()`] configuration.
642    ///
643    /// # Panics
644    /// Panics if some [`std::io::Error`] happens during the reading.
645    /// All invariants from [`Child::read_bytes()`] apply here too.
646    pub fn stdout_lines(&mut self) -> impl Iterator<Item = String> + '_ {
647        let log_cmd = self.cmd.0.log_cmd;
648        let id = self.child.id();
649        let bin_name = self.cmd.bin_name();
650        let stdout = io::BufReader::new(self.child.stdout.as_mut().unwrap());
651        stdout
652            .lines()
653            .map(|line| line.expect("Unexpected io error"))
654            .inspect(move |line| {
655                if let Some(level) = log_cmd {
656                    log::log!(level, "[{} {}] {}", id, bin_name, line);
657                }
658            })
659    }
660}
661
662/// Defines the kind of standard process output stream.
663#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
664pub enum Ostream {
665    /// `stdout` is the default output where process outputs its payload
666    StdOut,
667    /// `stderr` is the stream for human-readable messages not inteded for automation
668    StdErr,
669}
670
671impl fmt::Display for Ostream {
672    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
673        f.write_str(match self {
674            Ostream::StdOut => "stdout",
675            Ostream::StdErr => "stderr",
676        })
677    }
678}