Skip to content
Closed
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
38 changes: 38 additions & 0 deletions 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 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