seq: add SIGPIPE handling for GNU compatibility

This commit is contained in:
Christopher Dryden 2025-12-14 21:08:10 +00:00
parent c085cd1c21
commit 832ee2fd4c
5 changed files with 154 additions and 85 deletions

View file

@ -131,6 +131,7 @@ setfacl
setfattr
shortcode
shortcodes
sigaction
siginfo
sigusr
strcasecmp

View file

@ -30,6 +30,7 @@ uucore = { workspace = true, features = [
"format",
"parser",
"quoting-style",
"signals",
] }
fluent = { workspace = true }

View file

@ -28,6 +28,8 @@ mod numberparse;
use crate::error::SeqError;
use crate::number::PreciseNumber;
#[cfg(unix)]
use uucore::signals;
use uucore::translate;
const OPT_SEPARATOR: &str = "separator";
@ -90,8 +92,20 @@ fn select_precision(
}
}
// Initialize SIGPIPE state capture at process startup (Unix only)
#[cfg(unix)]
uucore::init_sigpipe_capture!();
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// Restore SIGPIPE to default if it wasn't explicitly ignored by parent.
// The Rust runtime ignores SIGPIPE, but we need to respect the parent's
// signal disposition for proper pipeline behavior (GNU compatibility).
#[cfg(unix)]
if !signals::sigpipe_was_ignored() {
let _ = signals::enable_pipe_errors();
}
let matches =
uucore::clap_localization::handle_clap_result(uu_app(), split_short_args_with_value(args))?;
@ -209,16 +223,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
padding,
);
match result {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => {
if let Err(err) = result {
if err.kind() == std::io::ErrorKind::BrokenPipe {
// GNU seq prints the Broken pipe message but still exits with status 0
// unless SIGPIPE was explicitly ignored, in which case it should fail.
let err = err.map_err_context(|| "write error".into());
uucore::show_error!("{err}");
Ok(())
#[cfg(unix)]
if signals::sigpipe_was_ignored() {
uucore::error::set_exit_code(1);
}
return Ok(());
}
Err(err) => Err(err.map_err_context(|| "write error".into())),
return Err(err.map_err_context(|| "write error".into()));
}
Ok(())
}
pub fn uu_app() -> Command {

View file

@ -426,6 +426,68 @@ pub fn ignore_interrupts() -> Result<(), Errno> {
unsafe { signal(SIGINT, SigIgn) }.map(|_| ())
}
// SIGPIPE state capture - captures whether SIGPIPE was ignored at process startup
#[cfg(unix)]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(unix)]
static SIGPIPE_WAS_IGNORED: AtomicBool = AtomicBool::new(false);
/// Captures SIGPIPE state at process initialization, before main() runs.
///
/// # Safety
/// Called from `.init_array` before main(). Only reads current SIGPIPE handler state.
#[cfg(unix)]
pub unsafe extern "C" fn capture_sigpipe_state() {
use nix::libc;
use std::mem::MaybeUninit;
use std::ptr;
let mut current = MaybeUninit::<libc::sigaction>::uninit();
// SAFETY: sigaction with null new-action just queries current state
if unsafe { libc::sigaction(libc::SIGPIPE, ptr::null(), current.as_mut_ptr()) } == 0 {
// SAFETY: sigaction succeeded, so current is initialized
let ignored = unsafe { current.assume_init() }.sa_sigaction == libc::SIG_IGN;
SIGPIPE_WAS_IGNORED.store(ignored, Ordering::Relaxed);
}
}
/// Initializes SIGPIPE state capture. Call once at crate root level.
#[macro_export]
#[cfg(unix)]
macro_rules! init_sigpipe_capture {
() => {
#[cfg(all(unix, not(target_os = "macos")))]
#[used]
#[unsafe(link_section = ".init_array")]
static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() =
$crate::signals::capture_sigpipe_state;
#[cfg(all(unix, target_os = "macos"))]
#[used]
#[unsafe(link_section = "__DATA,__mod_init_func")]
static CAPTURE_SIGPIPE_STATE: unsafe extern "C" fn() =
$crate::signals::capture_sigpipe_state;
};
}
#[macro_export]
#[cfg(not(unix))]
macro_rules! init_sigpipe_capture {
() => {};
}
/// Returns whether SIGPIPE was ignored at process startup.
#[cfg(unix)]
pub fn sigpipe_was_ignored() -> bool {
SIGPIPE_WAS_IGNORED.load(Ordering::Relaxed)
}
#[cfg(not(unix))]
pub const fn sigpipe_was_ignored() -> bool {
false
}
#[test]
fn signal_by_value() {
assert_eq!(signal_by_name_or_value("0"), Some(0));

View file

@ -3,34 +3,18 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore lmnop xlmnop
use rstest::rstest;
use uutests::new_ucmd;
#[cfg(unix)]
use uutests::util::TestScenario;
#[cfg(unix)]
use uutests::util_name;
#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
}
#[test]
#[cfg(unix)]
fn test_broken_pipe_still_exits_success() {
use std::process::Stdio;
let mut child = new_ucmd!()
// Use an infinite sequence so a burst of output happens immediately after spawn.
// With small output the process can finish before stdout is closed and the Broken pipe never occurs.
.args(&["inf"])
.set_stdout(Stdio::piped())
.run_no_wait();
// Trigger a Broken pipe by writing to a pipe whose reader closed first.
child.close_stdout();
let result = child.wait().unwrap();
result
.code_is(0)
.stderr_contains("write error: Broken pipe");
}
#[test]
fn test_no_args() {
new_ucmd!()
@ -203,6 +187,24 @@ fn test_width_invalid_float() {
.usage_error("invalid floating point argument: '1e2.3'");
}
#[test]
#[cfg(unix)]
fn test_sigpipe_ignored_reports_write_error() {
let scene = TestScenario::new(util_name!());
let seq_bin = scene.bin_path.clone().into_os_string();
let script = "trap '' PIPE; { \"$SEQ_BIN\" seq inf 2>err; echo $? >code; } | head -n1";
let result = scene.cmd_shell(script).env("SEQ_BIN", &seq_bin).succeeds();
assert_eq!(result.stdout_str(), "1\n");
let err_contents = scene.fixtures.read("err");
assert!(
err_contents.contains("seq: write error: Broken pipe"),
"stderr missing write error message: {err_contents:?}"
);
assert_eq!(scene.fixtures.read("code"), "1\n");
}
// ---- Tests for the big integer based path ----
#[test]
@ -648,52 +650,49 @@ fn test_width_floats() {
.stdout_only("09.0\n10.0\n");
}
#[test]
fn test_neg_inf() {
new_ucmd!()
.args(&["--", "-inf", "0"])
.run_stdout_starts_with(b"-inf\n-inf\n-inf\n")
.success();
}
#[test]
fn test_neg_infinity() {
new_ucmd!()
.args(&["--", "-infinity", "0"])
.run_stdout_starts_with(b"-inf\n-inf\n-inf\n")
.success();
}
#[test]
fn test_inf() {
new_ucmd!()
.args(&["inf"])
.run_stdout_starts_with(b"1\n2\n3\n")
.success();
}
#[test]
fn test_infinity() {
new_ucmd!()
.args(&["infinity"])
.run_stdout_starts_with(b"1\n2\n3\n")
.success();
}
#[test]
fn test_inf_width() {
new_ucmd!()
.args(&["-w", "1.000", "inf", "inf"])
.run_stdout_starts_with(b"1.000\n inf\n inf\n inf\n")
.success();
}
#[test]
fn test_neg_inf_width() {
new_ucmd!()
.args(&["-w", "1.000", "-inf", "-inf"])
.run_stdout_starts_with(b"1.000\n -inf\n -inf\n -inf\n")
.success();
/// Test infinite sequences - these produce endless output, so we check they start correctly
/// and terminate with SIGPIPE on Unix (or succeed on non-Unix where pipe behavior differs).
#[rstest]
#[case::neg_inf(
&["--", "-inf", "0"],
b"-inf\n-inf\n-inf\n"
)]
#[case::neg_infinity(
&["--", "-infinity", "0"],
b"-inf\n-inf\n-inf\n"
)]
#[case::inf(
&["inf"],
b"1\n2\n3\n"
)]
#[case::infinity(
&["infinity"],
b"1\n2\n3\n"
)]
#[case::inf_width(
&["-w", "1.000", "inf", "inf"],
b"1.000\n inf\n inf\n inf\n"
)]
#[case::neg_inf_width(
&["-w", "1.000", "-inf", "-inf"],
b"1.000\n -inf\n -inf\n -inf\n"
)]
#[case::precision_inf(
&["1", "1.2", "inf"],
b"1.0\n2.2\n3.4\n"
)]
#[case::equalize_width_inf(
&["-w", "1", "1.2", "inf"],
b"1.0\n2.2\n3.4\n"
)]
fn test_infinite_sequence(#[case] args: &[&str], #[case] expected_start: &[u8]) {
let result = new_ucmd!()
.args(args)
.run_stdout_starts_with(expected_start);
#[cfg(unix)]
result.signal_name_is("PIPE");
#[cfg(not(unix))]
result.success();
}
#[test]
@ -1073,12 +1072,6 @@ fn test_precision_corner_cases() {
.args(&["1", "1.20", "3.000000"])
.succeeds()
.stdout_is("1.00\n2.20\n");
// Infinity is ignored
new_ucmd!()
.args(&["1", "1.2", "inf"])
.run_stdout_starts_with(b"1.0\n2.2\n3.4\n")
.success();
}
// GNU `seq` manual only makes guarantees about `-w` working if the
@ -1135,11 +1128,4 @@ fn test_equalize_widths_corner_cases() {
.args(&["-w", "0x1.1", "1.00002", "3"])
.succeeds()
.stdout_is("1.0625\n2.06252\n");
// We can't really pad with infinite number of zeros, so `-w` is ignored.
// (there is another test with infinity as an increment above)
new_ucmd!()
.args(&["-w", "1", "1.2", "inf"])
.run_stdout_starts_with(b"1.0\n2.2\n3.4\n")
.success();
}