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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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]);
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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)?;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user