diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..67a37e28 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,11 @@ +# On Windows MSVC, statically link the C runtime so that the resulting EXE does +# not depend on the vcruntime DLL. +# +# See: https://github.com/sharkdp/bat/issues/3634 + +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.aarch64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a322a7..51b4b702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ ## Bugfixes - Sanitize control characters in filenames before displaying them in the file header, error messages, and the terminal title, preventing ANSI escape injection via crafted filenames. Closes #3054, see #3691 (@curious-rabbit) +- Report initial input read errors instead of treating them as empty input. Closes #3002, see #3706 (@lawrence3699) - Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar) - Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) - Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) @@ -37,15 +38,18 @@ - Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) - Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) - Fixed syntax tests path, see #3610 (@foxfromworld) +- Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz) ## Other - Use git version of cross. See #3533 (@OctopusET) - Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) - Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall) - Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon) +- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406) ## Syntaxes +- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn) - Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake) - Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall) - Add syntax highlighting support for COBOL, see #3584 (@adukhan99) diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 3c3f81d1..f8de56e4 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -90,7 +90,7 @@ _{{PROJECT_EXECUTABLE}}_main() { languages) local IFS=$'\n' local -a languages - languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') ) + languages=( ${(f)"$({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }')"} ) _describe 'language' languages && ret=0 ;; diff --git a/assets/patches/Tcl.sublime-syntax.patch b/assets/patches/Tcl.sublime-syntax.patch new file mode 100644 index 00000000..487a1841 --- /dev/null +++ b/assets/patches/Tcl.sublime-syntax.patch @@ -0,0 +1,12 @@ +diff --git syntaxes/01_Packages/TCL/Tcl.sublime-syntax syntaxes/01_Packages/TCL/Tcl.sublime-syntax +index 1234567..abcdefg 100644 +--- syntaxes/01_Packages/TCL/Tcl.sublime-syntax ++++ syntaxes/01_Packages/TCL/Tcl.sublime-syntax +@@ -3,6 +3,7 @@ + # http://www.sublimetext.com/docs/3/syntax.html + name: Tcl + file_extensions: + - tcl ++first_line_match: ^\#!.*\b(tclsh|wish|expect)\b + scope: source.tcl + variables: diff --git a/src/input.rs b/src/input.rs index 231e2a34..facd64bf 100644 --- a/src/input.rs +++ b/src/input.rs @@ -207,7 +207,7 @@ impl<'a> Input<'a> { kind: OpenedInputKind::StdIn, description, metadata: self.metadata, - reader: InputReader::new(stdin), + reader: InputReader::try_new(stdin)?, }) } @@ -236,14 +236,14 @@ impl<'a> Input<'a> { file = input_identifier.into_inner().expect("The file was lost in the clircle::Identifier, this should not have happened..."); } - InputReader::new(BufReader::new(file)) + InputReader::try_new(BufReader::new(file))? }, }), InputKind::CustomReader(reader) => Ok(OpenedInput { description, kind: OpenedInputKind::CustomReader, metadata: self.metadata, - reader: InputReader::new(BufReader::new(reader)), + reader: InputReader::try_new(BufReader::new(reader))?, }), } } @@ -257,24 +257,29 @@ pub(crate) struct InputReader<'a> { } impl<'a> InputReader<'a> { - pub(crate) fn new(mut reader: R) -> InputReader<'a> { + #[cfg(test)] + pub(crate) fn new(reader: R) -> InputReader<'a> { + Self::try_new(reader).expect("reading the first line failed") + } + + pub(crate) fn try_new(mut reader: R) -> io::Result> { let mut first_line = vec![]; - reader.read_until(b'\n', &mut first_line).ok(); + reader.read_until(b'\n', &mut first_line)?; let content_type = inspect_content_type(&first_line); if content_type == Some(ContentType::UTF_16LE) { - read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A).ok(); + read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?; } else if content_type == Some(ContentType::UTF_16BE) { - read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00).ok(); + read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?; } - InputReader { + Ok(InputReader { inner: Box::new(reader), first_line, content_type, unbuffered: false, - } + }) } pub(crate) fn read_line(&mut self, buf: &mut Vec) -> io::Result { @@ -405,6 +410,27 @@ fn non_zip_pk_prefix_is_not_treated_as_binary() { ); } +#[test] +fn input_open_returns_initial_read_errors() { + struct FailingRead; + + impl Read for FailingRead { + fn read(&mut self, _buf: &mut [u8]) -> io::Result { + Err(io::Error::other("initial read failed")) + } + } + + let input = Input::from_reader(Box::new(FailingRead)); + let result = input.open(io::empty(), None); + + assert!(result.is_err()); + assert!(result + .err() + .unwrap() + .to_string() + .contains("initial read failed")); +} + #[test] fn utf16le() { let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00"; diff --git a/src/lessopen.rs b/src/lessopen.rs index 966bc2c0..116206ba 100644 --- a/src/lessopen.rs +++ b/src/lessopen.rs @@ -155,7 +155,7 @@ impl LessOpenPreprocessor { Ok(OpenedInput { kind, - reader: InputReader::new(BufReader::new( + reader: InputReader::try_new(BufReader::new( if matches!(self.kind, LessOpenKind::TempFile) { let lessopen_string = match String::from_utf8(lessopen_stdout) { Ok(string) => string, @@ -192,7 +192,7 @@ impl LessOpenPreprocessor { .map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)), } }, - )), + ))?, metadata: input.metadata, description: input.description, }) diff --git a/tests/examples/regression_tests/issue_3647_expect b/tests/examples/regression_tests/issue_3647_expect new file mode 100644 index 00000000..3786df69 --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_expect @@ -0,0 +1,2 @@ +#!/usr/bin/expect -f +set timeout 30 diff --git a/tests/examples/regression_tests/issue_3647_tclsh b/tests/examples/regression_tests/issue_3647_tclsh new file mode 100644 index 00000000..0f4b3c5c --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_tclsh @@ -0,0 +1,2 @@ +#!/usr/bin/env tclsh +puts "Hello from tclsh" diff --git a/tests/examples/regression_tests/issue_3647_wish b/tests/examples/regression_tests/issue_3647_wish new file mode 100644 index 00000000..8bfe4baf --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_wish @@ -0,0 +1,2 @@ +#!/usr/bin/wish +button .b -text "Click" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 8ae4cfdd..432faed8 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -4202,3 +4202,36 @@ fn plain_without_diff_still_works() { .success() .stdout("line 1\nline 2 modified\nline 3\nline 4 added\n"); } + +#[test] +fn tcl_shebang_detection_tclsh() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_tclsh") + .assert() + .success(); +} + +#[test] +fn tcl_shebang_detection_wish() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_wish") + .assert() + .success(); +} + +#[test] +fn tcl_shebang_detection_expect() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_expect") + .assert() + .success(); +} diff --git a/tests/syntax-tests/highlighted/Tcl/expect_shebang b/tests/syntax-tests/highlighted/Tcl/expect_shebang new file mode 100644 index 00000000..871eed6c --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/expect_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/expect -f +# Expect script detected via expect shebang +set timeout 30 +spawn ssh user@host +expect "password:" +send "secret\r" +expect eof diff --git a/tests/syntax-tests/highlighted/Tcl/tclsh_shebang b/tests/syntax-tests/highlighted/Tcl/tclsh_shebang new file mode 100644 index 00000000..5461f5f8 --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/tclsh_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/env tclsh +# Tcl script detected via tclsh shebang +puts "Hello from tclsh" +set x 42 +if {$x > 0} { + puts "positive" +} diff --git a/tests/syntax-tests/highlighted/Tcl/wish_shebang b/tests/syntax-tests/highlighted/Tcl/wish_shebang new file mode 100644 index 00000000..94fb971f --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/wish_shebang @@ -0,0 +1,5 @@ +#!/usr/bin/wish +# Tk script detected via wish shebang +package require Tk +button .b -text "Click" -command {puts "clicked"} +pack .b diff --git a/tests/syntax-tests/source/Tcl/expect_shebang b/tests/syntax-tests/source/Tcl/expect_shebang new file mode 100644 index 00000000..ef288ba8 --- /dev/null +++ b/tests/syntax-tests/source/Tcl/expect_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/expect -f +# Expect script detected via expect shebang +set timeout 30 +spawn ssh user@host +expect "password:" +send "secret\r" +expect eof diff --git a/tests/syntax-tests/source/Tcl/tclsh_shebang b/tests/syntax-tests/source/Tcl/tclsh_shebang new file mode 100644 index 00000000..08faa4d6 --- /dev/null +++ b/tests/syntax-tests/source/Tcl/tclsh_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/env tclsh +# Tcl script detected via tclsh shebang +puts "Hello from tclsh" +set x 42 +if {$x > 0} { + puts "positive" +} diff --git a/tests/syntax-tests/source/Tcl/wish_shebang b/tests/syntax-tests/source/Tcl/wish_shebang new file mode 100644 index 00000000..690e73ef --- /dev/null +++ b/tests/syntax-tests/source/Tcl/wish_shebang @@ -0,0 +1,5 @@ +#!/usr/bin/wish +# Tk script detected via wish shebang +package require Tk +button .b -text "Click" -command {puts "clicked"} +pack .b