Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

cat: Suppress Broken Pipe errors. #7921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
Loading
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
cat: Suppress Broken Pipe errors.
At present, the `cat` command unexpectedly prints an error message
when it receives a broken pipe error.

As an example, there are many workflows that make use of `cat` and
`head` together to process only part of the data.  The `head`
command will stop reading after a configured number of bytes or
lines, subsequently exposing `cat` to a broken pipe condition.

Said workflows may fail when they unexpectedly get error messages
in their output.

Suppress broken pipe errors.

Signed-off-by: Frode Nordahl <fnordahl@ubuntu.com>
  • Loading branch information
fnordahl committed May 13, 2025
commit f2508fdae4e7f377176746b213e9e16b348b1c20
38 changes: 38 additions & 0 deletions 38 src/uu/cat/src/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,44 @@ fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> {

for path in files {
if let Err(err) = cat_path(path, options, &mut state, out_info.as_ref()) {
// At this point we know that `cat_path` returned an error.
//
// Most errors should be logged, but for purpose of compatibility
// with GNU cat, we do not want to log errors that occur on the
// back of broken pipe.
//
// A common pattern is to use cat piped together with other tools.
// As an example `cat | head -1` could induce a broken pipe
// condition.
//
// Existing workflows choke on there unexpectedly being errors
// printed for this condition.
//
// Different types of errors are wrapped by the `CatError` type,
// and the broken pipe error either come from std::io::Error or
// nix::Error.
//
// Make use of pattern matching to know how to check inside the
// different types of errors.
//
// We need to explicitly borrow std::io::Error because it does not
// implement the Copy trait and consequently it would be partially
// moved, we are also not modifying it and as such would benefit
// from not having to do the copy.
if let CatError::Io(ref err_io) = err {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add comments to explain what you are doing

if err_io.kind() == io::ErrorKind::BrokenPipe {
continue;
}
}
// While nix::Error does implement the Copy trait, we explicitly
// borrow it to avoid the unnecessary copy.
#[cfg(any(target_os = "linux", target_os = "android"))]
if let CatError::Nix(ref err_nix) = err {
// spell-checker:disable-next-line
if *err_nix == nix::errno::Errno::EPIPE {
continue;
}
}
error_messages.push(format!("{}: {err}", path.maybe_quote()));
}
}
Expand Down
83 changes: 83 additions & 0 deletions 83 tests/by-util/test_cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,3 +716,86 @@ fn test_child_when_pipe_in() {

ts.ucmd().pipe_in("content").run().stdout_is("content");
}

#[cfg(target_os = "linux")]
mod linux_only {
use uutests::util::{CmdResult, TestScenario, UCommand};

use std::fmt::Write;
use std::fs::File;
use std::process::Stdio;
use uutests::new_ucmd;
use uutests::util_name;

fn make_broken_pipe() -> File {
use libc::c_int;
use std::os::unix::io::FromRawFd;

let mut fds: [c_int; 2] = [0, 0];
assert!(
(unsafe { libc::pipe(std::ptr::from_mut::<c_int>(&mut fds[0])) } == 0),
"Failed to create pipe"
);

// Drop the read end of the pipe
let _ = unsafe { File::from_raw_fd(fds[0]) };

// Make the write end of the pipe into a Rust File
unsafe { File::from_raw_fd(fds[1]) }
}
Comment on lines +720 to +745
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sylvestre I stole this from the tee test code:

#[cfg(target_os = "linux")]
mod linux_only {
use uutests::util::{AtPath, CmdResult, TestScenario, UCommand};
use std::fmt::Write;
use std::fs::File;
use std::process::Stdio;
use std::time::Duration;
use uutests::at_and_ucmd;
use uutests::new_ucmd;
use uutests::util_name;
fn make_broken_pipe() -> File {
use libc::c_int;
use std::os::unix::io::FromRawFd;
let mut fds: [c_int; 2] = [0, 0];
assert!(
(unsafe { libc::pipe(std::ptr::from_mut::<c_int>(&mut fds[0])) } == 0),
"Failed to create pipe"
);
// Drop the read end of the pipe
let _ = unsafe { File::from_raw_fd(fds[0]) };
// Make the write end of the pipe into a Rust File
unsafe { File::from_raw_fd(fds[1]) }
}
fn make_hanging_read() -> File {
use libc::c_int;
use std::os::unix::io::FromRawFd;
let mut fds: [c_int; 2] = [0, 0];
assert!(
(unsafe { libc::pipe(std::ptr::from_mut::<c_int>(&mut fds[0])) } == 0),
"Failed to create pipe"
);
// PURPOSELY leak the write end of the pipe, so the read end hangs.
// Return the read end of the pipe
unsafe { File::from_raw_fd(fds[0]) }
}
fn run_tee(proc: &mut UCommand) -> (String, CmdResult) {
let content = (1..=100_000).fold(String::new(), |mut output, x| {
let _ = writeln!(output, "{x}");
output
});
let result = proc
.ignore_stdin_write_error()
.set_stdin(Stdio::piped())
.run_no_wait()
.pipe_in_and_wait(content.as_bytes());
(content, result)
}
fn expect_success(result: &CmdResult) {
assert!(
result.succeeded(),
"Command was expected to succeed.\nstdout = {}\n stderr = {}",
std::str::from_utf8(result.stdout()).unwrap(),
std::str::from_utf8(result.stderr()).unwrap(),
);
assert!(
result.stderr_str().is_empty(),
"Unexpected data on stderr.\n stderr = {}",
std::str::from_utf8(result.stderr()).unwrap(),
);
}
fn expect_failure(result: &CmdResult, message: &str) {
assert!(
!result.succeeded(),
"Command was expected to fail.\nstdout = {}\n stderr = {}",
std::str::from_utf8(result.stdout()).unwrap(),
std::str::from_utf8(result.stderr()).unwrap(),
);
assert!(
result.stderr_str().contains(message),
"Expected to see error message fragment {message} in stderr, but did not.\n stderr = {}",
std::str::from_utf8(result.stderr()).unwrap(),
);
}
fn expect_silent_failure(result: &CmdResult) {
assert!(
!result.succeeded(),
"Command was expected to fail.\nstdout = {}\n stderr = {}",
std::str::from_utf8(result.stdout()).unwrap(),
std::str::from_utf8(result.stderr()).unwrap(),
);
assert!(
result.stderr_str().is_empty(),
"Unexpected data on stderr.\n stderr = {}",
std::str::from_utf8(result.stderr()).unwrap(),
);
}
fn expect_correct(name: &str, at: &AtPath, contents: &str) {
assert!(at.file_exists(name));
let compare = at.read(name);
assert_eq!(compare, contents);
}
fn expect_short(name: &str, at: &AtPath, contents: &str) {
assert!(at.file_exists(name));
let compare = at.read(name);
assert!(
compare.len() < contents.len(),
"Too many bytes ({}) written to {name} (should be a short count from {})",
compare.len(),
contents.len()
);
assert!(
contents.starts_with(&compare),
"Expected truncated output to be a prefix of the correct output, but it isn't.\n Correct: {contents}\n Compare: {compare}"
);
}
#[test]
fn test_pipe_error_default() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd.arg(file_out_a).set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_silent_failure(&output);
expect_short(file_out_a, &at, content.as_str());
}

Is that OK, or would you prefer we find some way of hosting these in a shareable location, if the latter, any advice on preferred approach would be appreciated.


fn run_cat(proc: &mut UCommand) -> (String, CmdResult) {
let content = (1..=100_000).fold(String::new(), |mut output, x| {
let _ = writeln!(output, "{x}");
output
});

let result = proc
.ignore_stdin_write_error()
.set_stdin(Stdio::piped())
.run_no_wait()
.pipe_in_and_wait(content.as_bytes());

(content, result)
}

fn expect_silent_success(result: &CmdResult) {
assert!(
result.succeeded(),
"Command was expected to succeed.\nstdout = {}\n stderr = {}",
std::str::from_utf8(result.stdout()).unwrap(),
std::str::from_utf8(result.stderr()).unwrap(),
);
assert!(
result.stderr_str().is_empty(),
"Unexpected data on stderr.\n stderr = {}",
std::str::from_utf8(result.stderr()).unwrap(),
);
}

fn expect_short(result: &CmdResult, contents: &str) {
let compare = result.stdout_str();
assert!(
compare.len() < contents.len(),
"Too many bytes ({}) written to stdout (should be a short count from {})",
compare.len(),
contents.len()
);
assert!(
contents.starts_with(compare),
"Expected truncated output to be a prefix of the correct output, but it isn't.\n Correct: {contents}\n Compare: {compare}"
);
}

#[test]
fn test_pipe_error_default() {
let mut ucmd = new_ucmd!();

let proc = ucmd.set_stdout(make_broken_pipe());

let (content, output) = run_cat(proc);

expect_silent_success(&output);
expect_short(&output, &content);
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.