From c75cc01eef9b1a61611c9e10e767e7b90e44e29b Mon Sep 17 00:00:00 2001 From: Adrian Rivera Date: Tue, 30 Jun 2026 12:14:33 -0700 Subject: [PATCH] Fix --ignored-suffix to fall back to first-line detection When an ignored suffix is also a registered extension (e.g. `.txt` maps to "Plain Text"), `get_syntax_for_file_extension` matched the raw extension before attempting the suffix strip. As a result `--ignored-suffix .txt` had no effect on such files and first-line/shebang detection was never reached, so a shell script saved as `*.txt` stayed unhighlighted. Strip ignored suffixes first and detect on the remainder; only fall back to the raw extension when no ignored suffix applies. When a suffix is stripped but the remainder has no syntax, return `None` so the caller can use first-line detection. Default behavior (no `--ignored-suffix`) is unchanged. See #2745. --- CHANGELOG.md | 1 + src/assets.rs | 58 ++++++++++++++++--- .../examples/regression_tests/issue_2745.txt | 5 ++ tests/integration_tests.rs | 34 +++++++++++ 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 tests/examples/regression_tests/issue_2745.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index ce8fab72..78fdef3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres) ## Bugfixes +- Fix `--ignored-suffix` not falling back to first-line/shebang detection when the ignored suffix is also a registered extension (e.g. `--ignored-suffix .txt` on a shebang script), see #2745 and #3816 (@adnrivera) - Fix `capacity overflow` panic when printing a snip separator at `--terminal-width=1` with multiple line ranges. Closes #3803, see #3804 (@leeewee) - Pass `--no-paging` to `bat` invocations inside the bash / zsh / fish / PowerShell shell completion scripts so that shell-level pager wiring (e.g. `LESSOPEN='|-bat -f -pp %s'`) cannot inject ANSI escape sequences into the completion candidates. Closes #3760 (@mvanhorn) - Quote filenames before substituting them into `$LESSOPEN` / `$LESSCLOSE` templates, preventing shell injection when a filename contains shell metacharacters, see #3726 (@curious-rabbit) diff --git a/src/assets.rs b/src/assets.rs index 9483f032..8407678b 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -347,15 +347,15 @@ impl HighlightingAssets { file_name: &OsStr, ignored_suffixes: &IgnoredSuffixes, ) -> Result>> { - let mut syntax = self.find_syntax_by_extension(Path::new(file_name).extension())?; - if syntax.is_none() { - syntax = - ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| { - // Note: recursion - self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes) - })?; + let stripped = + ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| { + self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes) + .map(Some) + })?; + match stripped { + Some(syntax) => Ok(syntax), + None => self.find_syntax_by_extension(Path::new(file_name).extension()), } - Ok(syntax) } fn get_first_line_syntax( @@ -686,6 +686,48 @@ mod tests { ); } + #[test] + fn syntax_detection_ignored_suffix_falls_back_to_first_line() { + let mut test = SyntaxDetectionTest::new(); + + // By default a `.txt` file uses Plain Text, even with a shebang: the + // `.txt` extension wins and first-line detection is not reached. + assert_eq!( + test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"), + "Plain Text" + ); + + // Once `.txt` is an ignored suffix it is stripped before detection. The + // stripped name (`test`) has no extension, so detection falls back to the + // first line -- even though `.txt` is itself a registered extension that + // would otherwise match as Plain Text. See #2745. + test.syntax_mapping.insert_ignored_suffix(".txt"); + assert_eq!( + test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"), + "Bourne Again Shell (bash)" + ); + assert_eq!( + test.syntax_for_file_with_content("test.txt", " Plain Text match here. + assert_eq!( + test.syntax_for_file_with_content("notes.txt", "just some prose"), + "!no syntax!" + ); + + // Stripping that exposes a real extension still works: `.dev` is ignored, + // and the remaining `.json` extension is detected as usual. + test.syntax_mapping.insert_ignored_suffix(".dev"); + assert_eq!( + test.syntax_for_file_with_content("config.json.dev", ""), + "JSON" + ); + } + #[test] fn syntax_detection_is_case_insensitive() { let mut test = SyntaxDetectionTest::new(); diff --git a/tests/examples/regression_tests/issue_2745.txt b/tests/examples/regression_tests/issue_2745.txt new file mode 100644 index 00000000..b548c1a2 --- /dev/null +++ b/tests/examples/regression_tests/issue_2745.txt @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +NAME="${0##*/}" +for i in {1..3}; do + echo "hello $NAME $i" +done diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 2f2dcce8..3366bf0e 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -4248,3 +4248,37 @@ fn tcl_shebang_detection_expect() { .assert() .success(); } + +#[test] +fn ignored_suffix_enables_first_line_detection() { + // A shebang shell script saved with a `.txt` extension is Plain Text by + // default (the extension wins). With `--ignored-suffix .txt` the suffix is + // stripped before detection, so it falls back to the first line and is + // highlighted exactly as if its language were forced to bash. See #2745. + let fixture = "regression_tests/issue_2745.txt"; + let common = ["--color=always", "--decorations=never", "--style=plain"]; + + let stdout = |args: &[&str]| -> Vec { + let assert = bat() + .args(common) + .args(args) + .arg(fixture) + .assert() + .success(); + assert.get_output().stdout.clone() + }; + + let forced_bash = stdout(&["--language", "bash"]); + let with_ignored_suffix = stdout(&["--ignored-suffix", ".txt"]); + let default = stdout(&[]); + + // The fixture really is being highlighted (forcing bash is not a no-op). + assert!( + forced_bash.windows(2).any(|w| w == b"\x1b["), + "forced-bash output should contain ANSI color codes" + ); + // With the ignored suffix, detection matches forced bash highlighting... + assert_eq!(with_ignored_suffix, forced_bash); + // ...while the default (extension wins) stays plain and differs. + assert_ne!(default, forced_bash); +}