mirror of
https://github.com/uutils/coreutils.git
synced 2025-12-23 08:47:37 +00:00
seq: add SIGPIPE handling for GNU compatibility
This commit is contained in:
parent
c085cd1c21
commit
832ee2fd4c
5 changed files with 154 additions and 85 deletions
|
|
@ -131,6 +131,7 @@ setfacl
|
|||
setfattr
|
||||
shortcode
|
||||
shortcodes
|
||||
sigaction
|
||||
siginfo
|
||||
sigusr
|
||||
strcasecmp
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ uucore = { workspace = true, features = [
|
|||
"format",
|
||||
"parser",
|
||||
"quoting-style",
|
||||
"signals",
|
||||
] }
|
||||
fluent = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue