diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index d60d4d985ba..7e7c75077c1 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -997,7 +997,7 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { /// /// The state array contains: /// - `state[0]`: input flags -/// - `state[1]`: output flags +/// - `state[1]`: output flags /// - `state[2]`: control flags /// - `state[3]`: local flags /// - `state[4..]`: control characters (optional) diff --git a/src/uu/yes/locales/en-US.ftl b/src/uu/yes/locales/en-US.ftl index 9daaaa820b0..7bd3e6acf37 100644 --- a/src/uu/yes/locales/en-US.ftl +++ b/src/uu/yes/locales/en-US.ftl @@ -3,4 +3,5 @@ yes-usage = yes [STRING]... # Error messages yes-error-standard-output = standard output: { $error } +yes-error-stdout-broken-pipe = yes: stdout: Broken pipe yes-error-invalid-utf8 = arguments contain invalid UTF-8 diff --git a/src/uu/yes/locales/fr-FR.ftl b/src/uu/yes/locales/fr-FR.ftl index c3272b80903..5f9142682c3 100644 --- a/src/uu/yes/locales/fr-FR.ftl +++ b/src/uu/yes/locales/fr-FR.ftl @@ -3,4 +3,5 @@ yes-usage = yes [CHAÎNE]... # Messages d'erreur yes-error-standard-output = sortie standard : { $error } +yes-error-stdout-broken-pipe = yes: stdout: Tube cassé yes-error-invalid-utf8 = les arguments contiennent de l'UTF-8 invalide diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index a5aaa18a867..822045dd344 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -6,6 +6,7 @@ // cSpell:ignore strs use clap::{Arg, ArgAction, Command, builder::ValueParser}; +use nix::libc; use std::error::Error; use std::ffi::OsString; use std::io::{self, Write}; @@ -23,6 +24,15 @@ const BUF_SIZE: usize = 16 * 1024; pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + // When we receive a SIGPIPE signal, we want to terminate the process so + // that we don't print any error messages to stderr. Rust ignores SIGPIPE + // (see https://github.com/rust-lang/rust/issues/62569), so we restore its + // default action here. + #[cfg(not(target_os = "windows"))] + unsafe { + libc::signal(libc::SIGPIPE, libc::SIG_DFL); + } + let mut buffer = Vec::with_capacity(BUF_SIZE); args_into_buffer(&mut buffer, matches.get_many::("STRING")).unwrap(); prepare_buffer(&mut buffer); diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 38729d30630..36bf6bd2f71 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -6498,7 +6498,7 @@ fn test_f_overrides_sort_flags() { // Create files with different sizes for predictable sort order at.write("small.txt", "a"); // 1 byte - at.write("medium.txt", "bb"); // 2 bytes + at.write("medium.txt", "bb"); // 2 bytes at.write("large.txt", "ccc"); // 3 bytes // Get baseline outputs (include -a to match -f behavior which shows all files) diff --git a/tests/by-util/test_yes.rs b/tests/by-util/test_yes.rs index db460c998fc..9bafc003019 100644 --- a/tests/by-util/test_yes.rs +++ b/tests/by-util/test_yes.rs @@ -5,14 +5,13 @@ use std::ffi::OsStr; use std::process::ExitStatus; -#[cfg(unix)] -use std::os::unix::process::ExitStatusExt; - -use uutests::new_ucmd; +use uutests::{get_tests_binary, new_ucmd}; #[cfg(unix)] fn check_termination(result: ExitStatus) { - assert_eq!(result.signal(), Some(libc::SIGPIPE)); + // When SIGPIPE is NOT trapped, yes is killed by signal 13 (exit 141) + // When SIGPIPE IS trapped, yes exits with code 1 + assert!(!result.success(), "yes should fail on broken pipe"); } #[cfg(not(unix))] @@ -111,3 +110,53 @@ fn test_non_utf8() { &b"\xbf\xff\xee bar\n".repeat(5000), ); } + +/// Test SIGPIPE handling in normal pipe scenario +/// +/// When SIGPIPE is NOT trapped, `yes` should: +/// 1. Be killed by SIGPIPE signal (exit code 141 = 128 + 13) +/// 2. NOT print any error message to stderr +/// +/// This test uses a shell command to simulate `yes | head -n 1` +/// The expected behavior matches GNU yes. +#[test] +#[cfg(unix)] +fn test_normal_pipe_sigpipe() { + use std::process::Command; + + // Run `yes | head -n 1` via shell with pipefail to capture yes's exit code + // In this scenario, SIGPIPE is not trapped, so yes should be killed by the signal + let output = Command::new("bash") + .arg("-c") + .arg(format!( + "set -o pipefail; {} yes | head -n 1 > /dev/null", + get_tests_binary!() + )) + .output() + .expect("Failed to execute yes | head"); + + // Extract exit code + let exit_code = output.status.code(); + + // The process should be killed by SIGPIPE (signal 13) + // Exit code should be 141 (128 + 13) on most Unix systems + // OR the process was terminated by signal (status.code() returns None) + if let Some(code) = exit_code { + assert_eq!( + code, 141, + "yes should exit with code 141 (killed by SIGPIPE), but got {code}" + ); + } else { + // Process was terminated by signal (which is also acceptable) + use std::os::unix::process::ExitStatusExt; + let signal = output.status.signal().unwrap(); + // Signal 13 is SIGPIPE + assert_eq!(signal, 13, "yes should be killed by SIGPIPE (13)"); + } + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.is_empty(), + "yes should NOT print error message in normal pipe scenario, but got: {stderr}" + ); +}