diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index c0a41270f34..76e4fbae7d9 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -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 { + 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())); } } diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 926befe72ff..afc1e17fb4d 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -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::(&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 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); + } +}