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
2 changes: 1 addition & 1 deletion src/uu/stty/src/stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/uu/yes/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/uu/yes/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions src/uu/yes/src/yes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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::<OsString>("STRING")).unwrap();
prepare_buffer(&mut buffer);
Expand Down
2 changes: 1 addition & 1 deletion tests/by-util/test_ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
59 changes: 54 additions & 5 deletions tests/by-util/test_yes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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}"
);
}
Loading