Skip to content

Commit e2925fd

Browse files
committed
Fix clippy uninlined-format-args
1 parent 5f296d7 commit e2925fd

2 files changed

Lines changed: 73 additions & 21 deletions

File tree

src/uu/yes/src/yes.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
// cSpell:ignore strs
77

88
use clap::{Arg, ArgAction, Command, builder::ValueParser};
9+
use nix::libc;
910
use std::error::Error;
1011
use std::ffi::OsString;
1112
use std::io::{self, Write};
1213
use uucore::error::{UResult, USimpleError};
1314
use uucore::format_usage;
15+
#[cfg(unix)]
16+
use uucore::signals::enable_pipe_errors;
1417
use uucore::translate;
1518

1619
// it's possible that using a smaller or larger buffer might provide better performance on some
@@ -21,18 +24,22 @@ const BUF_SIZE: usize = 16 * 1024;
2124
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
2225
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
2326

27+
// When we receive a SIGPIPE signal, we want to terminate the process so
28+
// that we don't print any error messages to stderr. Rust ignores SIGPIPE
29+
// (see https://github.com/rust-lang/rust/issues/62569), so we restore its
30+
// default action here.
31+
#[cfg(not(target_os = "windows"))]
32+
unsafe {
33+
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
34+
}
35+
2436
let mut buffer = Vec::with_capacity(BUF_SIZE);
2537
args_into_buffer(&mut buffer, matches.get_many::<OsString>("STRING")).unwrap();
2638
prepare_buffer(&mut buffer);
2739

2840
match exec(&buffer) {
2941
Ok(()) => Ok(()),
30-
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => {
31-
// When SIGPIPE is trapped (SIG_IGN), write operations return EPIPE.
32-
// GNU coreutils prints an error message in this case.
33-
eprintln!("{}", translate!("yes-error-stdout-broken-pipe"));
34-
Ok(())
35-
}
42+
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()),
3643
Err(err) => Err(USimpleError::new(
3744
1,
3845
translate!("yes-error-standard-output", "error" => err),
@@ -116,16 +123,8 @@ fn prepare_buffer(buf: &mut Vec<u8>) {
116123
pub fn exec(bytes: &[u8]) -> io::Result<()> {
117124
let stdout = io::stdout();
118125
let mut stdout = stdout.lock();
119-
120-
// SIGPIPE handling:
121-
// Rust ignores SIGPIPE by default (rust-lang/rust#62569). When write() fails
122-
// because the pipe is closed, it returns EPIPE error instead of being killed by
123-
// the signal. We catch this error in uumain() and print a diagnostic message,
124-
// matching GNU coreutils behavior.
125-
//
126-
// Key point: We do NOT restore SIGPIPE to SIG_DFL. This preserves POSIX signal
127-
// inheritance semantics - if the parent set SIGPIPE to SIG_IGN (e.g., `trap '' PIPE`),
128-
// we respect that setting and rely on EPIPE error handling.
126+
#[cfg(unix)]
127+
enable_pipe_errors()?;
129128

130129
loop {
131130
stdout.write_all(bytes)?;

tests/by-util/test_yes.rs

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
use std::ffi::OsStr;
66
use std::process::ExitStatus;
77

8-
use uutests::new_ucmd;
8+
use uutests::{get_tests_binary, new_ucmd};
99

1010
#[cfg(unix)]
1111
fn check_termination(result: ExitStatus) {
12-
// yes should exit successfully (code 0) when the pipe breaks.
13-
// Rust ignores SIGPIPE by default, so write() returns EPIPE error,
14-
// which is caught and handled gracefully. This matches GNU coreutils behavior.
15-
assert!(result.success(), "yes should exit successfully");
12+
// When SIGPIPE is NOT trapped, yes is killed by signal 13 (exit 141)
13+
// When SIGPIPE IS trapped, yes exits with code 1
14+
assert!(!result.success(), "yes should fail on broken pipe");
1615
}
1716

1817
#[cfg(not(unix))]
@@ -111,3 +110,57 @@ fn test_non_utf8() {
111110
&b"\xbf\xff\xee bar\n".repeat(5000),
112111
);
113112
}
113+
114+
/// Test SIGPIPE handling in normal pipe scenario
115+
///
116+
/// When SIGPIPE is NOT trapped, `yes` should:
117+
/// 1. Be killed by SIGPIPE signal (exit code 141 = 128 + 13)
118+
/// 2. NOT print any error message to stderr
119+
///
120+
/// This test uses a shell command to simulate `yes | head -n 1`
121+
/// The expected behavior matches GNU yes.
122+
#[test]
123+
#[cfg(unix)]
124+
fn test_normal_pipe_sigpipe() {
125+
use std::process::Command;
126+
127+
// Run `yes | head -n 1` via shell with pipefail to capture yes's exit code
128+
// In this scenario, SIGPIPE is not trapped, so yes should be killed by the signal
129+
let output = Command::new("sh")
130+
.arg("-c")
131+
.arg(format!(
132+
"set -o pipefail; {} yes | head -n 1 > /dev/null",
133+
get_tests_binary!()
134+
))
135+
.output()
136+
.expect("Failed to execute yes | head");
137+
138+
// Extract exit code
139+
let exit_code = output.status.code();
140+
141+
// The process should be killed by SIGPIPE (signal 13)
142+
// Exit code should be 141 (128 + 13) on most Unix systems
143+
// OR the process was terminated by signal (status.code() returns None)
144+
if let Some(code) = exit_code {
145+
println!("Exit code: {code}");
146+
println!("Stderr: {}", String::from_utf8_lossy(&output.stderr));
147+
148+
assert_eq!(
149+
code, 141,
150+
"yes should exit with code 141 (killed by SIGPIPE), but got {code}"
151+
);
152+
} else {
153+
// Process was terminated by signal (which is also acceptable)
154+
use std::os::unix::process::ExitStatusExt;
155+
let signal = output.status.signal().unwrap();
156+
println!("Terminated by signal: {signal}");
157+
// Signal 13 is SIGPIPE
158+
assert_eq!(signal, 13, "yes should be killed by SIGPIPE (13)");
159+
}
160+
161+
let stderr = String::from_utf8_lossy(&output.stderr);
162+
assert!(
163+
stderr.is_empty(),
164+
"yes should NOT print error message in normal pipe scenario, but got: {stderr}"
165+
);
166+
}

0 commit comments

Comments
 (0)