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}