mirror of
https://github.com/sharkdp/bat.git
synced 2026-10-26 20:16:33 +00:00
Merge branch 'master' into fix-vcredist-link
This commit is contained in:
9 files changed
+213
-66
No files matched your search
@@ -26,6 +26,11 @@
|
||||
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
|
||||
|
||||
## Bugfixes
|
||||
- Allow boolean flags to be given more than once, so that a flag set in the config file can also be passed on the command line without erroring, see #3912 (@logarithmone1128)
|
||||
- Use parsed CLI arguments to detect number flags, see #3908 (@cuishuang)
|
||||
- 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 `--sanitize` passing through the bidi control characters U+200E, U+200F and U+061C, see #3862 (@lenamonj)
|
||||
|
||||
Generated
+10
-10
@@ -281,9 +281,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
version = "2.4.2"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e"
|
||||
checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
@@ -362,9 +362,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.3"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
|
||||
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
@@ -1604,9 +1604,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||
checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
@@ -1705,9 +1705,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
version = "2.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
@@ -2196,9 +2196,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.16"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
||||
+10
-53
@@ -72,59 +72,12 @@ impl App {
|
||||
|
||||
let interactive_output = std::io::stdout().is_terminal();
|
||||
|
||||
// Check if the -n / --number option was passed on the command line
|
||||
// (before merging with config file and environment variables).
|
||||
// This is needed to honor the -n flag when piping output, similar to `cat -n`.
|
||||
// We need to handle both standalone (-n, --number) and combined short flags (-pn, -An, etc.)
|
||||
// Note: We only check if -n appears and is not overridden by -p in the same combined flag.
|
||||
// For combined flags like -np, -p comes after -n and overrides it, so we don't count it.
|
||||
// For combined flags like -pn, -n comes after -p and takes effect.
|
||||
let number_from_cli = wild::args_os().any(|arg| {
|
||||
let arg_str = arg.to_string_lossy();
|
||||
if arg_str == "-n" || arg_str == "--number" {
|
||||
return true;
|
||||
}
|
||||
// Handle combined short flags
|
||||
// Only count -n if it's the LAST flag in the combined form (so -p doesn't override it)
|
||||
// or if -p is not present in the combined form
|
||||
if arg_str.starts_with('-') && !arg_str.starts_with("--") && arg_str.len() > 2 {
|
||||
let chars: Vec<char> = arg_str.chars().skip(1).collect();
|
||||
let n_pos = chars.iter().position(|&c| c == 'n');
|
||||
let p_pos = chars.iter().position(|&c| c == 'p');
|
||||
// -n is in the combined flag and either:
|
||||
// - -p is not present, OR
|
||||
// - -n comes after -p (so -n takes effect)
|
||||
if let Some(n) = n_pos {
|
||||
if p_pos.is_none() || n > p_pos.unwrap() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
});
|
||||
// Parse the original command line separately from config and environment arguments.
|
||||
// Using clap here keeps CLI-only detection consistent with the final parser, including
|
||||
// option terminators, combined short flags, and options that take values.
|
||||
let cli_matches = Self::cli_matches(interactive_output);
|
||||
let number_from_cli = cli_matches.get_flag("number");
|
||||
let number_nonblank_from_cli = cli_matches.get_flag("number-nonblank");
|
||||
|
||||
let matches = Self::matches(interactive_output)?;
|
||||
|
||||
@@ -238,6 +191,10 @@ impl App {
|
||||
args
|
||||
}
|
||||
|
||||
fn cli_matches(interactive_output: bool) -> ArgMatches {
|
||||
clap_app::build_app(interactive_output).get_matches_from(wild::args_os())
|
||||
}
|
||||
|
||||
fn matches(interactive_output: bool) -> Result<ArgMatches> {
|
||||
// Check if we should skip config file processing for special arguments
|
||||
// that don't require full application setup (version, diagnostic)
|
||||
|
||||
@@ -52,6 +52,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
.long("show-all")
|
||||
.alias("show-nonprintable")
|
||||
.short('A')
|
||||
.overrides_with("show-all")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("language")
|
||||
.help("Show non-printable characters (space, tab, newline, ..).")
|
||||
@@ -171,6 +172,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("diff")
|
||||
.long("diff")
|
||||
.short('d')
|
||||
.overrides_with("diff")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("line-range")
|
||||
.help("Only show lines that have been added/removed/modified.")
|
||||
@@ -235,6 +237,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("chop-long-lines")
|
||||
.long("chop-long-lines")
|
||||
.short('S')
|
||||
.overrides_with("chop-long-lines")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Truncate all lines longer than screen width. Alias for '--wrap=never'."),
|
||||
)
|
||||
@@ -473,6 +476,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("squeeze-blank")
|
||||
.long("squeeze-blank")
|
||||
.short('s')
|
||||
.overrides_with("squeeze-blank")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Squeeze consecutive empty lines.")
|
||||
.long_help("Squeeze consecutive empty lines into a single empty line.")
|
||||
@@ -598,6 +602,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("unbuffered")
|
||||
.short('u')
|
||||
.long("unbuffered")
|
||||
.overrides_with("unbuffered")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Enable unbuffered input reading for streaming use cases.")
|
||||
.long_help(
|
||||
@@ -619,6 +624,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
.arg(
|
||||
Arg::new("no-custom-assets")
|
||||
.long("no-custom-assets")
|
||||
.overrides_with("no-custom-assets")
|
||||
.action(ArgAction::SetTrue)
|
||||
.hide(true)
|
||||
.help("Do not load custom assets"),
|
||||
@@ -699,6 +705,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("quiet-empty")
|
||||
.long("quiet-empty")
|
||||
.short('E')
|
||||
.overrides_with("quiet-empty")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Produce no output when the input is empty.")
|
||||
.long_help(
|
||||
@@ -717,6 +724,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
.arg(
|
||||
Arg::new("set-terminal-title")
|
||||
.long("set-terminal-title")
|
||||
.overrides_with("set-terminal-title")
|
||||
.action(ArgAction::SetTrue)
|
||||
.hide_short_help(true)
|
||||
.help("Sets terminal title to filenames when using a pager."),
|
||||
|
||||
+71
-3
@@ -9,6 +9,8 @@ use content_inspector::{self, ContentType};
|
||||
|
||||
use crate::error::*;
|
||||
|
||||
const CONTENT_INSPECTION_LIMIT: usize = 1024;
|
||||
|
||||
/// A description of an Input source.
|
||||
/// This tells bat how to refer to the input.
|
||||
#[derive(Clone)]
|
||||
@@ -263,10 +265,29 @@ impl<'a> InputReader<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn try_new<R: BufRead + 'a>(mut reader: R) -> io::Result<InputReader<'a>> {
|
||||
let mut first_line = vec![];
|
||||
reader.read_until(b'\n', &mut first_line)?;
|
||||
// content_inspector scans at most 1024 bytes. Capture the already-buffered
|
||||
// prefix before splitting out the first line so an early newline in binary
|
||||
// data does not shorten the inspected content. This does not consume input
|
||||
// or perform an additional read beyond the one read_until needs anyway.
|
||||
let mut inspection_prefix = {
|
||||
let buffered = reader.fill_buf()?;
|
||||
buffered[..buffered.len().min(CONTENT_INSPECTION_LIMIT)].to_vec()
|
||||
};
|
||||
|
||||
let content_type = inspect_content_type(&first_line);
|
||||
let mut first_line = vec![];
|
||||
if !inspection_prefix.is_empty() {
|
||||
reader.read_until(b'\n', &mut first_line)?;
|
||||
}
|
||||
|
||||
// A custom BufRead implementation may expose less than 1024 bytes at a
|
||||
// time. Keep the old behavior for long first lines in that case.
|
||||
let first_line_prefix_len = first_line.len().min(CONTENT_INSPECTION_LIMIT);
|
||||
if first_line_prefix_len > inspection_prefix.len() {
|
||||
inspection_prefix.clear();
|
||||
inspection_prefix.extend_from_slice(&first_line[..first_line_prefix_len]);
|
||||
}
|
||||
|
||||
let content_type = inspect_content_type(&inspection_prefix);
|
||||
|
||||
if content_type == Some(ContentType::UTF_16LE) {
|
||||
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?;
|
||||
@@ -410,6 +431,53 @@ fn non_zip_pk_prefix_is_not_treated_as_binary() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_detection_scans_beyond_first_line_and_preserves_input() {
|
||||
let mut content = vec![b'a'; CONTENT_INSPECTION_LIMIT + 1];
|
||||
content[1] = b'\n';
|
||||
content[CONTENT_INSPECTION_LIMIT - 1] = 0;
|
||||
|
||||
let mut reader = InputReader::new(&content[..]);
|
||||
assert_eq!(Some(ContentType::BINARY), reader.content_type);
|
||||
|
||||
let mut replayed = Vec::new();
|
||||
while reader.read_line(&mut replayed).unwrap() {}
|
||||
assert_eq!(content, replayed);
|
||||
|
||||
drop(reader);
|
||||
|
||||
content[CONTENT_INSPECTION_LIMIT - 1] = b'a';
|
||||
content[CONTENT_INSPECTION_LIMIT] = 0;
|
||||
|
||||
let reader = InputReader::new(&content[..]);
|
||||
assert_eq!(Some(ContentType::UTF_8), reader.content_type);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_detection_does_not_read_twice() {
|
||||
struct OneRead(Option<&'static [u8]>);
|
||||
|
||||
impl Read for OneRead {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self.0.take() {
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"no more data is available yet",
|
||||
)),
|
||||
Some(content) => {
|
||||
buf[..content.len()].copy_from_slice(content);
|
||||
Ok(content.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (content, expected) in [(&b"text\n"[..], Some(ContentType::UTF_8)), (&b""[..], None)] {
|
||||
let input = InputReader::try_new(BufReader::new(OneRead(Some(content)))).unwrap();
|
||||
assert_eq!(expected, input.content_type);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_open_returns_initial_read_errors() {
|
||||
struct FailingRead;
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
--show-all
|
||||
@@ -213,6 +213,70 @@ 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");
|
||||
}
|
||||
|
||||
#[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 option_terminator_keeps_filename_from_number_flags() {
|
||||
let tmp_dir = tempdir().expect("can create temporary directory");
|
||||
std::fs::write(tmp_dir.path().join("-b"), "hello\nworld\n").expect("can write temporary file");
|
||||
|
||||
bat()
|
||||
.current_dir(tmp_dir.path())
|
||||
.args(["--", "-b"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("hello\nworld\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attached_short_option_values_are_not_treated_as_flags() {
|
||||
let tmp_dir = tempdir().expect("can create temporary directory");
|
||||
std::fs::write(tmp_dir.path().join("input.txt"), "hello\nworld\n")
|
||||
.expect("can write temporary file");
|
||||
|
||||
bat()
|
||||
.current_dir(tmp_dir.path())
|
||||
.args(["-lbn", "input.txt"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("hello\nworld\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_nonblank_style() {
|
||||
bat()
|
||||
@@ -582,6 +646,30 @@ fn piped_output_with_default_style_flag() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_boolean_flag_is_accepted() {
|
||||
// A flag in the config file is prepended to the command line, so a user
|
||||
// who has `--show-all` in their config and also types `-A` ends up passing
|
||||
// it twice. That must not be an error.
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("--show-all")
|
||||
.arg("--show-all")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boolean_flag_in_config_and_on_command_line() {
|
||||
bat_with_config()
|
||||
.env("BAT_CONFIG_PATH", "bat-show-all.conf")
|
||||
.arg("empty_lines.txt")
|
||||
.arg("--show-all")
|
||||
.arg("--paging=never")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn squeeze_blank() {
|
||||
bat()
|
||||
@@ -2227,6 +2315,26 @@ fn header_binary() {
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/sharkdp/bat/issues/3554
|
||||
#[test]
|
||||
fn header_binary_with_null_after_first_line() {
|
||||
let tmp_dir = tempdir().expect("can create temporary directory");
|
||||
let tmp_path = tmp_dir.path().join("encrypted.gpg");
|
||||
std::fs::write(&tmp_path, b"packet-header\npayload\0bytes\n")
|
||||
.expect("can write temporary file");
|
||||
|
||||
bat()
|
||||
.arg(&tmp_path)
|
||||
.arg("--decorations=always")
|
||||
.arg("--style=header")
|
||||
.arg("--line-range=0:0")
|
||||
.arg("--file-name=encrypted.gpg")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("File: encrypted.gpg <BINARY>\n")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_zip_file_is_binary() {
|
||||
let tmp_dir = tempdir().expect("can create temporary directory");
|
||||
|
||||
Vendored
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user