From 2b689a59b4c0fe55e87abf203e0f9b319fccc6e7 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:26:22 +0530 Subject: [PATCH 1/3] grep: pad -T line numbers and byte offsets like GNU grep A regular file on stdin got the fixed 19-column width of a pipe instead of a width from its size, and byte offsets were never padded. --- src/output.rs | 11 ++++++----- src/searcher.rs | 9 +++++++-- tests/test_grep.rs | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/output.rs b/src/output.rs index dc90127..4bf8ddf 100644 --- a/src/output.rs +++ b/src/output.rs @@ -30,13 +30,13 @@ impl<'a> OutputWriter<'a> { } } - /// Returns true if line numbers will be padded with whitespace (`-T`), - /// which means that you should call `set_line_number_width`. + /// Returns true if line numbers or byte offsets will be padded with + /// whitespace (`-T`), which means that you should call `set_line_number_width`. pub fn wants_padded_line_numbers(&self) -> bool { - self.config.initial_tab && self.config.line_number + self.config.initial_tab && (self.config.line_number || self.config.byte_offset) } - /// Set the minimum field width for line numbers (used with `-T`). + /// Set the minimum field width for line numbers and byte offsets (used with `-T`). /// /// It's sort of wrong for this to be a setter, because it's technically /// per-session state in searcher.rs, but it's convenient and it's cheap. @@ -143,9 +143,10 @@ impl<'a> OutputWriter<'a> { } if self.config.byte_offset { + let width = self.line_number_width; self.write_colored_fmt( self.config.color_config.byte_offset, - format_args!("{}", byte_offset), + format_args!("{:>width$}", byte_offset, width = width), )?; self.write_separator(sep_char)?; } diff --git a/src/searcher.rs b/src/searcher.rs index 64c4c40..611f9b2 100644 --- a/src/searcher.rs +++ b/src/searcher.rs @@ -107,9 +107,14 @@ impl<'a> Searcher<'a> { let path = Path::new(&self.config.label); - // Stdin is not seekable; use fixed width. + // Like a named file, a regular file on stdin sizes the field from its + // length; anything else (a pipe, a terminal) gets a fixed width. if self.writer.wants_padded_line_numbers() { - self.writer.set_line_number_width(19); + let width = match stdin.metadata() { + Ok(m) if m.is_file() => m.len().max(1).ilog10() as usize + 1, + _ => 19, + }; + self.writer.set_line_number_width(width); } // Where standard input was before we started buffering ahead of it. diff --git a/tests/test_grep.rs b/tests/test_grep.rs index 83a472f..b1bb43c 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -1049,6 +1049,25 @@ fn line_number_and_byte_offset_prefixes() { .stdout_contains("1:\tx\n"); } +#[test] +fn initial_tab_pads_to_the_input_size() { + // GNU grep pads line numbers and byte offsets under -T to the number of + // digits of the input's size; 100 bytes gives three columns. + let (scene, mut c) = ucmd(); + scene.fixtures.write("f", &format!("{}x\n", "a".repeat(98))); + c.args(&["-T", "-n", "-b", "x", "f"]) + .succeeds() + .stdout_only(format!(" 1: 0:\t{}x\n", "a".repeat(98))); + + // A regular file on stdin is sized the same way. + let (scene, mut c) = ucmd(); + scene.fixtures.write("f", &format!("{}x\n", "a".repeat(98))); + c.args(&["-T", "-n", "-b", "x"]) + .set_stdin(std::fs::File::open(scene.fixtures.plus("f")).unwrap()) + .succeeds() + .stdout_only(format!(" 1: 0:\t{}x\n", "a".repeat(98))); +} + #[test] fn null_filename_separator() { let (scene, mut c) = ucmd(); From 95e5e5e388a84398eb5ee6fd9d7d4df44392055d Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:23:39 +0530 Subject: [PATCH 2/3] grep: share the -T width formula and test the pipe case --- src/searcher.rs | 10 +++++++--- tests/test_grep.rs | 22 ++++++++++++++++------ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/searcher.rs b/src/searcher.rs index 611f9b2..9ab9416 100644 --- a/src/searcher.rs +++ b/src/searcher.rs @@ -39,6 +39,11 @@ pub struct Searcher<'a> { session_last_match_end: u64, } +/// The `-T` field width for an input of `size` bytes: the digits of its size. +fn padded_width(size: u64) -> usize { + size.max(1).ilog10() as usize + 1 +} + impl<'a> Searcher<'a> { pub fn new(config: &'a Config<'a>, matcher: Matcher<'a>, writer: OutputWriter<'a>) -> Self { Self { @@ -111,7 +116,7 @@ impl<'a> Searcher<'a> { // length; anything else (a pipe, a terminal) gets a fixed width. if self.writer.wants_padded_line_numbers() { let width = match stdin.metadata() { - Ok(m) if m.is_file() => m.len().max(1).ilog10() as usize + 1, + Ok(m) if m.is_file() => padded_width(m.len()), _ => 19, }; self.writer.set_line_number_width(width); @@ -213,8 +218,7 @@ impl<'a> Searcher<'a> { let result = File::open(path).and_then(|mut file| { if self.writer.wants_padded_line_numbers() { let file_size = file.metadata().map_or(0, |m| m.len()); - self.writer - .set_line_number_width(file_size.max(1).ilog10() as usize + 1); + self.writer.set_line_number_width(padded_width(file_size)); } self.session_run(lb, path, &mut file) }); diff --git a/tests/test_grep.rs b/tests/test_grep.rs index b1bb43c..db43799 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -1054,18 +1054,28 @@ fn initial_tab_pads_to_the_input_size() { // GNU grep pads line numbers and byte offsets under -T to the number of // digits of the input's size; 100 bytes gives three columns. let (scene, mut c) = ucmd(); - scene.fixtures.write("f", &format!("{}x\n", "a".repeat(98))); + let text = format!("{}x\n", "a".repeat(98)); + scene.fixtures.write("f", &text); + let expected = format!(" 1: 0:\t{text}"); c.args(&["-T", "-n", "-b", "x", "f"]) .succeeds() - .stdout_only(format!(" 1: 0:\t{}x\n", "a".repeat(98))); + .stdout_only(&expected); // A regular file on stdin is sized the same way. - let (scene, mut c) = ucmd(); - scene.fixtures.write("f", &format!("{}x\n", "a".repeat(98))); - c.args(&["-T", "-n", "-b", "x"]) + scene + .cmd(env!("CARGO_BIN_EXE_grep")) + .args(&["-T", "-n", "-b", "x"]) .set_stdin(std::fs::File::open(scene.fixtures.plus("f")).unwrap()) .succeeds() - .stdout_only(format!(" 1: 0:\t{}x\n", "a".repeat(98))); + .stdout_only(&expected); + + // A pipe has no size, so it gets the fixed width. + scene + .cmd(env!("CARGO_BIN_EXE_grep")) + .args(&["-bT", "x"]) + .pipe_in("x\n") + .succeeds() + .stdout_only(format!("{:>19}:\tx\n", 0)); } #[test] From c2799c52860bcc87dab4f6b5c6bb7e57cb63414a Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:49:02 +0530 Subject: [PATCH 3/3] grep: only check the -T pipe width on unix --- tests/test_grep.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_grep.rs b/tests/test_grep.rs index db43799..1772ffd 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -1069,7 +1069,9 @@ fn initial_tab_pads_to_the_input_size() { .succeeds() .stdout_only(&expected); - // A pipe has no size, so it gets the fixed width. + // A pipe has no size, so it gets the fixed width. On Windows the + // metadata of a pipe looks like a regular file's. + #[cfg(unix)] scene .cmd(env!("CARGO_BIN_EXE_grep")) .args(&["-bT", "x"])