Merge branch 'master' into fix-binary-detection-across-lines-3554

This commit is contained in:
Keith Hall authored and GitHub committed 2026-09-03 21:54:06 +03:00
commit 5fee86b97b
17 files changed
+395 -25

No files matched your search

+4
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)
@@ -25,6 +26,9 @@
## Bugfixes ## Bugfixes
- Detect binary content beyond the first line, preventing encrypted files with early line breaks from being treated as text. Closes #3554, see #3877 (@Matei02355) - Detect binary content beyond the first line, preventing encrypted files with early line breaks from being treated as text. Closes #3554, see #3877 (@Matei02355)
- Use the last occurrence of numbering and plain flags in combined short arguments, see #3897 (@cuishuang)
- Avoid a spurious Cargo manifest error during installation from Git, see #3899 (@rootsec1)
- Avoid repeated scans of long lines in Log syntax highlighting, see #3876 (@Matei02355)
- Fix `--list-languages` respecting `--paging=never`, see #3828 (@cyphercodes) - Fix `--list-languages` respecting `--paging=never`, see #3828 (@cyphercodes)
- Fix `--sanitize` passing through the bidi control characters U+200E, U+200F and U+061C, see #3862 (@lenamonj) - Fix `--sanitize` passing through the bidi control characters U+200E, U+200F and U+061C, see #3862 (@lenamonj)
- `--strip-ansi`: also strip 8-bit C1 introducers (U+0090, U+0098, U+009B, U+009D, U+009E, U+009F) and DCS/SOS/PM/APC sequence bodies, which previously passed through. See #3729 (@curious-rabbit) - `--strip-ansi`: also strip 8-bit C1 introducers (U+0090, U+0098, U+009B, U+009D, U+009E, U+009F) and DCS/SOS/PM/APC sequence bodies, which previously passed through. See #3729 (@curious-rabbit)
Generated
+10 -10
View File
@@ -281,9 +281,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]] [[package]]
name = "bytesize" name = "bytesize"
version = "2.4.2" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e" checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b"
[[package]] [[package]]
name = "cc" name = "cc"
@@ -362,9 +362,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]] [[package]]
name = "console" name = "console"
version = "0.16.3" version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
dependencies = [ dependencies = [
"encode_unicode", "encode_unicode",
"libc", "libc",
@@ -1604,9 +1604,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]] [[package]]
name = "globset" name = "globset"
version = "0.4.18" version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"bstr", "bstr",
@@ -1705,9 +1705,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.14.0" version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.1", "hashbrown 0.17.1",
@@ -2196,9 +2196,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.16" version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
+1 -1
View File
@@ -902,7 +902,7 @@ cargo install --path . --locked --force
``` ```
If you want to build an application that uses `bat`'s pretty-printing If you want to build an application that uses `bat`'s pretty-printing
features as a library, check out the [the API documentation](https://docs.rs/bat/). features as a library, check out the [API documentation](https://docs.rs/bat/).
Note that you have to use either `regex-onig` or `regex-fancy` as a feature Note that you have to use either `regex-onig` or `regex-fancy` as a feature
when you depend on `bat` as a library. when you depend on `bat` as a library.
+23 -4
View File
@@ -38,22 +38,41 @@ contexts:
scope: markup.underline.link.scheme.log scope: markup.underline.link.scheme.log
push: url-host push: url-host
log_level_lines: log_level_lines:
- match: (?=.*{{error}}) # Prevent retrying each lookahead at every position.
- match: ^(?=.*{{error}})
push: push:
- error_line_meta - error_line_meta
- main_pop_at_eol - main_pop_at_eol
- match: (?=.*{{warning}}) - match: ^(?=.*{{warning}})
push: push:
- warning_line_meta - warning_line_meta
- main_pop_at_eol - main_pop_at_eol
- match: (?=.*{{info}}) - match: ^(?=.*{{info}})
push: push:
- info_line_meta - info_line_meta
- main_pop_at_eol - main_pop_at_eol
- match: (?=.*{{debug}}) - match: ^(?=.*{{debug}})
push: push:
- debug_line_meta - debug_line_meta
- main_pop_at_eol - main_pop_at_eol
# For syntaxes that embed Log after consuming a prefix. Call this context once.
log_level_lines_at_current_position:
- match: (?=.*{{error}})
set:
- error_line_meta
- main_pop_at_eol
- match: (?=.*{{warning}})
set:
- warning_line_meta
- main_pop_at_eol
- match: (?=.*{{info}})
set:
- info_line_meta
- main_pop_at_eol
- match: (?=.*{{debug}})
set:
- debug_line_meta
- main_pop_at_eol
log_levels: log_levels:
- match: '{{error}}' - match: '{{error}}'
scope: markup.error.log scope: markup.error.log
+11 -1
View File
@@ -50,6 +50,17 @@ contexts:
- match: (?=\]) - match: (?=\])
pop: true pop: true
text: text:
- include: text_special
- match: ''
set: classify_log_text
classify_log_text:
- include: scope:text.log#log_level_lines_at_current_position
- match: ''
set: text_body
text_body:
- include: text_special
- include: scope:text.log
text_special:
- match: $ - match: $
pop: true pop: true
- match: '<\w+>' - match: '<\w+>'
@@ -62,4 +73,3 @@ contexts:
escape: \)$ escape: \)$
escape_captures: escape_captures:
0: punctuation.section.block.end.syslog 0: punctuation.section.block.end.syslog
- include: scope:text.log
+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>
+2 -2
View File
@@ -363,11 +363,11 @@ impl HighlightingAssets {
reader: &mut InputReader, reader: &mut InputReader,
) -> Result<Option<SyntaxReferenceInSet<'_>>> { ) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let syntax_set = self.get_syntax_set()?; let syntax_set = self.get_syntax_set()?;
Ok(String::from_utf8(reader.first_line.clone()) Ok(std::str::from_utf8(&reader.first_line)
.ok() .ok()
.and_then(|l| { .and_then(|l| {
// Strip UTF-8 BOM if present // Strip UTF-8 BOM if present
let line = l.strip_prefix('\u{feff}').unwrap_or(&l); let line = l.strip_prefix('\u{feff}').unwrap_or(l);
syntax_set.find_syntax_by_first_line(line) syntax_set.find_syntax_by_first_line(line)
}) })
.map(|syntax| SyntaxReferenceInSet { syntax, syntax_set })) .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
+43 -5
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 {
@@ -81,12 +85,12 @@ impl App {
return true; return true;
} }
// Handle combined short flags // Handle combined short flags
// Only count -n if it's the LAST flag in the combined form (so -p doesn't override it) // Only count -n if its last occurrence comes after the last -p,
// or if -p is not present in the combined form // or if -p is not present in the combined form.
if arg_str.starts_with('-') && !arg_str.starts_with("--") && arg_str.len() > 2 { if arg_str.starts_with('-') && !arg_str.starts_with("--") && arg_str.len() > 2 {
let chars: Vec<char> = arg_str.chars().skip(1).collect(); let chars: Vec<char> = arg_str.chars().skip(1).collect();
let n_pos = chars.iter().position(|&c| c == 'n'); let n_pos = chars.iter().rposition(|&c| c == 'n');
let p_pos = chars.iter().position(|&c| c == 'p'); let p_pos = chars.iter().rposition(|&c| c == 'p');
// -n is in the combined flag and either: // -n is in the combined flag and either:
// - -p is not present, OR // - -p is not present, OR
// - -n comes after -p (so -n takes effect) // - -n comes after -p (so -n takes effect)
@@ -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 by comparing the last -b and -p occurrences.
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().rposition(|&c| c == 'b');
let p_pos = chars.iter().rposition(|&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"))]
+12 -1
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
// line counter for empty lines (content-free lines that contain
// only the line terminator).
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 buffered_lines
.push_back((mem::take(&mut current_line_buffer), current_line_number)); .push_back((mem::take(&mut current_line_buffer), current_line_number));
current_line_number += 1; 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)?;
+239
View File
@@ -213,6 +213,106 @@ 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 repeated_combined_flags_use_last_number_or_plain_flag() {
bat()
.arg("multiline.txt")
.arg("-npn")
.arg("--decorations=auto")
.assert()
.success()
.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");
bat()
.arg("multiline.txt")
.arg("-pnp")
.arg("--decorations=auto")
.assert()
.success()
.stdout(
"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n",
);
bat()
.arg("empty_lines.txt")
.arg("-pbp")
.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");
bat()
.arg("empty_lines.txt")
.arg("-bpb")
.arg("--decorations=auto")
.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_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()
@@ -2579,6 +2679,145 @@ fn no_first_line_fallback_when_mapping_to_invalid_syntax() {
.stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'")); .stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'"));
} }
#[test]
fn stdin_detects_bash_from_first_line() {
let content = "#!/bin/bash\necho hi\n";
let detected_output = bat()
.arg("--color=always")
.arg("--style=plain")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
let explicit_output = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--language=bash")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
from_utf8(&detected_output).expect("output is valid utf-8"),
from_utf8(&explicit_output).expect("output is valid utf-8")
);
}
#[test]
fn stdin_detects_diff_from_first_line() {
let content = "diff --git a/x b/y\n--- a/x\n+++ b/y\n@@ -1 +1 @@\n-old\n+new\n";
let detected_output = bat()
.arg("--color=always")
.arg("--style=plain")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
let explicit_output = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--language=diff")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
from_utf8(&detected_output).expect("output is valid utf-8"),
from_utf8(&explicit_output).expect("output is valid utf-8")
);
}
#[test]
fn empty_stdin_does_not_detect_syntax() {
bat()
.arg("--color=always")
.arg("--style=plain")
.write_stdin("")
.assert()
.success()
.stdout("")
.stderr("");
}
#[test]
fn binary_stdin_does_not_detect_syntax_from_invalid_utf8_first_line() {
let content = b"#!/bin/bash\xff\necho hi\n";
let detected_output = bat()
.arg("--binary=as-text")
.arg("--color=always")
.arg("--style=plain")
.write_stdin(content.as_slice())
.assert()
.success()
.get_output()
.stdout
.clone();
let plain_text_output = bat()
.arg("--binary=as-text")
.arg("--color=always")
.arg("--style=plain")
.arg("--language=txt")
.write_stdin(content.as_slice())
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
from_utf8(&detected_output).expect("output is valid utf-8"),
from_utf8(&plain_text_output).expect("output is valid utf-8")
);
}
#[test]
fn explicit_language_overrides_stdin_first_line_detection() {
let content = "diff --git a/x b/y\n--- a/x\n+++ b/y\n@@ -1 +1 @@\n-old\n+new\n";
let detected_output = bat()
.arg("--color=always")
.arg("--style=plain")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
let explicit_output = bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--language=json")
.arg("-")
.write_stdin(content)
.assert()
.success()
.get_output()
.stdout
.clone();
assert_ne!(
from_utf8(&detected_output).expect("output is valid utf-8"),
from_utf8(&explicit_output).expect("output is valid utf-8")
);
}
#[test] #[test]
fn fallback_syntax_is_used_when_no_syntax_is_detected() { fn fallback_syntax_is_used_when_no_syntax_is_detected() {
let content = "# comment\nfoo=bar\n"; let content = "# comment\nfoo=bar\n";
File renamed without changes.