From c3a4750b5b516672c8bed25f9247cefb32bf35b9 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Fri, 9 May 2025 15:41:56 +0000 Subject: [PATCH 1/2] coreutils: Print usage to stderr. Scripts exist that may call one of the multi-binary entry points with argument 0 set to some other value than the name of the entry point. One example is the Open vSwitch testsuite which makes use of /bin/true as an argument to the bash builtin `exec` to check whether it supports the '-a' argument [0]. In this situation coreutils will print usage on standard output, which makes unnecessary noise. Printing usage on standard error, which is customary for other tools, allows the script to succeed. 0: https://github.com/openvswitch/ovs/blob/28064e9fa50d/tests/ovs-macros.at#L199 Signed-off-by: Frode Nordahl --- src/bin/coreutils.rs | 14 +++++++------- tests/test_util_name.rs | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index b29e7ea2337..4a771a1738b 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -20,18 +20,18 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); fn usage(utils: &UtilityMap, name: &str) { - println!("{name} {VERSION} (multi-call binary)\n"); - println!("Usage: {name} [function [arguments...]]"); - println!(" {name} --list\n"); - println!("Options:"); - println!(" --list lists all defined functions, one per row\n"); - println!("Currently defined functions:\n"); + eprintln!("{name} {VERSION} (multi-call binary)\n"); + eprintln!("Usage: {name} [function [arguments...]]"); + eprintln!(" {name} --list\n"); + eprintln!("Options:"); + eprintln!(" --list lists all defined functions, one per row\n"); + eprintln!("Currently defined functions:\n"); #[allow(clippy::map_clone)] let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect(); utils.sort_unstable(); let display_list = utils.join(", "); let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions - println!( + eprintln!( "{}", textwrap::indent(&textwrap::fill(&display_list, width), " ") ); diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index 7a8a076e893..79d000443fc 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -120,8 +120,8 @@ fn util_invalid_name_help() { .unwrap(); let output = child.wait_with_output().unwrap(); assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stderr, b""); - let output_str = String::from_utf8(output.stdout).unwrap(); + assert_eq!(output.stdout, b""); + let output_str = String::from_utf8(output.stderr).unwrap(); assert!(output_str.contains("(multi-call binary)"), "{output_str:?}"); assert!( output_str.contains("Usage: invalid_name [function "), @@ -159,8 +159,8 @@ fn util_non_utf8_name_help() { .unwrap(); let output = child.wait_with_output().unwrap(); assert_eq!(output.status.code(), Some(0)); - assert_eq!(output.stderr, b""); - let output_str = String::from_utf8(output.stdout).unwrap(); + assert_eq!(output.stdout, b""); + let output_str = String::from_utf8(output.stderr).unwrap(); assert!(output_str.contains("(multi-call binary)"), "{output_str:?}"); assert!( output_str.contains("Usage: [function "), From a77381af0d162c9829afb3b773479b4b0cbbafbf Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Fri, 9 May 2025 22:31:50 +0000 Subject: [PATCH 2/2] 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 --- src/uu/cat/src/cat.rs | 12 ++++++ tests/by-util/test_cat.rs | 83 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index c0a41270f34..bbec5d42d16 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -450,6 +450,18 @@ 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()) { + if let CatError::Io(ref err_io) = err { + if err_io.kind() == io::ErrorKind::BrokenPipe { + continue; + } + } + #[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..750316adeab 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); + } +}