From d6fdcd6e55acecc759eb3d3c981365e74ff68d34 Mon Sep 17 00:00:00 2001 From: Zeng Fanfan Date: Tue, 20 Oct 2026 23:27:49 +0800 Subject: [PATCH 1/2] grep: line-buffer terminal output Refs #145 --- src/lib.rs | 3 +- src/output.rs | 2 +- tests/buffering/mod.rs | 105 +++++++++++++++++++++++++++++++++++++++++ tests/test_grep.rs | 2 + 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 tests/buffering/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 4a8c4b6..f247a92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,7 +167,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let max_count = matches.get_one::("max_count").copied(); let byte_offset = matches.get_flag("byte_offset"); let line_number = matches.get_flag("line_number"); - let line_buffered = matches.get_flag("line_buffered"); + // Interactive output must be visible while grep waits for more input. + let line_buffered = matches.get_flag("line_buffered") || std::io::stdout().is_terminal(); let with_filename = matches.get_flag("with_filename"); let no_filename = matches.get_flag("no_filename"); let label = matches diff --git a/src/output.rs b/src/output.rs index ba1f06a..dc90127 100644 --- a/src/output.rs +++ b/src/output.rs @@ -231,7 +231,7 @@ impl<'a> OutputWriter<'a> { } } - /// Flush in `--line-buffered` mode. + /// Flush for terminal output or explicit `--line-buffered` mode. fn maybe_flush(&mut self) -> io::Result<()> { if self.config.line_buffered { self.out.flush() diff --git a/tests/buffering/mod.rs b/tests/buffering/mod.rs new file mode 100644 index 0000000..32d568e --- /dev/null +++ b/tests/buffering/mod.rs @@ -0,0 +1,105 @@ +use std::process::Stdio; +use std::time::{Duration, Instant}; +use uutests::util::UChild; + +use super::ucmd; + +// Captured output is read by uutests in the background. Poll with a deadline +// while keeping stdin open, so EOF cannot accidentally satisfy the test. +fn wait_for_output(child: &mut UChild, stderr: bool) -> Vec { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + let output = if stderr { + child.stderr_bytes() + } else { + child.stdout_bytes() + }; + if !output.is_empty() || Instant::now() >= deadline { + return output; + } + std::thread::sleep(Duration::from_millis(10)); + } +} + +#[cfg(unix)] +#[test] +fn terminal_flushes_matching_lines_before_eof() { + use std::io::Read; + use std::sync::mpsc; + use uutests::util::pty_path; + + for (options, expected) in [ + (vec![], "x\n"), + (vec!["--color=never"], "x\n"), + (vec!["--color=always"], "\x1b[01;31m\x1b[Kx\x1b[m\x1b[K\n"), + (vec!["--line-buffered", "--color=never"], "x\n"), + ] { + let (_scene, mut command) = ucmd(); + let (_, mut controller, replica) = pty_path(); + // Read the PTY directly: TerminalSimulation buffers its captured output. + let expected = expected.replace('\n', "\r\n").into_bytes(); + let length = expected.len(); + let (sender, receiver) = mpsc::channel(); + let reader = std::thread::spawn(move || { + let mut bytes = vec![0; length]; + let result = controller.read_exact(&mut bytes).map(|()| bytes); + let _ = sender.send(result); + }); + command + .set_stdin(Stdio::piped()) + .set_stdout(replica) + .args(&options) + .arg("x") + .env("GREP_COLORS", "") + .env("GREP_COLOR", "") + .timeout(Duration::from_secs(5)); + let mut child = command.run_no_wait(); + drop(command); + let written = child.try_write_in("x\n"); + let early = receiver.recv_timeout(Duration::from_secs(5)); + // Kill on missing/short output so the PTY reader also reaches EOF. + if !matches!(&early, Ok(Ok(_))) { + child.kill(); + } + // Reap and join before asserting, including the regression case. + let result = child.wait().unwrap(); + reader.join().unwrap(); + written.unwrap(); + assert_eq!(early.unwrap().unwrap(), expected, "options: {options:?}"); + result.success().no_stderr(); + } +} + +#[test] +fn explicit_line_buffering_flushes_pipe_before_eof() { + let (_scene, mut command) = ucmd(); + let mut child = command + .set_stdin(Stdio::piped()) + .args(&["--line-buffered", "--color=never", "x"]) + .timeout(Duration::from_secs(5)) + .run_no_wait(); + child.write_in("x\n"); + let early = wait_for_output(&mut child, false); + child.wait().unwrap().success().no_stderr(); + assert_eq!(early, b"x\n"); +} + +#[test] +fn redirected_output_remains_buffered_by_default() { + let (scene, mut command) = ucmd(); + scene.fixtures.write("first", "x\n"); + let mut child = command + .set_stdin(Stdio::piped()) + .args(&["--color=never", "-h", "x", "first", "missing", "-"]) + .timeout(Duration::from_secs(5)) + .run_no_wait(); + // The missing-file diagnostic follows the first file's match. It proves + // grep has processed that match before we check that stdout is buffered. + let ready = wait_for_output(&mut child, true); + std::thread::sleep(Duration::from_millis(200)); + let early = child.stdout_bytes(); + let result = child.wait().unwrap(); + result.code_is(2).stdout_is("x\n"); + assert!(!ready.is_empty(), "missing-file readiness diagnostic"); + assert!(early.is_empty(), "redirected output flushed before EOF"); +} diff --git a/tests/test_grep.rs b/tests/test_grep.rs index fc6783b..9aaa188 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -3,6 +3,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use uutests::util::TerminalSimulation; use uutests::util::{TestScenario, UCommand}; +mod buffering; + static UCMD_INIT: AtomicBool = AtomicBool::new(false); fn ucmd() -> (TestScenario, UCommand) { From af1697faf6703a745b1d9f12f50a5c9ce5290469 Mon Sep 17 00:00:00 2001 From: Zeng Fanfan Date: Wed, 21 Oct 2026 12:14:21 +0800 Subject: [PATCH 2/2] grep: simplify terminal buffering change Refs #145 --- src/lib.rs | 6 +-- tests/buffering/mod.rs | 105 ----------------------------------------- tests/test_grep.rs | 2 - 3 files changed, 3 insertions(+), 110 deletions(-) delete mode 100644 tests/buffering/mod.rs diff --git a/src/lib.rs b/src/lib.rs index f247a92..60b813d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,8 +167,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let max_count = matches.get_one::("max_count").copied(); let byte_offset = matches.get_flag("byte_offset"); let line_number = matches.get_flag("line_number"); - // Interactive output must be visible while grep waits for more input. - let line_buffered = matches.get_flag("line_buffered") || std::io::stdout().is_terminal(); + let stdout_is_terminal = std::io::stdout().is_terminal(); + let line_buffered = matches.get_flag("line_buffered") || stdout_is_terminal; let with_filename = matches.get_flag("with_filename"); let no_filename = matches.get_flag("no_filename"); let label = matches @@ -392,7 +392,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ColorMode::Never => false, // On Windows, an unset TERM allows color support, matching anstyle-query. ColorMode::Auto => { - std::io::stdout().is_terminal() + stdout_is_terminal && match std::env::var_os("TERM") { Some(term) => term != "dumb", None => cfg!(windows), diff --git a/tests/buffering/mod.rs b/tests/buffering/mod.rs deleted file mode 100644 index 32d568e..0000000 --- a/tests/buffering/mod.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::process::Stdio; -use std::time::{Duration, Instant}; -use uutests::util::UChild; - -use super::ucmd; - -// Captured output is read by uutests in the background. Poll with a deadline -// while keeping stdin open, so EOF cannot accidentally satisfy the test. -fn wait_for_output(child: &mut UChild, stderr: bool) -> Vec { - let deadline = Instant::now() + Duration::from_secs(5); - loop { - let output = if stderr { - child.stderr_bytes() - } else { - child.stdout_bytes() - }; - if !output.is_empty() || Instant::now() >= deadline { - return output; - } - std::thread::sleep(Duration::from_millis(10)); - } -} - -#[cfg(unix)] -#[test] -fn terminal_flushes_matching_lines_before_eof() { - use std::io::Read; - use std::sync::mpsc; - use uutests::util::pty_path; - - for (options, expected) in [ - (vec![], "x\n"), - (vec!["--color=never"], "x\n"), - (vec!["--color=always"], "\x1b[01;31m\x1b[Kx\x1b[m\x1b[K\n"), - (vec!["--line-buffered", "--color=never"], "x\n"), - ] { - let (_scene, mut command) = ucmd(); - let (_, mut controller, replica) = pty_path(); - // Read the PTY directly: TerminalSimulation buffers its captured output. - let expected = expected.replace('\n', "\r\n").into_bytes(); - let length = expected.len(); - let (sender, receiver) = mpsc::channel(); - let reader = std::thread::spawn(move || { - let mut bytes = vec![0; length]; - let result = controller.read_exact(&mut bytes).map(|()| bytes); - let _ = sender.send(result); - }); - command - .set_stdin(Stdio::piped()) - .set_stdout(replica) - .args(&options) - .arg("x") - .env("GREP_COLORS", "") - .env("GREP_COLOR", "") - .timeout(Duration::from_secs(5)); - let mut child = command.run_no_wait(); - drop(command); - let written = child.try_write_in("x\n"); - let early = receiver.recv_timeout(Duration::from_secs(5)); - // Kill on missing/short output so the PTY reader also reaches EOF. - if !matches!(&early, Ok(Ok(_))) { - child.kill(); - } - // Reap and join before asserting, including the regression case. - let result = child.wait().unwrap(); - reader.join().unwrap(); - written.unwrap(); - assert_eq!(early.unwrap().unwrap(), expected, "options: {options:?}"); - result.success().no_stderr(); - } -} - -#[test] -fn explicit_line_buffering_flushes_pipe_before_eof() { - let (_scene, mut command) = ucmd(); - let mut child = command - .set_stdin(Stdio::piped()) - .args(&["--line-buffered", "--color=never", "x"]) - .timeout(Duration::from_secs(5)) - .run_no_wait(); - child.write_in("x\n"); - let early = wait_for_output(&mut child, false); - child.wait().unwrap().success().no_stderr(); - assert_eq!(early, b"x\n"); -} - -#[test] -fn redirected_output_remains_buffered_by_default() { - let (scene, mut command) = ucmd(); - scene.fixtures.write("first", "x\n"); - let mut child = command - .set_stdin(Stdio::piped()) - .args(&["--color=never", "-h", "x", "first", "missing", "-"]) - .timeout(Duration::from_secs(5)) - .run_no_wait(); - // The missing-file diagnostic follows the first file's match. It proves - // grep has processed that match before we check that stdout is buffered. - let ready = wait_for_output(&mut child, true); - std::thread::sleep(Duration::from_millis(200)); - let early = child.stdout_bytes(); - let result = child.wait().unwrap(); - result.code_is(2).stdout_is("x\n"); - assert!(!ready.is_empty(), "missing-file readiness diagnostic"); - assert!(early.is_empty(), "redirected output flushed before EOF"); -} diff --git a/tests/test_grep.rs b/tests/test_grep.rs index 9aaa188..fc6783b 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -3,8 +3,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use uutests::util::TerminalSimulation; use uutests::util::{TestScenario, UCommand}; -mod buffering; - static UCMD_INIT: AtomicBool = AtomicBool::new(false); fn ucmd() -> (TestScenario, UCommand) {