1
0
mirror of https://github.com/sharkdp/bat synced 2026-08-11 20:11:43 +00:00

Merge pull request #3857 from MeGaurav4/master

feat: implement `-b` / `--number-nonblank` for `cat -b` compatibility
This commit is contained in:
Keith Hall
2026-08-11 17:39:20 +03:00
committed by GitHub
10 changed files with 168 additions and 6 deletions
+1
View File
@@ -9,6 +9,7 @@
## Features ## Features
- Add `-b` / `--number-nonblank` flag to only number non-blank lines, for `cat -b` compatibility. Closes #3856, see #3857 (@MeGaurav4)
- Add a `--sanitize=<auto|always|never>` flag for safe display of untrusted input. It implies `--strip-ansi` at the same value and additionally substitutes terminal-active control bytes (cursor moves, charset switches, beep, etc.) and Unicode bidi / zero-width formatting characters with the Unicode replacement character (U+FFFD). Mitigates Trojan-Source-style spoofing (CVE-2021-42574). See #3729 (@curious-rabbit) - Add a `--sanitize=<auto|always|never>` flag for safe display of untrusted input. It implies `--strip-ansi` at the same value and additionally substitutes terminal-active control bytes (cursor moves, charset switches, beep, etc.) and Unicode bidi / zero-width formatting characters with the Unicode replacement character (U+FFFD). Mitigates Trojan-Source-style spoofing (CVE-2021-42574). See #3729 (@curious-rabbit)
- Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta) - Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta)
- Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn)
+10
View File
@@ -83,6 +83,16 @@ Options:
-n, --number -n, --number
Only show line numbers, no other decorations. This is an alias for '--style=numbers' Only show line numbers, no other decorations. This is an alias for '--style=numbers'
-b, --number-nonblank
Only show line numbers for non-blank lines, no other decorations. This is an alias for
'--style=numbers'. Non-blank lines are lines that contain any character before the line
ending, including spaces and tabs. When used together with --number (-n),
--number-nonblank (-b) takes precedence.
Example:
printf 'alpha\n\nbeta\n' | bat -b
numbers 'alpha' and 'beta', but skips the empty line.
--color <when> --color <when>
Specify when to use colored output. The automatic mode only enables colors if an Specify when to use colored output. The automatic mode only enables colors if an
interactive terminal is detected - colors are automatically disabled if the output goes to interactive terminal is detected - colors are automatically disabled if the output goes to
+2
View File
@@ -33,6 +33,8 @@ Options:
Truncate all lines longer than screen width. Alias for '--wrap=never'. Truncate all lines longer than screen width. Alias for '--wrap=never'.
-n, --number -n, --number
Show line numbers (alias for '--style=numbers'). Show line numbers (alias for '--style=numbers').
-b, --number-nonblank
Show line numbers for non-blank lines only (alias for '--style=numbers').
--color <when> --color <when>
When to use colors (*auto*, never, always). When to use colors (*auto*, never, always).
--italic-text <when> --italic-text <when>
+39 -1
View File
@@ -59,6 +59,10 @@ pub struct App {
/// (not from config file or environment variables). /// (not from config file or environment variables).
/// This is used to honor the flag when piping output, similar to `cat -n`. /// This is used to honor the flag when piping output, similar to `cat -n`.
number_from_cli: bool, number_from_cli: bool,
/// True if -b / --number-nonblank was passed on the command line
/// (not from config file or environment variables).
/// This is used to honor the flag when piping output, similar to `cat -b`.
number_nonblank_from_cli: bool,
} }
impl App { impl App {
@@ -99,6 +103,29 @@ impl App {
false false
}); });
// Check if the -b / --number-nonblank option was passed on the command line
// (before merging with config file and environment variables).
// This is needed to honor the -b flag when piping output, similar to `cat -b`.
// The same combined-flag logic applies as for -n above.
let number_nonblank_from_cli = wild::args_os().any(|arg| {
let arg_str = arg.to_string_lossy();
if arg_str == "-b" || arg_str == "--number-nonblank" {
return true;
}
// Handle combined short flags
if arg_str.starts_with('-') && !arg_str.starts_with("--") && arg_str.len() > 2 {
let chars: Vec<char> = arg_str.chars().skip(1).collect();
let b_pos = chars.iter().position(|&c| c == 'b');
let p_pos = chars.iter().position(|&c| c == 'p');
if let Some(b) = b_pos {
if p_pos.is_none() || b > p_pos.unwrap() {
return true;
}
}
}
false
});
let matches = Self::matches(interactive_output)?; let matches = Self::matches(interactive_output)?;
if matches.get_flag("help") { if matches.get_flag("help") {
@@ -139,6 +166,7 @@ impl App {
matches, matches,
interactive_output, interactive_output,
number_from_cli, number_from_cli,
number_nonblank_from_cli,
}) })
} }
@@ -454,7 +482,8 @@ impl App {
.map(|s| s.as_str()) .map(|s| s.as_str())
== Some("always") == Some("always")
|| self.matches.get_flag("force-colorization") || self.matches.get_flag("force-colorization")
|| self.number_from_cli), || self.number_from_cli
|| self.number_nonblank_from_cli),
tab_width: self tab_width: self
.matches .matches
.get_one::<String>("tabs") .get_one::<String>("tabs")
@@ -495,6 +524,8 @@ impl App {
), ),
quiet_empty: self.matches.get_flag("quiet-empty"), quiet_empty: self.matches.get_flag("quiet-empty"),
unbuffered: self.matches.get_flag("unbuffered"), unbuffered: self.matches.get_flag("unbuffered"),
number_nonblank: self.matches.get_flag("number-nonblank")
|| self.number_nonblank_from_cli,
theme: theme(self.theme_options()).to_string(), theme: theme(self.theme_options()).to_string(),
visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default() visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default()
&& self.matches.get_flag("diff") && self.matches.get_flag("diff")
@@ -613,6 +644,13 @@ impl App {
]))); ])));
} }
// Only line numbers for non-blank lines if `--number-nonblank`.
if self.matches.get_flag("number-nonblank") || self.number_nonblank_from_cli {
return Some(StyleComponents(HashSet::from([
StyleComponent::LineNumbers,
])));
}
// Plain if `--plain` is specified at least once. // Plain if `--plain` is specified at least once.
if self.matches.get_count("plain") > 0 { if self.matches.get_count("plain") > 0 {
let mut components = HashSet::from([StyleComponent::Plain]); let mut components = HashSet::from([StyleComponent::Plain]);
+18
View File
@@ -97,6 +97,7 @@ pub fn build_app(interactive_output: bool) -> Command {
Arg::new("plain") Arg::new("plain")
.overrides_with("plain") .overrides_with("plain")
.overrides_with("number") .overrides_with("number")
.overrides_with("number-nonblank")
.short('p') .short('p')
.long("plain") .long("plain")
.action(ArgAction::Count) .action(ArgAction::Count)
@@ -277,6 +278,23 @@ pub fn build_app(interactive_output: bool) -> Command {
'--style=numbers'", '--style=numbers'",
), ),
) )
.arg(
Arg::new("number-nonblank")
.long("number-nonblank")
.overrides_with("number-nonblank")
.short('b')
.action(ArgAction::SetTrue)
.help("Show line numbers for non-blank lines only (alias for '--style=numbers').")
.long_help(
"Only show line numbers for non-blank lines, no other decorations. This is an \
alias for '--style=numbers'. Non-blank lines are lines that contain any \
character before the line ending, including spaces and tabs. When used \
together with --number (-n), --number-nonblank (-b) takes precedence.\n\n\
Example:\n \
printf 'alpha\\n\\nbeta\\n' | bat -b\n \
numbers 'alpha' and 'beta', but skips the empty line.",
),
)
.arg( .arg(
Arg::new("color") Arg::new("color")
.long("color") .long("color")
+4
View File
@@ -119,6 +119,10 @@ pub struct Config<'a> {
/// Whether or not to use unbuffered input reading for streaming use cases /// Whether or not to use unbuffered input reading for streaming use cases
pub unbuffered: bool, pub unbuffered: bool,
/// Only number non-blank lines (like `cat -b`). Has no effect if `style_components` doesn't
/// include `LineNumbers`.
pub number_nonblank: bool,
} }
#[cfg(all(feature = "minimal-application", feature = "paging"))] #[cfg(all(feature = "minimal-application", feature = "paging"))]
+15 -4
View File
@@ -276,10 +276,21 @@ impl Controller<'_> {
} }
if !reached_eof { if !reached_eof {
if reader.read_line(&mut current_line_buffer)? { if reader.read_line(&mut current_line_buffer)? {
// Fill the buffer // Fill the buffer. In number-nonblank mode, don't advance the
buffered_lines // line counter for empty lines (content-free lines that contain
.push_back((mem::take(&mut current_line_buffer), current_line_number)); // only the line terminator).
current_line_number += 1; if self.config.number_nonblank
&& current_line_buffer
.iter()
.all(|&b| b == b'\r' || b == b'\n')
{
buffered_lines
.push_back((mem::take(&mut current_line_buffer), current_line_number));
} else {
buffered_lines
.push_back((mem::take(&mut current_line_buffer), current_line_number));
current_line_number += 1;
}
} else { } else {
// No more data to read // No more data to read
reached_eof = true; reached_eof = true;
+8
View File
@@ -45,6 +45,14 @@ impl Decoration for LineNumberDecoration {
continuation: bool, continuation: bool,
_printer: &InteractivePrinter, _printer: &InteractivePrinter,
) -> DecorationText { ) -> DecorationText {
if line_number == 0 {
// Blank line in number-nonblank mode: show empty space instead of a number.
return DecorationText {
text: self.color.paint(" ".repeat(self.width())).to_string(),
width: self.width(),
};
}
if continuation { if continuation {
if line_number >= self.cached_wrap_invalid_at { if line_number >= self.cached_wrap_invalid_at {
let new_width = self.cached_wrap.width + 1; let new_width = self.cached_wrap.width + 1;
+8 -1
View File
@@ -738,10 +738,17 @@ impl Printer for InteractivePrinter<'_> {
// Line decorations. // Line decorations.
if self.panel_width > 0 { if self.panel_width > 0 {
let display_line_number =
if self.config.number_nonblank && line.trim_end_matches(['\r', '\n']).is_empty() {
0
} else {
line_number
};
let decorations = self let decorations = self
.decorations .decorations
.iter() .iter()
.map(|d| d.generate(line_number, false, self)); .map(|d| d.generate(display_line_number, false, self));
for deco in decorations { for deco in decorations {
write!(handle, "{} ", deco.text)?; write!(handle, "{} ", deco.text)?;
+63
View File
@@ -213,6 +213,69 @@ fn numbers_honored_from_cli_when_preceeded_by_plain_in_loop_through_mode() {
.stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n");
} }
#[test]
fn number_nonblank_style() {
bat()
.arg("empty_lines.txt")
.arg("-b")
.arg("--decorations=always")
.assert()
.success()
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
}
#[test]
fn number_nonblank_from_cli_in_loop_through_mode() {
bat()
.arg("empty_lines.txt")
.arg("-b")
.assert()
.success()
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
}
#[test]
fn number_nonblank_takes_precedence_over_number() {
// -bn should behave like -b
bat()
.arg("empty_lines.txt")
.arg("-bn")
.arg("--decorations=always")
.assert()
.success()
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
// -nb should also behave like -b
bat()
.arg("empty_lines.txt")
.arg("-nb")
.arg("--decorations=always")
.assert()
.success()
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
}
#[test]
fn number_nonblank_ignored_when_followed_by_plain() {
bat()
.arg("empty_lines.txt")
.arg("-bp")
.arg("--decorations=auto")
.assert()
.success()
.stdout("line 1\n\n\n\nline 5\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n");
}
#[test]
fn piped_output_with_number_nonblank_flag() {
bat()
.arg("-b")
.write_stdin("hello\n\nworld\n")
.assert()
.success()
.stdout(" 1 hello\n \n 2 world\n");
}
#[test] #[test]
fn line_range_2_3() { fn line_range_2_3() {
bat() bat()