-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
I noticed that when you pipe output and close it early (like seq inf | head -n 1
), many utilities panic with error messages instead of exiting silently like GNU coreutils does.
Looks like we've known about this since 2014 (#374) but only 7 out of 102 utilities handle it properly.
Why it matters
POSIX 1003.1-2001 says:
SIGPIPE shall be sent to a process that attempts to write to a pipe when there are no readers. Default action: Terminate the process.
GNU Coreutils does this:
'PIPE': Write on a pipe with no one to read it.
They both say utilities should just quit silently. We should probably do the same.
Why this hasn't been fixed yet
I did some digging. Here's what I think happened:
- Rust ignores SIGPIPE by default (rust-lang/rust#62569)
- The bug only shows up in specific cases, so not many people report it
- Since it affects all utilities, nobody took ownership
- Different people tried different fixes over the years
- There was no documentation on how to handle it (until recently)
Good news: cat
(4406b403), tail
(f04ed45a), and tr
(c1eeed61) were fixed in 2025, so there's momentum now.
What's working and what's not
Already fixed: cat, env, tail, tee, timeout, tr, yes (7 utilities)
Still broken: seq, head, echo, ls, wc, cut, sort, uniq, nl, and more (>17+ utilities)
Suggested fix
Looking at how other utilities handle this, most use the signal handler approach from uucore::signals
:
Examples from the codebase:
tee
(src/uu/tee/src/tee.rs:166)tail
(src/uu/tail/src/tail.rs:49)tr
(src/uu/tr/src/tr.rs:42)
They all do this:
use uucore::signals::enable_pipe_errors;
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
#[cfg(unix)]
enable_pipe_errors()?;
// rest of the code
}
note: cat uses a different approach with direct libc calls 4406b40, but the enable_pipe_errors()
method seems to be the pattern most utilities follow.
For testing, cat has a good example: (tests/by-util/test_cat.rs:122-135)
#[test]
fn test_broken_pipe() {
let mut child = new_ucmd!()
.args(&["alpha.txt"])
.set_stdout(Stdio::piped())
.run_no_wait();
child.close_stdout();
child.wait().unwrap().fails_silently();
}