Writing to standard output and error streams is easy in Rust: use one of the
print!
, println!
(stdout), eprint!
, or eprintln!
(stderr) macros to
format a message and print it directly to the appropriate file descriptor.
But these macros return ()
, not an io::Result
, even though they perform I/O.
That’s because if the write fails, they panic. Normally, this is fine: the
standard streams should basically always be there.
Here’s an example where they aren’t:
prints_more_than_ten_lines | head
The Unix head
program reads ten lines (by default) from its standard input,
prints them to its standard output, and then exits. When it exits, it closes its
standard streams.
The stdout
stream of prints_more_than_ten_lines
is connected to a kernel
pipe, whose other end is the stdin
stream of head
. When head
exits, it
closes the read end of the pipe. When the kernel processes close()
calls for
the read ends of pipes, it sends SIGPIPE
to the process holding the write side
of the pipe (which the C runtime crt0
catches and terminates, but the Rust
runtime ignores), and then any future write()
calls to the pipe are
immediately returned with -EPIPE
.
Rust’s std::io::Write
function correctly translates this into an Err
, which
println!
unwraps, beginning a panic.
The calm I/O crate does not panic in the face of closed streams: it propagates the error, and allows the caller to gracefully unwind and exit.
This crate exposes four macros: stdout!
, stdoutln!
, stderr!
, and
stderrln!
. These behave exactly like the macros listed above, except that they
return the io::Result
from write!
and writeln!
rather than unwrapping it
and potentially panicking.
In addition, this crate exports a function attribute, pipefail
, which
suppresses only the BrokenPipe
error. It can be attached to any function which
returns io::Result
(but should only be attached to main
). Functions
decorated with #[pipefail]
have a match
shim wrapped around their body,
which replaces both Ok(_)
and Err(io::ErrorKind::BrokenPipe)
with Ok(())
,
and leaves all other errors as they were.
use calm_io::*;
#[pipefail]
fn main() -> std::io::Result<()> {
stdoutln!("Hello stdout from Rust")?;
stderrln!("Hello stderr from Rust")?;
}
As an example, consider this reimplementation of yes | head
:
// examples/yeah.rs
use calm_io::*;
#[pipefail]
fn main () -> std::io::Result<!> {
let text = std::env::args().nth(1).unwrap_or("y".to_string());
loop {
stdoutln!("{}", text)?;
}
}
Try running these commands in your shell!
$ cargo run --example good_yes | head > /dev/null
$ echo "${PIPESTATUS[@]}"
# The name is `PIPESTATUS` in bash, but `pipestatus` (lowercase!) in zsh
0 0
# yeah exits successfully, head exits successfully
$ yes | head > /dev/null
$ echo "${PIPESTATUS[@]}"
141 0
# yes crashes due to SIGPIPE, head exits successfully
$ cargo run --example bad_yes | head > /dev/null
thread 'main' panicked at 'failed printing to stdout: Broken pipe (os error 32)', src/libstd/io/stdio.rs:792:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
In the future, other suppression attributes may be added, or a general suppression attribute may be created that takes a list of errors to suppress.