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..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 { @@ -107,9 +112,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() => padded_width(m.len()), + _ => 19, + }; + self.writer.set_line_number_width(width); } // Where standard input was before we started buffering ahead of it. @@ -208,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 83a472f..1772ffd 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -1049,6 +1049,37 @@ 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(); + 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(&expected); + + // A regular file on stdin is sized the same way. + 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(&expected); + + // 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"]) + .pipe_in("x\n") + .succeeds() + .stdout_only(format!("{:>19}:\tx\n", 0)); +} + #[test] fn null_filename_separator() { let (scene, mut c) = ucmd();