Compare commits

...
Author SHA1 Message Date
Keith Hall 6561174ab8 Merge pull request #4119 from FFspiky/test/highlight-color-tty
test: cover automatic highlight colors on pipes and terminals
2026-11-08 10:24:11 +02:00
FFspiky 833a1a7647 test: cover automatic highlight colors on pipes and terminals 2026-11-08 15:47:47 +08:00
Keith Hall c231941daa Merge pull request #4118 from pcbeingused333/fix/utf16-nonblank-c1626
fix: skip empty UTF-16 lines in nonblank numbering
2026-11-07 05:35:08 +02:00
Alex Castillo 5dd9196bef docs: fill in PR number in changelog entry 2026-11-06 22:29:36 -04:00
Alex Castillo c89e356840 fix: skip empty UTF-16 lines in nonblank numbering 2026-11-06 19:05:05 -04:00
Keith Hall 7187ec3c02 Merge pull request #4117 from sharkdp/changelog_fix
fix wrong merge result in CHANGELOG.md
2026-11-07 00:35:56 +02:00
Keith Hall a691ab6c74 fix wrong merge result in CHANGELOG.md
one entry became duplicated with a placeholder PR number
2026-11-07 00:23:11 +02:00
Keith Hall 3e58878e43 Merge pull request #4100 from DeviousCardi/fix-highlight-line-color-never
Don't emit highlight escape sequences with --color=never
2026-11-06 21:43:21 +02:00
Keith Hall 1b92cd272b Merge branch 'master' into fix-highlight-line-color-never 2026-11-06 21:27:08 +02:00
Keith Hall d14e46c393 Merge pull request #4115 from rezamuliawijaya/mintty-terminal-detection
fix: detect mintty/MSYS2/Cygwin terminals as interactive output on Windows
2026-11-06 18:16:51 +02:00
rezamuliawijaya 903808ea3e docs: add changelog entry for mintty terminal detection fix 2026-11-06 22:48:14 +07:00
rezamuliawijaya 4701d15bc5 fix: detect mintty/MSYS2/Cygwin terminals as interactive output on Windows
Since #3478 moved help rendering into bat's own pipeline, the decision to
colorize (and page) is gated on std::io::stdout().is_terminal(), which
reports false under mintty/MSYS2 and Cygwin because their pseudo-terminals
are named pipes. Fall back to the console crate's is_msys_tty() check so
interactive sessions in those terminals get colors, the pager, and
decorations again, while real pipes (e.g. bat file | grep) stay plain.

Fixes #3034
2026-11-06 22:32:25 +07:00
Keith Hall 408801f2a7 Merge pull request #4113 from b84330808/fix-map-syntax-glob-colon
Make full-path syntax mappings work on Windows
2026-11-05 22:58:20 +02:00
Keith Hall 3c8ca42bbc Merge branch 'master' into fix-map-syntax-glob-colon 2026-11-05 22:47:05 +02:00
b84330808 ba5fac9c29 Test a mapping written with a verbatim prefix 2026-11-06 03:47:12 +09:00
Keith Hall 956522a359 Merge pull request #4094 from nikarh/cue-lang
Add syntax highlighting for CUE
2026-11-05 18:41:02 +02:00
b84330808 b843b05927 Make full-path syntax mappings work on Windows
`--map-syntax` split its argument on every `:`, so `C:/dir/*.conf:INI` was
rejected as an invalid mapping. Split on the last `:` instead.

The pattern still could not match, because `PathAbs` and `fs::canonicalize`
return verbatim paths such as `\\?\C:\dir\file.conf` on Windows. That also kept
the built-in `${HOME}` and `${XDG_CONFIG_HOME}` mappings from ever matching.
Try the path without the verbatim prefix as well.
2026-11-05 21:26:58 +09:00
Nikolay Arhipov ed4cb7e2e9 Merge branch 'master' into cue-lang 2026-11-05 00:42:50 +02:00
Keith Hall b47e686563 Merge pull request #4112 from Marve10s/fix/config-option-terminator
Keep configuration active for option-like filenames
2026-11-04 22:35:08 +02:00
Keith Hall a9d75b780b Merge branch 'master' into fix/config-option-terminator 2026-11-04 21:29:48 +02:00
Keith Hall 5bdd8e2800 Merge pull request #4108 from Marve10s/fix/emoji-character-wrapping
Preserve emoji sequences when wrapping content
2026-11-04 20:54:31 +02:00
Keith Hall 3e2eb6f9a8 Merge branch 'master' into fix/emoji-character-wrapping 2026-11-04 20:38:43 +02:00
Keith Hall cbdad20b48 Merge pull request #4111 from Marve10s/fix/syntax-mapping-suffix-path
Preserve path mappings when ignoring backup suffixes
2026-11-04 20:37:03 +02:00
Marve10s b2ef386d0b Reference configuration filename pull request in changelog 2026-11-04 20:08:34 +03:00
Marve10s 3dba15138d Use parsed CLI flags when loading configuration 2026-11-04 20:07:00 +03:00
Marve10s 87c8610ffb Merge master after UTF-16 alignment fix 2026-11-04 20:00:48 +03:00
Keith Hall 73676df96d Merge pull request #4110 from Marve10s/fix/utf16-line-alignment
Keep UTF-16 line splitting aligned to code units
2026-11-04 18:56:45 +02:00
Marve10s a0fd127e1e Reference syntax mapping pull request in changelog 2026-11-04 19:51:55 +03:00
Marve10s 4cbcb2fdbf Preserve standalone retries for stripped dot names 2026-11-04 19:43:23 +03:00
Marve10s b2cb0352f8 Reference UTF-16 alignment pull request in changelog 2026-11-04 19:34:35 +03:00
Marve10s c83792cfba Preserve syntax mapping paths when stripping ignored suffixes 2026-11-04 19:30:23 +03:00
Marve10s 94e9ae480c Respect UTF-16 code-unit boundaries when splitting lines 2026-11-04 19:24:00 +03:00
Marve10s ac510bb19c Retry CI after hosted runner provisioning failure 2026-11-04 18:53:09 +03:00
Marve10s 0e8994a3f5 Add changelog reference for emoji wrapping fix 2026-11-04 18:31:46 +03:00
Marve10s ad3f336f76 Keep emoji graphemes intact when wrapping content 2026-11-04 18:02:02 +03:00
Keith Hall 41931bc4ad Merge pull request #3801 from xfocus3/fix/help-language-submodule-note
docs: explain submodules when regenerating syntax assets
2026-11-03 22:51:58 +02:00
xfocus3 78f54b3fee Merge master and preserve documentation changelog for #3801 2026-11-03 20:26:18 +00:00
Keith Hall d9479df104 Merge pull request #4026 from ump45nose/docs/assets-anchor-case
docs: fix anchor case in assets.md
2026-11-03 20:41:35 +02:00
Keith Hall d19cca2349 Merge branch 'master' into docs/assets-anchor-case 2026-11-03 20:05:59 +02:00
Keith Hall 46031658a0 Merge pull request #4098 from feafel/fix/wrapped-line-background-underflow
fix: avoid underflow when painting a wrapped line background
2026-11-03 19:50:21 +02:00
Keith Hall b99f3176f9 Merge branch 'master' into fix/wrapped-line-background-underflow 2026-11-03 19:35:18 +02:00
Keith Hall 5f971d9a40 Merge pull request #4104 from jaeminbird/win-pager-path
Keep backslashes in the pager command on Windows
2026-11-03 19:31:42 +02:00
jaeminbird 599bae4594 Quote the pager path in pager_windows_absolute_path
A checkout under a path with spaces split the unquoted path at the
first space. Quoting it also covers the quoted-path form end to end.
2026-11-03 00:35:25 -05:00
jaeminbird d6a2958df2 Merge branch 'master' into win-pager-path 2026-11-03 00:32:33 -05:00
Keith Hall 67e2ec24e0 Merge pull request #4048 from qodelicious/fix/no-color-error-prefix
Respect NO_COLOR for [bat error] and [bat warning] prefixes
2026-11-02 22:07:29 +02:00
Keith Hall d0920557da Merge branch 'master' into fix/no-color-error-prefix 2026-11-02 21:51:30 +02:00
Keith Hall cb6bb8eb03 Merge pull request #4038 from mmustafasenoglu/fix/cache-auto-build-4017
Build cache automatically on first run when custom sources exist
2026-11-02 21:48:23 +02:00
Keith Hall 1752596aa1 Merge branch 'master' into fix/cache-auto-build-4017 2026-11-02 21:09:54 +02:00
Keith Hall 46eacfbbb8 Merge pull request #3911 from logarithmone1128/kubeconfig-yaml-mapping
Map .kube/config to YAML
2026-11-02 20:57:44 +02:00
Keith Hall d389ea3869 Merge branch 'master' into kubeconfig-yaml-mapping 2026-11-02 20:46:47 +02:00
Keith Hall 0db13cbd37 Merge pull request #4070 from ump45nose/ps1-completion-wrap-auto
fix(completions): the PowerShell completion offers an invalid --wrap value
2026-11-02 15:03:58 +02:00
Keith Hall 673bac99eb Merge branch 'master' into ps1-completion-wrap-auto 2026-11-02 14:52:13 +02:00
Keith Hall 6ca658c80a Merge pull request #4103 from jaeminbird/config-bom
Ignore a UTF-8 BOM at the start of the config file
2026-11-02 13:08:25 +02:00
Keith Hall 6b2e311d79 Merge branch 'master' into config-bom 2026-11-02 12:54:14 +02:00
Keith Hall 1a2189789f Merge pull request #4102 from Huang-404-Q/fix/repeatable-valued-options
Allow value-taking options to be repeated, like boolean flags
2026-11-02 12:53:08 +02:00
Nikolay Arhipov 57160c1bdd Merge branch 'master' into cue-lang 2026-11-02 10:45:17 +02:00
huang b5281b5a4f fix(cli): allow valued options to be repeated
The config file is prepended to the command line, so an option set in
both places is passed twice. Boolean flags were made repeatable in
#3912, but options that take a value were missed: --binary,
--italic-text, --nonprintable-notation, --fallback-syntax and
--squeeze-limit were rejected with 'cannot be used multiple times',
which aborts bat with exit code 2 before it reads any input.

Add self-overrides so that the last occurrence wins, matching the
behavior of --color, --theme, --wrap, --tabs and friends.
2026-11-02 13:50:48 +08:00
auto-merge-dependabot-prs[bot] e95c32b6f0 Merge pull request #4106 from sharkdp/dependabot/github_actions/vedantmgoyal9/winget-releaser-b7370d4d152efdce803a26cc6ca9eee58cb2ad54
build(deps): bump vedantmgoyal9/winget-releaser from a8fff44b123f115ce4c21543edf8b677376fc7e0 to b7370d4d152efdce803a26cc6ca9eee58cb2ad54
2026-11-01 03:16:44 +00:00
dependabot[bot] f059c8da38 build(deps): bump vedantmgoyal9/winget-releaser
Bumps [vedantmgoyal9/winget-releaser](https://github.com/vedantmgoyal9/winget-releaser) from a8fff44b123f115ce4c21543edf8b677376fc7e0 to b7370d4d152efdce803a26cc6ca9eee58cb2ad54.
- [Release notes](https://github.com/vedantmgoyal9/winget-releaser/releases)
- [Commits](https://github.com/vedantmgoyal9/winget-releaser/compare/a8fff44b123f115ce4c21543edf8b677376fc7e0...b7370d4d152efdce803a26cc6ca9eee58cb2ad54)

---
updated-dependencies:
- dependency-name: vedantmgoyal9/winget-releaser
  dependency-version: b7370d4d152efdce803a26cc6ca9eee58cb2ad54
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-01 03:03:56 +00:00
yuwk 9507f53270 Merge remote-tracking branch 'upstream/master' into ps1-completion-wrap-auto
Signed-off-by: yuwk <1729065730@qq.com>

# Conflicts:
#	CHANGELOG.md
2026-10-31 21:49:19 +08:00
yuwk 42a60bcce2 Merge remote-tracking branch 'upstream/master' into docs/assets-anchor-case
Signed-off-by: yuwk <1729065730@qq.com>

# Conflicts:
#	CHANGELOG.md
2026-10-31 21:49:18 +08:00
jaeminbird 0318353e9d Keep backslashes in the pager command on Windows 2026-10-30 01:59:41 -04:00
jaeminbird 7df6406ca9 Ignore a UTF-8 BOM at the start of the config file 2026-10-30 01:59:32 -04:00
Keith Hall 8f5727ea38 Merge pull request #4068 from ump45nose/diag-list-bat-config-dir
Report BAT_CONFIG_DIR in bat --diagnostic
2026-10-29 21:45:30 +02:00
Keith Hall b4fdcec6a1 Merge branch 'master' into diag-list-bat-config-dir 2026-10-29 21:29:06 +02:00
xfocus3 f06fd51c5a Merge current master and retain submodule documentation 2026-10-29 08:54:50 +00:00
Keith Hall fe901f5051 Merge pull request #4101 from PerchAnton/codex/bat-i686-runner
ci: pin the i686 GNU runner to Ubuntu 24.04
2026-10-28 21:36:22 +02:00
PerchAnton 3e2c1bdc8e docs: link the i686 runner fix in changelog 2026-10-28 13:50:21 +02:00
PerchAnton e423744a7d ci: pin i686 GNU runner to Ubuntu 24.04 2026-10-28 13:48:22 +02:00
DeviousCardiandClaude Opus 5.5 445608e4e6 Add changelog entry
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 11:55:27 +05:30
DeviousCardiandClaude Opus 5.5 135582e99b Don't emit highlight escape sequences with --color=never
`--highlight-line` is rendered with escape sequences only (the theme's
line highlight background, or an underline for the `ansi` theme), but
they were emitted even when colored output was disabled.

Skip line highlighting when `colored_output` is false. The theme
selection tests relied on that leak to detect the theme in use, so they
now request colors explicitly.

Fixes #4099

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 11:54:56 +05:30
feafel e0af722689 chore: reference PR number in changelog 2026-10-25 12:31:47 +07:00
feafel 26899e31cf fix: avoid underflow when painting a wrapped line background
A double-width character does not fit in a line narrower than the
character, so the wrap loop leaves the cursor past cursor_max. The
background fill then computed cursor_max - cursor, which underflows to
a huge value and aborts in str::repeat (capacity overflow).

Use saturating_sub for the remaining width, and for the wrap loop's
initial max_width which can hit the same condition across text chunks.

Closes #3844.
2026-10-25 12:31:36 +07:00
Nikolay Arhipov 41100fffcf changelog: add CUE syntax entry (#4094) 2026-10-22 14:42:49 +03:00
Nikolay Arhipov 9a633511a8 Add syntax highlighting for CUE
Add the official CUE grammar repository cue-lang/vscode-cue as a
submodule, and CUE.sublime-syntax converted from its
extension/syntaxes/cue.tmLanguage.json at commit 16796709f36c:

1. Inline rules that only group other patterns, which Sublime Text's
   converter rejects (cue-lang/cue#3427):

       jq 'walk(if type == "object" and has("patterns") then .patterns |= [.[] | if type == "object" and keys == ["patterns"] then .patterns[] else . end] else . end)' \
           extension/syntaxes/cue.tmLanguage.json > CUE.tmLanguage.json

2. Convert the JSON grammar to a plist:

       python3 -c 'import json, plistlib, sys; plistlib.dump(json.load(sys.stdin), sys.stdout.buffer)' \
           < CUE.tmLanguage.json > CUE.tmLanguage

3. Convert CUE.tmLanguage with Sublime Text's converter
   ("Tools → Developer → New Syntax from CUE.tmLanguage…").

The result reproduces all token goldens in vscode-cue's
extension/testdata/syntax with both the oniguruma and fancy-regex engines.
2026-10-22 14:33:50 +03:00
Keith Hall a9aa73d215 Merge pull request #3895 from 1t1sCooL/fix-vcredist-link
docs: fix dead Visual C++ Redistributable link in README
2026-10-20 22:18:09 +03:00
Keith Hall f0a58595bd Merge branch 'master' into fix-vcredist-link 2026-10-20 22:07:28 +03:00
Keith Hall 2fd71ad588 Merge pull request #4091 from NavpreetSSidhu/fix-js-block-comment-hang
Fix hang on block comment after an expression in JavaScript
2026-10-20 22:03:22 +03:00
Navpreet Sidhu 49d9ac1d6e changelog: reference PR #4091 2026-10-20 21:40:03 +05:30
Navpreet Sidhu 54b1f9c6af Fix hang on block comment after an expression in JavaScript 2026-10-20 21:31:39 +05:30
Mikhail Alabugin b841eadf09 Merge upstream into PR branch, resolve conflicts 2026-10-20 13:59:30 +03:00
qodelicious c199cc427a Add changelog entry for #4048 2026-10-18 07:04:43 +00:00
qodelicious f3cc4b6f03 Respect NO_COLOR for [bat error] and [bat warning] prefixes
The error and warning prefixes were always painted red/yellow, even
when NO_COLOR is set, which bat otherwise honors for its output.
2026-10-18 07:04:20 +00:00
yuwk 62db8f51b4 Merge remote-tracking branch 'up/master' into docs/assets-anchor-case
# Conflicts:
#	CHANGELOG.md
2026-10-18 00:28:37 +08:00
yuwk 4475d88b47 Merge remote-tracking branch 'up/master' into ps1-completion-wrap-auto
# Conflicts:
#	CHANGELOG.md
2026-10-18 00:28:36 +08:00
yuwk b49aae8b1f Merge remote-tracking branch 'up/master' into diag-list-bat-config-dir
# Conflicts:
#	CHANGELOG.md
2026-10-18 00:28:36 +08:00
Keith Hall f748c1a467 Merge pull request #4069 from injust/patch-1
Add syntax mapping for Podman `.quadlets` files
2026-10-17 14:57:57 +03:00
Keith Hall d3afbaeb06 Merge branch 'master' into patch-1 2026-10-17 14:48:35 +03:00
Keith Hall dbe6ec265d Merge pull request #4076 from cindykrafft/fix/utf16be-first-line
Fix the first line of UTF-16BE input swallowing the second line
2026-10-17 14:41:31 +03:00
Keith Hall bc0a67e07c Merge branch 'master' into fix/utf16be-first-line 2026-10-17 14:28:44 +03:00
Keith Hall fca6c542d4 Merge pull request #4067 from ump45nose/docs-strip-ansi-mode
Document StripAnsiMode and its variants
2026-10-17 14:27:03 +03:00
Claude c2adbf6caa Fix the first line of UTF-16BE input swallowing the second line
In UTF-16BE a newline is 00 0A, so the initial read_until(b'\n') usually
already ends exactly at the end of line 1. The BE branch then always called
read_utf16_line, which read on to the next newline, merging lines 1 and 2.
Only keep reading when the first read did not end on a complete 00 0A.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVn4SeAXPsUajXT2rtHQfo
2026-10-16 19:50:14 +00:00
ump45nose fef8513f6e Add changelog entry for #4070 2026-10-16 11:19:17 +08:00
yuwk 61340f0e42 fix(completions): --wrap has no 'always' mode, the value is 'auto' 2026-10-16 07:43:59 +08:00
injust f48149b119 Add syntax mapping for Podman .quadlets files 2026-10-15 21:52:43 +00:00
yuwk ca5eeb7c2a Add changelog entry for BAT_CONFIG_DIR in --diagnostic 2026-10-16 02:04:46 +08:00
yuwk 9438a74e6d Report BAT_CONFIG_DIR in bat --diagnostic
BAT_CONFIG_DIR overrides the config directory (src/bin/bat/directories.rs) and is documented in the README, but it was missing from the environment list that bat --diagnostic prints, so a bug report never showed where bat was actually reading its config from. Extend diagnostic_sanity_check to cover it.
2026-10-16 02:04:02 +08:00
ump45nose 8c63b37205 changelog: note StripAnsiMode rustdoc (#4067) 2026-10-15 15:13:56 +08:00
yuwk d50b53d0ef Document StripAnsiMode and its variants with doc comments 2026-10-15 09:37:35 +08:00
Keith Hall 62b439dae3 Merge pull request #4064 from ump45nose/doc-range-check-result-variants
Document RangeCheckResult variants with doc comments
2026-10-14 22:41:06 +03:00
Keith Hall e8d05da87a Merge branch 'master' into doc-range-check-result-variants 2026-10-14 22:22:14 +03:00
Keith Hall edd161aefb Merge pull request #4065 from ump45nose/doc-wrapping-mode-variant
Document WrappingMode::NoWrapping's bool payload with a doc comment
2026-10-14 22:22:03 +03:00
Keith Hall b95f8d37bf Merge branch 'master' into doc-range-check-result-variants 2026-10-14 22:20:52 +03:00
yuwk 0e3074cfe1 Document WrappingMode::NoWrapping's bool payload with a doc comment
The explanation of what the payload means used //, so rustdoc dropped it
from the public WrappingMode page. WrappingMode is re-exported from
lib.rs, so the variant is documented like the neighbouring enums.
2026-10-15 02:49:37 +08:00
Keith Hall 4d916905d1 Merge pull request #4063 from ump45nose/config-doc-comments
Document 4 public Config fields with doc comments
2026-10-14 20:59:49 +03:00
yuwk c47a163f4b Document RangeCheckResult variants with doc comments 2026-10-15 01:55:16 +08:00
yuwk a56bca503b Fix doc comments on four Config fields
The use_lessopen, set_terminal_title, strip_ansi and sanitize fields were
annotated with // instead of ///, so they were silently missing from the
rustdoc output for the public bat::config::Config struct. Every sibling
field already uses ///.
2026-10-15 00:23:49 +08:00
ump45nose 164902d69b Merge branch 'master' into docs/assets-anchor-case 2026-10-12 00:00:41 +08:00
ump45nose ec3f691a53 Add changelog entry 2026-10-10 19:42:34 +08:00
Keith Hall d9559c69f5 Merge pull request #4035 from Mathjk/fix/aux-epipe
Fix panic on closed stdout in auxiliary output paths
2026-10-07 22:49:03 +03:00
ump45nose 6c779b38c2 docs: fix anchor case in assets.md 2026-10-05 00:24:38 +08:00
Mustafa Senoglu 78ea14bd9a Add PR number to changelog entry 2026-10-02 21:02:35 +03:00
Mustafa Senoglu 6ae020f998 Build cache automatically on first run when custom sources exist
Custom themes and syntaxes dropped into the config directory were
silently ignored until 'bat cache --build' was run manually. Now bat
builds the cache on first run when sources are present but no cache
exists yet. A failed build only warns and falls back to the integrated
assets; --no-custom-assets opts out.

Fixes #4017
2026-10-02 21:01:45 +03:00
Mathjk 12cbb20de4 changelog: reference PR #4035 so the changelog check passes 2026-10-03 01:03:09 +08:00
Mathjk a9b112b6fe Exit quietly on broken pipe in auxiliary output paths
The main output path has exited with status 0 on EPIPE since #232, but
auxiliary outputs still used println!/print!/io::stdout().flush().unwrap()
and panicked (exit code 101) when the reader of a piped stdout went away
early, e.g. 'bat --completion bash | head -n0'.

Route them through the existing convention instead: write!/writeln! to
io::stdout() and '?' so that Error::Io(BrokenPipe) reaches
default_error_handler, which exits with status 0. This covers
--completion, --config-file, --diagnostic (bugreport's print() becomes
format() + writeln!), --generate-config-file, the 'cache' subcommand
progress messages, the OSC title sequence of --set-terminal-title, and
the 'built without feature' notices.

Closes #4034
2026-10-02 02:23:08 +08:00
xfocus3 5dbfe7fea3 docs: clarify submodule use when rebuilding assets 2026-10-01 13:18:55 +01:00
auto-merge-dependabot-prs[bot] 4608fc959a Merge pull request #4027 from sharkdp/dependabot/github_actions/vedantmgoyal9/winget-releaser-a8fff44b123f115ce4c21543edf8b677376fc7e0
build(deps): bump vedantmgoyal9/winget-releaser from 19e706d4c9121098010096f9c495a70a7518b30f to a8fff44b123f115ce4c21543edf8b677376fc7e0
2026-10-01 02:13:09 +00:00
dependabot[bot] dcd725dc4e build(deps): bump vedantmgoyal9/winget-releaser
Bumps [vedantmgoyal9/winget-releaser](https://github.com/vedantmgoyal9/winget-releaser) from 19e706d4c9121098010096f9c495a70a7518b30f to a8fff44b123f115ce4c21543edf8b677376fc7e0.
- [Release notes](https://github.com/vedantmgoyal9/winget-releaser/releases)
- [Commits](https://github.com/vedantmgoyal9/winget-releaser/compare/19e706d4c9121098010096f9c495a70a7518b30f...a8fff44b123f115ce4c21543edf8b677376fc7e0)

---
updated-dependencies:
- dependency-name: vedantmgoyal9/winget-releaser
  dependency-version: a8fff44b123f115ce4c21543edf8b677376fc7e0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-01 02:03:55 +00:00
LOG 279395d607 Merge remote-tracking branch 'origin/master' into kubeconfig-yaml-mapping 2026-09-07 09:13:17 +08:00
Mikhail Alabugin eca6c5b688 Merge branch 'master' into fix-vcredist-link 2026-09-05 16:01:25 +03:00
LOG 5b30913c55 Add changelog entry 2026-09-04 17:38:09 +08:00
LOG 9fbbfb60ec Map .kube/config to YAML
kubectl's config file has no extension, so it falls back to plain text.
The format is YAML, and `**/.kube/config` follows the same shape as the
existing `**/.aws/config` and gcloud mappings.
2026-09-04 17:37:45 +08:00
Mikhail Alabugin ea3909bbdf Add changelog entry 2026-09-02 11:10:47 +03:00
Mikhail Alabugin a3f202f367 docs: fix dead Visual C++ Redistributable link in README 2026-08-27 13:11:46 +03:00
34 changed files with 2731 additions and 132 deletions

No files matched your search

+3 -2
View File
@@ -166,7 +166,8 @@ jobs:
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-latest , dpkg_arch: armhf, use-cross: true }
- { target: arm-unknown-linux-musleabihf, os: ubuntu-latest , dpkg_arch: musl-linux-armhf, use-cross: true }
- { target: i686-pc-windows-msvc , os: windows-2025 , }
- { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i386, use-cross: true }
# The timeout test's socketpair fails on the Ubuntu 26.04 runner.
- { target: i686-unknown-linux-gnu , os: ubuntu-24.04 , dpkg_arch: i386, use-cross: true }
- { target: i686-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-i686, use-cross: true }
- { target: x86_64-apple-darwin , os: macos-15-intel, }
- { target: aarch64-apple-darwin , os: macos-latest , }
@@ -457,7 +458,7 @@ jobs:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
- uses: vedantmgoyal9/winget-releaser@b7370d4d152efdce803a26cc6ca9eee58cb2ad54
with:
identifier: sharkdp.bat
installers-regex: '-pc-windows-msvc\.zip$'
+3
View File
@@ -284,3 +284,6 @@
[submodule "assets/syntaxes/02_Extra/Caddy"]
path = assets/syntaxes/02_Extra/Caddy
url = https://github.com/caddyserver/sublimetext.git
[submodule "assets/syntaxes/02_Extra/CUE"]
path = assets/syntaxes/02_Extra/CUE
url = https://github.com/cue-lang/vscode-cue.git
+27
View File
@@ -3,15 +3,22 @@
## Other
- Document submodule initialization when rebuilding syntax and theme assets, see #3801 (@xfocus3)
- Fix anchor case in `doc/assets.md` links, see #4026 (@ump45nose)
- Pin the i686 GNU CI runner to Ubuntu 24.04 to avoid `socketpair` failures on Ubuntu 26.04, see #4101 (@PerchAnton)
- Fix dead Visual C++ Redistributable link in README, see #3895 (@1t1sCooL)
- Document `StripAnsiMode` and its variants in rustdoc, see #4067 (@ump45nose)
- Update Cargo dependencies to resolve current RustSec advisories, see #3861 (@TyceHerrman)
- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536
- Add .NET slnx extension, see #3682 (@ltrzesniewski)
## Features
- Automatically build the syntax/theme cache on first run when custom themes or syntaxes are present but no cache exists yet, so newly installed dotfiles work without a manual `bat cache --build`. Use `--no-custom-assets` to opt out. Closes #4017, see #4038 (@mmustafasenoglu)
- 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)
- Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta)
- Map `.kube/config` to YAML syntax highlighting, see #3911 (@logarithmone1128)
- Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn)
- Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355)
- Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey)
@@ -25,8 +32,25 @@
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
## Bugfixes
- Fix `--number-nonblank` counting empty UTF-16 lines, see #4118 (@pcbeingused333)
- Don't emit `--highlight-line` escape sequences (background color or `ansi` theme underline) when colors are disabled with `--color=never`. Closes #4099, see #4100 (@DeviousCardi)
- Fix missing colors, pager, and decorations in mintty/MSYS2 and Cygwin terminals on Windows, see #4115 (@rezamuliawijaya)
- Make full-path `--map-syntax` patterns and the built-in `$HOME` and `$XDG_CONFIG_HOME` mappings work on Windows, see #4113 (@b84330808)
- Keep configuration and BAT_OPTS active for option-like filenames after `--`, and report invalid configuration for help-like filenames, see #4112 (@Marve10s)
- Keep emoji sequences together within text chunks when wrapping content, and measure their combined display width, see #4108 (@Marve10s)
- Preserve full-path syntax mappings when removing ignored filename suffixes, see #4111 (@Marve10s)
- Fix UTF-16 line splitting at unaligned newline bytes corrupting text and shifting line ranges, see #4110 (@Marve10s)
- Respect `NO_COLOR` for the `[bat error]` and `[bat warning]` prefixes, see #4048 (@qodelicious)
- Offer the valid `--wrap` value `auto` instead of the rejected `always` in the PowerShell completion, see #4070 (@ump45nose)
- Report `BAT_CONFIG_DIR` in `bat --diagnostic`, so bug reports show a config directory that overrides `$XDG_CONFIG_HOME`. See #4068 (@ump45nose)
- Ignore a UTF-8 byte order mark at the start of the config file instead of treating it as part of the first argument, see #4103 (@jaeminbird)
- Keep backslashes in the pager command on Windows, so `BAT_PAGER=C:\tools\less.exe` is found instead of becoming `C:toolsless.exe`, see #4104 (@jaeminbird)
- Fix `bat` hanging forever on JavaScript (and Svelte) code where an expression is followed by a block comment, e.g. `a /* x */`. Closes #4080, see #4091 (@NavpreetSSidhu)
- Fix the first line of UTF-16BE input swallowing the second line, which broke line numbers, `--line-range` and highlighting, see #4102 (@cindykrafft)
- Exit quietly with status 0 instead of panicking when stdout is closed early (broken pipe) on auxiliary output paths such as `--completion`, `--config-file`, `--diagnostic`, `--generate-config-file`, `--set-terminal-title` and `cache --build`/`--clear`, matching the main output path since #232, closes #4034, see #4035 (@Mathjk)
- Track strikethrough (SGR 9 / 29) in the ANSI style tracker so it is re-emitted like bold, dim, italic, and underline, and is no longer dropped after the first highlighted token when displaying ANSI input with syntax highlighting, see #4015 (@mikehasa)
- 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)
- Allow options that take a value (`--binary`, `--italic-text`, `--nonprintable-notation`, `--fallback-syntax`, `--squeeze-limit`) to be given more than once, so that a value from the config file can be overridden on the command line, see #4102 (@Huang-404-Q)
- 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)
@@ -85,6 +109,8 @@
- Add syntax highlighting for `Caddyfile` #3789 (@CosmicHorrorDev)
- Include `.code-workspace` as a JSON extension #3809 (@dhruvkb)
- Add syntax mapping for DNF repo configuration files, see #3814 (@injust)
- Add syntax mapping for Podman `.quadlets` files, see #4069 (@injust)
- Add syntax highlighting for CUE, see #4094 (@nikarh)
## Themes
@@ -100,6 +126,7 @@
## Bugfixes
- Fix `capacity overflow` panic when wrapping a double-width character on a line narrower than the character while a background is painted (e.g. `--terminal-width=1 --wrap=character --highlight-line=1`). Closes #3844, see #4098 (@feafel)
- Fix hang when using `--list-themes` with an explicit pager, see #3457 (@abhinavcool42)
- Fix negative values of N not being parsed in <N:M> line ranges without `=` flag value separator, see #3442 (@lmmx)
- Fix broken Docker syntax preventing use of custom assets, see #3476 (@keith-hall)
+1 -1
View File
@@ -104,7 +104,7 @@ tempfile = "3.27.0"
serde = { version = "1.0", features = ["derive"] }
[target.'cfg(unix)'.dev-dependencies]
nix = { version = "0.31", default-features = false, features = ["term"] }
nix = { version = "0.31", default-features = false, features = ["poll", "term"] }
[build-dependencies]
anyhow = "1.0.97"
+8 -2
View File
@@ -417,7 +417,7 @@ scoop install bat
You can download prebuilt binaries from the [Release page](https://github.com/sharkdp/bat/releases),
You will need to install the [Visual C++ Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads) package.
You will need to install the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist) package.
### From binaries
@@ -431,6 +431,12 @@ If you want to build `bat` from source, you need Rust 1.79.0 or
higher. You can then use `cargo` to build everything:
#### From local source
The checkout includes prebuilt syntax and theme assets. If you want to regenerate them
from their sources, first initialize the Git submodules with
`git submodule update --init --recursive`, then follow the [Development](#development)
instructions. This includes the `help` syntax used to highlight command-line help.
```bash
cargo install --path . --locked
```
@@ -794,7 +800,7 @@ Example configuration file:
### Prerequisites
You will need to install the [Visual C++ Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads) package.
You will need to install the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist) package.
### Paging
+1 -1
View File
@@ -9,7 +9,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
$ArrayCompletion = @('bash', 'fish', 'zsh', 'ps1')
$ArrayWhen = @('auto', 'never', 'always')
$ArrayYesNo = @('never', 'always')
$ArrayWrap = @('always', 'never', 'character', 'word')
$ArrayWrap = @('auto', 'never', 'character', 'word')
$ArrayBinary = @('no-printing', 'as-text')
$ArrayPrint = @('unicode', 'caret')
+10
View File
@@ -12,3 +12,13 @@ index 05a4fed6..78a7bf55 100644
scope: source.js
variables:
bin_digit: '[01_]'
@@ -93,7 +93,8 @@ variables:
(?! \+\+ | -- )
(?=
!= |
- [-+*/%><=&|^\[(;,.:?] |
+ [-+*%><=&|^\[(;,.:?] |
+ /(?!\*) |
(?:in|instanceof){{identifier_break}}
)
))
+952
View File
@@ -0,0 +1,952 @@
%YAML 1.2
---
# http://www.sublimetext.com/docs/3/syntax.html
name: CUE
file_extensions:
- cue
scope: source.cue
contexts:
main:
- include: invalid_slash
- include: whitespace
- include: comment
- match: '(?<![\p{L}\p{Nd}_\$\#])(package)[ \t]+([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*)(?![\p{L}\p{Nd}_\$\#])'
captures:
1: keyword.other.package
2: entity.name.namespace
- match: '(?<![\p{L}\p{Nd}_\$\#])(import)[ \t]+(\()'
captures:
1: keyword.other.import
2: punctuation.section.parens.begin
push:
- meta_scope: meta.imports
- match: \)
captures:
0: punctuation.section.parens.end
pop: true
- include: whitespace
- include: comment
- match: '(?:([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+|_)[ \t]+)?(")([^:"]+)(?:(:)([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*))?(")'
scope: meta.import-spec
captures:
1: entity.name.namespace
2: punctuation.definition.string.begin
3: string.quoted.double-import
4: punctuation.colon
5: entity.name
6: punctuation.definition.string.end
- match: ;
scope: punctuation.separator
- include: invalid_in_parens
- match: '(?<![\p{L}\p{Nd}_\$\#])(import)[ \t]+(?:([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+|_)[ \t]+)?(")([^:"]+)(?:(:)([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*))?(")'
scope: meta.import
captures:
1: keyword.other.import
2: entity.name.namespace
3: punctuation.definition.string.begin
4: string.quoted.double-import
5: punctuation.colon
6: entity.name
7: punctuation.definition.string.end
- include: punctuation_comma
- include: declaration
- include: invalid_in_braces
comment:
- match: (//).*$\n?
scope: comment.line
captures:
1: punctuation.definition.comment
alias_postfix:
- match: '(~)[ \t]*(?:([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+|_)|(\()[ \t]*([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+|_)(?:[ \t]*(,)[ \t]*([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+|_))?[ \t]*(\)))'
captures:
1: punctuation.alias
2: variable.other
3: punctuation.section.parens.begin
4: variable.other
5: punctuation.separator
6: variable.other
7: punctuation.section.parens.end
attribute:
- match: '(@)([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+)(\()'
captures:
1: punctuation.definition.annotation
2: variable.annotation
3: punctuation.attribute-elements.begin
push:
- meta_scope: meta.annotation
- match: \)
captures:
0: punctuation.attribute-elements.end
pop: true
- include: comment
- include: punctuation_comma
- include: attribute_element
attribute_element:
- match: '[ \t]*(?<![\p{L}\p{Nd}_\$\#])((?:[\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+)(?:\.(?:[\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+))*)[ \t]*(=)(?!=)'
captures:
1: variable.other
2: punctuation.bind
push:
- match: '(?=[,\)])'
pop: true
- include: comment
- include: attribute_nested
- include: attribute_string
- include: attribute_nested
- include: attribute_string
attribute_nested:
- match: '[ \t]*(?:([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+)[ \t]*)?(\()'
captures:
1: variable.other
2: punctuation.attribute-elements.begin
push:
- match: \)
captures:
0: punctuation.attribute-elements.end
pop: true
- include: comment
- include: punctuation_comma
- include: attribute_element
attribute_string:
- include: string
- match: '(?:[^\n,"''#\(\)/]|/(?!/)|(?<!#)#++(?!["'']))+'
scope: string.unquoted
- match: '[^,\)]+'
scope: invalid
declaration:
- include: attribute
- include: alias_postfix
- include: punctuation_ellipsis
- include: punctuation_colon
- include: punctuation_option
- include: punctuation_bind
- include: expression
expression:
- match: '(?<![\p{L}\p{Nd}_\$\#])(for)[ \t]+([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+|_)(?:[ \t]*(,)[ \t]*([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+|_))?[ \t]+(in)(?![\p{L}\p{Nd}_\$\#])(?:[ \t]+(else|otherwise|fallback|try)(?![\p{L}\p{Nd}_\$\#]))?'
captures:
1: keyword.control.for
2: variable.other
3: punctuation.separator
4: variable.other
5: keyword.control.in
6: variable.other
- match: '(?<![\p{L}\p{Nd}_\$\#])(if)(?![\p{L}\p{Nd}_\$\#])(?:[ \t]+(else|otherwise|fallback|try)(?![\p{L}\p{Nd}_\$\#]))?'
captures:
1: keyword.control.conditional
2: variable.other
- match: '(?<![\p{L}\p{Nd}_\$\#])(try)[ \t]+([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+|_)[ \t]*(=)(?![=~])'
captures:
1: keyword.control.try
2: variable.other
3: punctuation.bind
- match: '(?<![\p{L}\p{Nd}_\$\#])try(?=[ \t]*\{|[ \t]+(?:for|if|let|try)(?![\p{L}\p{Nd}_\$\#]))'
scope: keyword.control.try
- match: '(?<![\p{L}\p{Nd}_\$\#])(?:else|otherwise|fallback)(?=[ \t]*\{)'
scope: keyword.control.conditional
- match: '(?<![\p{L}\p{Nd}_\$\#])func(?=[ \t]*\()'
scope: keyword.function
- match: '(?<![\p{L}\p{Nd}_\$\#])(let)[ \t]+([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+)[ \t]*(=)(?![=])'
captures:
1: keyword.control.let
2: variable.other
3: punctuation.bind
- match: "->"
scope: keyword.operator.arrow
- match: '[\+\-\*]|/(?!/)'
scope: keyword.operator
- match: "=[=~]|![=~]|<=|>=|[<](?!=)|[>](?![=])"
scope: keyword.operator.comparison
- match: '&{2}|\|{2}|!(?![=~])'
scope: keyword.operator.logical
- match: '&(?!&)|\|(?!\|)'
scope: keyword.operator.set
- match: '(?<!\.)(\.)[ \t]*([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+)(?![\p{L}\p{Nd}_\$\#])'
captures:
1: punctuation.accessor
2: variable.other.member
- match: '(?<!\.)(\.)(?=[ \t]*(?:#*"|//|$))'
captures:
1: punctuation.accessor
- match: '(?<![\p{L}\p{Nd}_\$\#])_(?!\|)(?![\p{L}\p{Nd}_\$\#])'
scope: constant.language.top
- match: '(?<![\p{L}\p{Nd}_\$\#])_\|_(?![\p{L}\p{Nd}_\$\#])'
scope: constant.language.bottom
- match: '(?<![\p{L}\p{Nd}_\$\#])null(?![\p{L}\p{Nd}_\$\#])'
scope: constant.language.null
- match: '(?<![\p{L}\p{Nd}_\$\#])(?:true|false)(?![\p{L}\p{Nd}_\$\#])'
scope: constant.language.bool
- match: '(?<![\p{L}\p{Nd}_\.])[0-9](?:_?[0-9])*\.(?:[0-9](?:_?[0-9])*)?(?:[eE][\+\-]?[0-9](?:_?[0-9])*)?(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.float.decimal
- match: '(?<![\p{L}\p{Nd}_\.])[0-9](?:_?[0-9])*[eE][\+\-]?[0-9](?:_?[0-9])*(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.float.decimal
- match: '(?<![\p{L}\p{Nd}_\.])\.[0-9](?:_?[0-9])*(?:[eE][\+\-]?[0-9](?:_?[0-9])*)?(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.float.decimal
- match: '(?<![\p{L}\p{Nd}_\.])(?:0|[1-9](?:_?[0-9])*)(?:\.[0-9](?:_?[0-9])*)?(?:[KMGTP]i?)(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.integer.other
- match: '(?<![\p{L}\p{Nd}_\.])\.[0-9](?:_?[0-9])*(?:[KMGTP]i?)(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.integer.other
- match: '(?<![\p{L}\p{Nd}_\.])(?:0|[1-9](?:_?[0-9])*)(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.integer.decimal
- match: '(?<![\p{L}\p{Nd}_\.])0b_?[0-1](?:_?[0-1])*(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.integer.binary
- match: '(?<![\p{L}\p{Nd}_\.])0[xX]_?[0-9a-fA-F](?:_?[0-9a-fA-F])*(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.integer.hexadecimal
- match: '(?<![\p{L}\p{Nd}_\.])0o_?[0-7](?:_?[0-7])*(?![\p{L}\p{Nd}_]|\.(?!\.\.))'
scope: constant.numeric.integer.octal
- include: string
- match: '(?<![\p{L}\p{Nd}_\$\#])(?:bool|u?int(?:8|16|32|64|128)?|float(?:32|64)?|string|bytes|number|rune)(?![\p{L}\p{Nd}_\$\#])'
scope: support.type
- match: '(?<![\p{L}\p{Nd}_\$\#])(?:__)?self(?![\p{L}\p{Nd}_\$\#])(?![ \t]*+(?:[?!][ \t]*+)?:|[ \t]*+~|[ \t]*+=(?![=~]))'
scope: variable.language.self
- match: '(?<![\p{L}\p{Nd}_\$\#])(len|close|and|or|div|mod|quo|rem|matchN|matchIf|error|validator)(\()'
captures:
1: support.function
2: punctuation.section.parens.begin
push:
- meta_scope: meta.function-call
- match: \)
captures:
0: punctuation.section.parens.end
pop: true
- include: whitespace
- include: comment
- include: punctuation_comma
- include: attribute
- include: alias_postfix
- include: punctuation_colon
- include: punctuation_option
- include: punctuation_bind
- include: punctuation_ellipsis
- include: expression
- include: invalid_in_parens
- match: '(?<![\p{L}\p{Nd}_\$\#])([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+)(\.)(\p{Lu}[\p{L}\p{Nd}_\$\#]*)(\()'
captures:
1: support.module
2: punctuation
3: support.function
4: punctuation.section.parens.begin
push:
- meta_scope: meta.function-call
- match: \)
captures:
0: punctuation.section.parens.end
pop: true
- include: whitespace
- include: comment
- include: punctuation_comma
- include: attribute
- include: alias_postfix
- include: punctuation_colon
- include: punctuation_option
- include: punctuation_bind
- include: punctuation_ellipsis
- include: expression
- include: invalid_in_parens
- match: '(?<![\p{L}\p{Nd}_\$\#])(?:[\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+)(?![\p{L}\p{Nd}_\$\#])'
scope: variable.other
- match: '\{'
captures:
0: punctuation.definition.struct.begin
push:
- meta_scope: meta.struct
- match: '\}'
captures:
0: punctuation.definition.struct.end
pop: true
- include: invalid_slash
- include: whitespace
- include: comment
- include: punctuation_comma
- include: punctuation_ellipsis
- include: declaration
- include: invalid_in_braces
- match: '\['
captures:
0: punctuation.section.brackets.begin
push:
- meta_scope: meta.brackets
- match: '\]'
captures:
0: punctuation.section.brackets.end
pop: true
- include: whitespace
- include: comment
- include: punctuation_colon
- include: punctuation_comma
- include: punctuation_ellipsis
- include: punctuation_option
- match: '(?<![\p{L}\p{Nd}_\$\#])([\p{L}\$\#][\p{L}\p{Nd}_\$\#]*|_[\p{L}\p{Nd}_\$\#]+)[ \t]*(=)(?![=~])'
captures:
1: variable.other
2: punctuation.alias
- include: expression
- match: '[^\s\]]'
scope: invalid
- match: \(
captures:
0: punctuation.section.parens.begin
push:
- meta_scope: meta.parens
- match: \)
captures:
0: punctuation.section.parens.end
pop: true
- include: whitespace
- include: comment
- include: punctuation_comma
- include: attribute
- include: alias_postfix
- include: punctuation_colon
- include: punctuation_option
- include: punctuation_bind
- include: punctuation_ellipsis
- include: expression
- include: invalid_in_parens
invalid_in_braces:
- match: '[^\s\}]'
scope: invalid
invalid_in_parens:
- match: '[^\s\)]'
scope: invalid
invalid_slash:
- match: '(?<=^|[\{,:])[ \t]*+(/)(?!/)'
captures:
1: invalid
punctuation_bind:
- match: "(?<![=!><])=(?![=~])"
scope: punctuation.bind
punctuation_colon:
- match: (?<!:):(?!:)
scope: punctuation.colon
punctuation_comma:
- match: ","
scope: punctuation.separator
punctuation_ellipsis:
- match: '(?<!\.)\.{3}(?!\.)'
scope: punctuation.ellipsis
punctuation_option:
- match: \?
scope: punctuation.option
string:
- match: '####"""'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double-multiline
- match: '"""####'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\####(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\####(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\####\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\####$
scope: constant.character.escape
- match: \\####.
scope: invalid.illegal
- match: '####"'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double
- match: '"####'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\####(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\####(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\####\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\####.
scope: invalid.illegal
- match: "####'''"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single-multiline
- match: "'''####"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\####(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\####(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\####\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\####$
scope: constant.character.escape
- match: \\####.
scope: invalid.illegal
- match: "####'"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single
- match: "'####"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\####(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\####(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\####\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\####.
scope: invalid.illegal
- match: '###"""'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double-multiline
- match: '"""###'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\###(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\###(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\###\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\###$
scope: constant.character.escape
- match: \\###.
scope: invalid.illegal
- match: '###"'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double
- match: '"###'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\###(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\###(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\###\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\###.
scope: invalid.illegal
- match: "###'''"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single-multiline
- match: "'''###"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\###(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\###(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\###\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\###$
scope: constant.character.escape
- match: \\###.
scope: invalid.illegal
- match: "###'"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single
- match: "'###"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\###(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\###(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\###\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\###.
scope: invalid.illegal
- match: '##"""'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double-multiline
- match: '"""##'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\##(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\##(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\##\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\##$
scope: constant.character.escape
- match: \\##.
scope: invalid.illegal
- match: '##"'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double
- match: '"##'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\##(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\##(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\##\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\##.
scope: invalid.illegal
- match: "##'''"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single-multiline
- match: "'''##"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\##(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\##(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\##\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\##$
scope: constant.character.escape
- match: \\##.
scope: invalid.illegal
- match: "##'"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single
- match: "'##"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\##(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\##(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\##\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\##.
scope: invalid.illegal
- match: '#"""'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double-multiline
- match: '"""#'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\#(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\#(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\#\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\#$
scope: constant.character.escape
- match: \\#.
scope: invalid.illegal
- match: '#"'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double
- match: '"#'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\#(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\#(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\#\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\#.
scope: invalid.illegal
- match: "#'''"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single-multiline
- match: "'''#"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\#(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\#(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\#\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\#$
scope: constant.character.escape
- match: \\#.
scope: invalid.illegal
- match: "#'"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single
- match: "'#"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\#(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\#(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\#\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\#.
scope: invalid.illegal
- match: '"""'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double-multiline
- match: '"""'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\$
scope: constant.character.escape
- match: \\.
scope: invalid.illegal
- match: '"'
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.double
- match: '"'
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\(?:"|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: invalid.illegal
- match: \\\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\.
scope: invalid.illegal
- match: "'''"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single-multiline
- match: "'''"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\$
scope: constant.character.escape
- match: \\.
scope: invalid.illegal
- match: "'"
captures:
0: punctuation.definition.string.begin
push:
- meta_scope: meta.string
- meta_content_scope: string.quoted.single
- match: "'"
captures:
0: punctuation.definition.string.end
pop: true
- match: '\\(?:''|/|\\|[abfnrtv]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})'
scope: constant.character.escape
- match: '\\(?:[0-7]{3}|x[0-9A-Fa-f]{2})'
scope: constant.character.escape
- match: \\\(
captures:
0: punctuation.section.interpolation.begin
push:
- meta_scope: meta.interpolation
- meta_content_scope: source.cue.embedded
- match: \)
captures:
0: punctuation.section.interpolation.end
pop: true
- include: whitespace
- include: punctuation_option
- include: expression
- include: invalid_in_parens
- match: \\.
scope: invalid.illegal
whitespace:
- match: '[ \t\r\n]+'
+6 -2
View File
@@ -6,7 +6,7 @@ Should you find that a particular syntax is not available within `bat` and think
code. As a basis, syntect uses [Sublime Text](https://www.sublimetext.com/) syntax definitions
in the `.sublime-syntax` format.
**Important:** Before proceeding, verify that the syntax you wish to add meets the [criteria for inclusion](#Criteria-for-inclusion-of-new-syntaxes).
**Important:** Before proceeding, verify that the syntax you wish to add meets the [criteria for inclusion](#criteria-for-inclusion-of-new-syntaxes).
1. Find a Sublime Text syntax for the given language, preferably in a separate Git repository
which can be included as a submodule (under `assets/syntaxes`) using
@@ -28,7 +28,7 @@ in the `.sublime-syntax` format.
5. Use `bat --list-languages` to check if the new languages are available. You may want to do something like ``export PATH="`pwd`/target/debug:$PATH"`` to ensure the locally compiled version is the one being used.
6. Add a syntax test for the new language. See [below](#Syntax-tests) for details.
6. Add a syntax test for the new language. See [below](#syntax-tests) for details.
7. If you send a pull request with your changes, please do *not* include the changed `syntaxes.bin`
file. A new binary cache file will be created once before every new release of `bat`. This
@@ -103,3 +103,7 @@ The following files have been manually modified after converting from a `.tmLang
* `Cabal.sublime_syntax` has been added manually from
https://github.com/SublimeHaskell/SublimeHaskell/ - we don't want to include the whole submodule because it includes other syntaxes ("Haskell improved") as well.
* `Lean.sublime-syntax` has been added manually from https://github.com/leanprover/vscode-lean4/blob/master/vscode-lean4/syntaxes/lean4.json via conversion.
* `CUE.sublime-syntax` has been added manually from https://github.com/cue-lang/vscode-cue/blob/master/extension/syntaxes/cue.tmLanguage.json via conversion.
Sublime Text's converter rejects rules that only group other patterns (see https://github.com/cue-lang/cue/issues/3427), so they are inlined before converting:
`jq 'walk(if type == "object" and has("patterns") then .patterns |= [.[] | if type == "object" and keys == ["patterns"] then .patterns[] else . end] else . end)'`.
The converted syntax reproduces the token goldens in `extension/testdata/syntax` of that repository.
+25 -15
View File
@@ -1,4 +1,5 @@
use std::convert::TryInto;
use std::io::{self, Write};
use std::path::Path;
use syntect::highlighting::ThemeSet;
@@ -24,7 +25,7 @@ pub fn build(
let acknowledgements = build_acknowledgements(source_dir, include_acknowledgements)?;
print_unlinked_contexts(&syntax_set);
print_unlinked_contexts(&syntax_set)?;
write_assets(
&theme_set,
@@ -46,16 +47,18 @@ fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> Result
if theme_dir.exists() {
let res = theme_set.add_from_folder(&theme_dir);
if let Err(err) = res {
println!(
writeln!(
io::stdout(),
"Failed to load one or more themes from '{}' (reason: '{err}')",
theme_dir.to_string_lossy(),
);
)?;
}
} else {
println!(
writeln!(
io::stdout(),
"No themes were found in '{}', using the default set",
theme_dir.to_string_lossy()
);
)?;
}
theme_set.try_into()
@@ -78,23 +81,25 @@ fn build_syntax_set_builder(
if syntax_dir.exists() {
syntax_set_builder.add_from_folder(syntax_dir, true)?;
} else {
println!(
writeln!(
io::stdout(),
"No syntaxes were found in '{}', using the default set.",
syntax_dir.to_string_lossy()
);
)?;
}
Ok(syntax_set_builder)
}
fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
fn print_unlinked_contexts(syntax_set: &SyntaxSet) -> Result<()> {
let missing_contexts = syntax_set.find_unlinked_contexts();
if !missing_contexts.is_empty() {
println!("Some referenced contexts could not be found!");
writeln!(io::stdout(), "Some referenced contexts could not be found!")?;
for context in missing_contexts {
println!("- {context}");
writeln!(io::stdout(), "- {context}")?;
}
}
Ok(())
}
fn write_assets(
@@ -127,12 +132,13 @@ fn write_assets(
)?;
}
print!(
write!(
io::stdout(),
"Writing metadata to folder {} ... ",
target_dir.to_string_lossy()
);
)?;
crate::assets_metadata::AssetsMetadata::new(current_version).save_to_folder(target_dir)?;
println!("okay");
writeln!(io::stdout(), "okay")?;
Ok(())
}
@@ -161,10 +167,14 @@ fn asset_to_cache<T: serde::Serialize>(
description: &str,
compressed: bool,
) -> Result<()> {
print!("Writing {description} to {} ... ", path.to_string_lossy());
write!(
io::stdout(),
"Writing {description} to {} ... ",
path.to_string_lossy()
)?;
let contents = asset_to_contents(asset, description, compressed)?;
std::fs::write(path, &contents[..])
.map_err(|_| format!("Could not save {description} to {}", path.to_string_lossy()))?;
println!("okay");
writeln!(io::stdout(), "okay")?;
Ok(())
}
+22 -21
View File
@@ -65,12 +65,20 @@ pub struct App {
number_nonblank_from_cli: bool,
}
/// True if stdout is attached to an interactive terminal. On Windows this also
/// recognizes mintty/MSYS2 and Cygwin pseudo-terminals, which expose stdout as
/// a named pipe that `std::io::IsTerminal` does not report as a terminal.
/// Regular pipes (e.g. `bat file | grep`) are not affected.
fn is_interactive_output() -> bool {
std::io::stdout().is_terminal() || Term::stdout().features().is_msys_tty()
}
impl App {
pub fn new() -> Result<Self> {
#[cfg(windows)]
let _ = nu_ansi_term::enable_ansi_support();
let interactive_output = std::io::stdout().is_terminal();
let interactive_output = is_interactive_output();
// Parse the original command line separately from config and environment arguments.
// Using clap here keeps CLI-only detection consistent with the final parser, including
@@ -79,7 +87,7 @@ impl App {
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)?;
let matches = Self::matches(interactive_output, &cli_matches)?;
if matches.get_flag("help") {
let help_type = if wild::args_os().any(|arg| arg == "--help") {
@@ -169,7 +177,8 @@ impl App {
};
let cache_dir = PROJECT_DIRS.cache_dir();
let assets = assets_from_cache_or_binary(use_custom_assets, cache_dir)?;
let config_dir = PROJECT_DIRS.config_dir();
let assets = assets_from_cache_or_binary(use_custom_assets, config_dir, cache_dir)?;
Controller::new(&help_config, &assets)
.run(inputs, None)
.ok();
@@ -195,20 +204,13 @@ impl App {
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)
let should_skip_config = wild::args_os().any(|arg| {
matches!(
arg.to_str(),
Some("-V" | "--version" | "--diagnostic" | "--diagnostics")
)
});
fn matches(interactive_output: bool, cli_matches: &ArgMatches) -> Result<ArgMatches> {
let should_skip_config =
cli_matches.get_flag("no-config") || cli_matches.get_flag("diagnostic");
// Check if help was requested - help should read the config file but be
// forgiving of invalid arguments (so configured theme etc. can be used)
let help_requested =
wild::args_os().any(|arg| matches!(arg.to_str(), Some("-h" | "--help")));
let help_requested = cli_matches.get_flag("help");
if wild::args_os().nth(1) == Some("cache".into()) {
// Skip the config file and env vars
@@ -216,9 +218,9 @@ impl App {
return Ok(clap_app::build_app(interactive_output).get_matches_from(args));
}
if wild::args_os().any(|arg| arg == "--no-config") || should_skip_config {
if should_skip_config {
// Skip the arguments in bats config file when --no-config is present
// or when user requests version or diagnostic information
// or when user requests diagnostic information
let args = Self::build_args_without_config();
return Ok(clap_app::build_app(interactive_output).get_matches_from(args));
}
@@ -334,13 +336,12 @@ impl App {
// later args take precedence over earlier ones, hence `.rev()`
// see: https://github.com/sharkdp/bat/pull/2755#discussion_r1456416875
for from_to in values.rev() {
let parts: Vec<_> = from_to.split(':').collect();
if parts.len() != 2 {
// The glob can contain colons itself, e.g. after a drive letter on Windows.
let Some((from, to)) = from_to.rsplit_once(':') else {
return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is '<glob-pattern>:<syntax-name>'. For example: '*.cpp:C++'.".into());
}
};
syntax_mapping.insert(parts[0], MappingTarget::MapTo(parts[1]))?;
syntax_mapping.insert(from, MappingTarget::MapTo(to))?;
}
}
+61 -10
View File
@@ -1,5 +1,5 @@
use std::fs;
use std::io;
use std::io::{self, Write};
use std::path::Path;
use std::path::PathBuf;
@@ -9,16 +9,22 @@ use bat::assets::HighlightingAssets;
use bat::assets_metadata::AssetsMetadata;
use bat::error::*;
pub fn clear_assets(cache_dir: &Path) {
clear_asset(cache_dir.join("themes.bin"), "theme set cache");
clear_asset(cache_dir.join("syntaxes.bin"), "syntax set cache");
clear_asset(cache_dir.join("metadata.yaml"), "metadata file");
pub fn clear_assets(cache_dir: &Path) -> Result<()> {
clear_asset(cache_dir.join("themes.bin"), "theme set cache")?;
clear_asset(cache_dir.join("syntaxes.bin"), "syntax set cache")?;
clear_asset(cache_dir.join("metadata.yaml"), "metadata file")?;
Ok(())
}
pub fn assets_from_cache_or_binary(
use_custom_assets: bool,
config_dir: &Path,
cache_dir: &Path,
) -> Result<HighlightingAssets> {
if use_custom_assets {
ensure_cache_built(config_dir, cache_dir);
}
if let Some(metadata) = AssetsMetadata::load_from_folder(cache_dir)? {
if !metadata.is_compatible_with(crate_version!()) {
return Err(format!(
@@ -43,15 +49,60 @@ pub fn assets_from_cache_or_binary(
Ok(custom_assets.unwrap_or_else(HighlightingAssets::from_binary))
}
fn clear_asset(path: PathBuf, description: &str) {
print!("Clearing {description} ... ");
fn clear_asset(path: PathBuf, description: &str) -> Result<()> {
write!(io::stdout(), "Clearing {description} ... ")?;
match fs::remove_file(&path) {
Err(err) if err.kind() == io::ErrorKind::NotFound => {
println!("skipped (not present)");
writeln!(io::stdout(), "skipped (not present)")?;
}
Err(err) => {
println!("could not remove the cache file {path:?}: {err}");
writeln!(
io::stdout(),
"could not remove the cache file {path:?}: {err}"
)?;
}
Ok(_) => println!("okay"),
Ok(_) => writeln!(io::stdout(), "okay")?,
}
Ok(())
}
/// Build the cache on first run when custom themes or syntaxes are present
/// but no cache exists yet (see #4017). A failed build is not fatal: warn
/// and fall back to the integrated assets.
#[cfg(feature = "build-assets")]
fn ensure_cache_built(config_dir: &Path, cache_dir: &Path) {
if cache_exists(cache_dir) || !custom_sources_exist(config_dir) {
return;
}
eprintln!("bat: custom themes or syntaxes found, building cache ...");
if let Err(err) = bat::assets::build(config_dir, true, false, cache_dir, crate_version!()) {
eprintln!("bat: automatic cache build failed ({err}), falling back to integrated assets.");
}
}
#[cfg(not(feature = "build-assets"))]
fn ensure_cache_built(_config_dir: &Path, _cache_dir: &Path) {}
#[cfg(feature = "build-assets")]
fn cache_exists(cache_dir: &Path) -> bool {
cache_dir.join("themes.bin").is_file()
&& cache_dir.join("syntaxes.bin").is_file()
&& cache_dir.join("metadata.yaml").is_file()
}
#[cfg(feature = "build-assets")]
fn custom_sources_exist(config_dir: &Path) -> bool {
dir_has_files(&config_dir.join("themes")) || dir_has_files(&config_dir.join("syntaxes"))
}
#[cfg(feature = "build-assets")]
fn dir_has_files(dir: &Path) -> bool {
fs::read_dir(dir)
.map(|entries| {
entries
.filter_map(|entry| entry.ok())
.any(|entry| entry.path().is_file())
})
.unwrap_or(false)
}
+5
View File
@@ -65,6 +65,7 @@ pub fn build_app(interactive_output: bool) -> Command {
.arg(
Arg::new("nonprintable-notation")
.long("nonprintable-notation")
.overrides_with("nonprintable-notation")
.action(ArgAction::Set)
.default_value("unicode")
.value_parser(["unicode", "caret"])
@@ -81,6 +82,7 @@ pub fn build_app(interactive_output: bool) -> Command {
.arg(
Arg::new("binary")
.long("binary")
.overrides_with("binary")
.action(ArgAction::Set)
.default_value("no-printing")
.value_parser(["no-printing", "as-text"])
@@ -125,6 +127,7 @@ pub fn build_app(interactive_output: bool) -> Command {
.arg(
Arg::new("fallback-syntax")
.long("fallback-syntax")
.overrides_with("fallback-syntax")
.visible_alias("fallback-language")
.help("Set a fallback language for undetected syntaxes.")
.long_help(
@@ -317,6 +320,7 @@ pub fn build_app(interactive_output: bool) -> Command {
.arg(
Arg::new("italic-text")
.long("italic-text")
.overrides_with("italic-text")
.value_name("when")
.value_parser(["always", "never"])
.default_value("never")
@@ -484,6 +488,7 @@ pub fn build_app(interactive_output: bool) -> Command {
.arg(
Arg::new("squeeze-limit")
.long("squeeze-limit")
.overrides_with("squeeze-limit")
.value_parser(|s: &str| s.parse::<usize>().map_err(|_| "Requires a non-negative number".to_owned()))
.long_help("Set the maximum number of consecutive empty lines to be printed.")
.hide_short_help(true)
+15 -7
View File
@@ -32,12 +32,13 @@ pub fn config_file() -> PathBuf {
pub fn generate_config_file() -> bat::error::Result<()> {
let config_file = config_file();
if config_file.is_file() {
println!(
writeln!(
io::stdout(),
"A config file already exists at: {}",
config_file.to_string_lossy()
);
)?;
print!("Overwrite? (y/N): ");
write!(io::stdout(), "Overwrite? (y/N): ")?;
io::stdout().flush()?;
let mut decision = String::new();
io::stdin().read_line(&mut decision)?;
@@ -93,10 +94,11 @@ pub fn generate_config_file() -> bat::error::Result<()> {
)
})?;
println!(
writeln!(
io::stdout(),
"Success! Config file written to {}",
config_file.to_string_lossy()
);
)?;
Ok(())
}
@@ -108,7 +110,7 @@ pub fn get_args_from_config_file() -> Result<Vec<OsString>, shell_words::ParseEr
let user_config = config_file();
if let Ok(c) = fs::read_to_string(&system_config) {
config.push_str(&c);
config.push_str(strip_bom(&c));
config.push('\n');
}
@@ -116,7 +118,7 @@ pub fn get_args_from_config_file() -> Result<Vec<OsString>, shell_words::ParseEr
// which can happen when BAT_CONFIG_DIR is set to e.g. "/etc/bat". See #3589.
if !same_file(&system_config, &user_config) {
if let Ok(c) = fs::read_to_string(&user_config) {
config.push_str(&c);
config.push_str(strip_bom(&c));
}
}
@@ -134,6 +136,12 @@ pub fn get_args_from_env_opts_var() -> Option<Result<Vec<OsString>, shell_words:
env::var("BAT_OPTS").ok().map(|s| get_args_from_str(&s))
}
/// Editors on Windows (e.g. `Set-Content -Encoding UTF8` in Windows PowerShell)
/// may prepend a UTF-8 byte order mark, which would otherwise end up in the first argument.
fn strip_bom(content: &str) -> &str {
content.strip_prefix('\u{feff}').unwrap_or(content)
}
fn get_args_from_str(content: &str) -> Result<Vec<OsString>, shell_words::ParseError> {
let args_per_line = content
.split('\n')
+38 -26
View File
@@ -74,9 +74,9 @@ fn run_cache_subcommand(
#[cfg(feature = "build-assets")]
build_assets(matches, config_dir, cache_dir)?;
#[cfg(not(feature = "build-assets"))]
println!("bat has been built without the 'build-assets' feature. The 'cache --build' option is not available.");
writeln!(io::stdout(), "bat has been built without the 'build-assets' feature. The 'cache --build' option is not available.")?;
} else if matches.get_flag("clear") {
clear_assets(cache_dir);
clear_assets(cache_dir)?;
}
Ok(())
@@ -97,10 +97,10 @@ where
map
}
pub fn get_languages(config: &Config, cache_dir: &Path) -> Result<String> {
pub fn get_languages(config: &Config, config_dir: &Path, cache_dir: &Path) -> Result<String> {
let mut result: String = String::new();
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
let assets = assets_from_cache_or_binary(config.use_custom_assets, config_dir, cache_dir)?;
let mut languages = assets
.get_syntaxes()?
.iter()
@@ -205,7 +205,7 @@ pub fn list_themes(
cache_dir: &Path,
theme_options: ThemeOptions,
) -> Result<()> {
let assets = assets_from_cache_or_binary(cfg.use_custom_assets, cache_dir)?;
let assets = assets_from_cache_or_binary(cfg.use_custom_assets, config_dir, cache_dir)?;
let mut config = cfg.clone();
let mut style = HashSet::new();
style.insert(StyleComponent::Plain);
@@ -262,13 +262,17 @@ pub fn list_themes(
Ok(())
}
fn set_terminal_title_to(new_terminal_title: String) {
fn set_terminal_title_to(new_terminal_title: String) -> Result<()> {
let osc_command_for_setting_terminal_title = "\x1b]0;";
let osc_end_command = "\x07";
// Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC.
let safe_title = bat::sanitize_for_terminal(&new_terminal_title);
print!("{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}");
io::stdout().flush().unwrap();
write!(
io::stdout(),
"{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}"
)?;
io::stdout().flush()?;
Ok(())
}
fn get_new_terminal_title(inputs: &Vec<Input>) -> String {
@@ -282,17 +286,22 @@ fn get_new_terminal_title(inputs: &Vec<Input>) -> String {
new_terminal_title
}
fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Result<bool> {
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
fn run_controller(
inputs: Vec<Input>,
config: &Config,
config_dir: &Path,
cache_dir: &Path,
) -> Result<bool> {
let assets = assets_from_cache_or_binary(config.use_custom_assets, config_dir, cache_dir)?;
let controller = Controller::new(config, &assets);
if config.paging_mode != PagingMode::Never && config.set_terminal_title {
set_terminal_title_to(get_new_terminal_title(&inputs));
set_terminal_title_to(get_new_terminal_title(&inputs))?;
}
controller.run(inputs, None)
}
#[cfg(feature = "bugreport")]
fn invoke_bugreport(app: &App, cache_dir: &Path) {
fn invoke_bugreport(app: &App, cache_dir: &Path) -> Result<()> {
use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry};
struct ColorSchemeCollector;
@@ -329,6 +338,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
.info(CommandLine::default())
.info(EnvironmentVariables::list(&[
"BAT_CACHE_PATH",
"BAT_CONFIG_DIR",
"BAT_CONFIG_PATH",
"BAT_OPTS",
"BAT_PAGER",
@@ -370,7 +380,8 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
))
};
report.print::<Markdown>();
writeln!(io::stdout(), "{}", report.format::<Markdown>())?;
Ok(())
}
/// Returns `Err(..)` upon fatal errors. Otherwise, returns `Ok(true)` on full success and
@@ -382,21 +393,22 @@ fn run() -> Result<bool> {
if app.matches.get_flag("diagnostic") {
#[cfg(feature = "bugreport")]
invoke_bugreport(&app, cache_dir);
invoke_bugreport(&app, cache_dir)?;
#[cfg(not(feature = "bugreport"))]
println!("bat has been built without the 'bugreport' feature. The '--diagnostic' option is not available.");
writeln!(io::stdout(), "bat has been built without the 'bugreport' feature. The '--diagnostic' option is not available.")?;
return Ok(true);
}
#[cfg(feature = "application")]
if let Some(shell) = app.matches.get_one::<String>("completion") {
match shell.as_str() {
"bash" => println!("{}", completions::BASH_COMPLETION),
"fish" => println!("{}", completions::FISH_COMPLETION),
"ps1" => println!("{}", completions::PS1_COMPLETION),
"zsh" => println!("{}", completions::ZSH_COMPLETION),
let completion = match shell.as_str() {
"bash" => completions::BASH_COMPLETION,
"fish" => completions::FISH_COMPLETION,
"ps1" => completions::PS1_COMPLETION,
"zsh" => completions::ZSH_COMPLETION,
_ => unreachable!("No completion for shell '{shell}' available."),
}
};
writeln!(io::stdout(), "{completion}")?;
return Ok(true);
}
@@ -417,7 +429,7 @@ fn run() -> Result<bool> {
let inputs = vec![Input::ordinary_file("cache")];
let config = app.config(&inputs)?;
run_controller(inputs, &config, cache_dir)
run_controller(inputs, &config, config_dir, cache_dir)
}
}
_ => {
@@ -425,17 +437,17 @@ fn run() -> Result<bool> {
let config = app.config(&inputs)?;
if app.matches.get_flag("list-languages") {
let languages: String = get_languages(&config, cache_dir)?;
let languages: String = get_languages(&config, config_dir, cache_dir)?;
let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))];
let mut plain_config = config.clone();
plain_config.style_components =
StyleComponents::new(StyleComponent::Plain.components(false));
run_controller(inputs, &plain_config, cache_dir)
run_controller(inputs, &plain_config, config_dir, cache_dir)
} else if app.matches.get_flag("list-themes") {
list_themes(&config, config_dir, cache_dir, app.theme_options())?;
Ok(true)
} else if app.matches.get_flag("config-file") {
println!("{}", config_file().to_string_lossy());
writeln!(io::stdout(), "{}", config_file().to_string_lossy())?;
Ok(true)
} else if app.matches.get_flag("generate-config-file") {
generate_config_file()?;
@@ -450,7 +462,7 @@ fn run() -> Result<bool> {
writeln!(io::stdout(), "{}", bat::assets::get_acknowledgements())?;
Ok(true)
} else {
run_controller(inputs, &config, cache_dir)
run_controller(inputs, &config, config_dir, cache_dir)
}
}
}
+4 -4
View File
@@ -98,20 +98,20 @@ pub struct Config<'a> {
/// cached assets) are not available, assets from the binary will be used instead.
pub use_custom_assets: bool,
// Whether or not to use $LESSOPEN if set
/// Whether or not to use $LESSOPEN if set
#[cfg(feature = "lessopen")]
pub use_lessopen: bool,
// Whether or not to set terminal title when using a pager
/// Whether or not to set terminal title when using a pager
pub set_terminal_title: bool,
/// The maximum number of consecutive empty lines to display
pub squeeze_lines: Option<usize>,
// Whether or not to strip ANSI escape codes from the input
/// Whether or not to strip ANSI escape codes from the input
pub strip_ansi: StripAnsiMode,
// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi.
/// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi.
pub sanitize: StripAnsiMode,
/// Whether or not to produce no output when input is empty
+17 -3
View File
@@ -18,6 +18,8 @@ use std::io::{self, BufRead, Write};
use std::mem;
use clircle::{Clircle, Identifier};
use content_inspector::ContentType;
use encoding_rs::{UTF_16BE, UTF_16LE};
pub struct Controller<'a> {
config: &'a Config<'a>,
@@ -280,9 +282,21 @@ impl Controller<'_> {
// 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')
&& match reader.content_type {
Some(ContentType::UTF_16LE) => UTF_16LE
.decode_with_bom_removal(&current_line_buffer)
.0
.trim_end_matches(['\r', '\n'])
.is_empty(),
Some(ContentType::UTF_16BE) => UTF_16BE
.decode_with_bom_removal(&current_line_buffer)
.0
.trim_end_matches(['\r', '\n'])
.is_empty(),
_ => 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));
+13 -2
View File
@@ -53,6 +53,17 @@ impl From<String> for Error {
pub type Result<T> = std::result::Result<T, Error>;
/// Paint a `[bat error]`/`[bat warning]` style prefix, unless the user asked
/// for no colors by setting the `NO_COLOR` environment variable.
#[doc(hidden)]
pub fn paint_prefix(color: nu_ansi_term::Color, prefix: &str) -> String {
if std::env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty()) {
prefix.to_owned()
} else {
color.paint(prefix).to_string()
}
}
pub fn default_error_handler(error: &Error, output: &mut dyn Write) {
use nu_ansi_term::Color::Red;
@@ -64,7 +75,7 @@ pub fn default_error_handler(error: &Error, output: &mut dyn Write) {
writeln!(
output,
"{}: Error while parsing metadata.yaml file: {error}",
Red.paint("[bat error]"),
paint_prefix(Red, "[bat error]"),
)
.ok();
}
@@ -72,7 +83,7 @@ pub fn default_error_handler(error: &Error, output: &mut dyn Write) {
writeln!(
&mut std::io::stderr().lock(),
"{}: {error}",
Red.paint("[bat error]"),
paint_prefix(Red, "[bat error]"),
)
.ok();
}
+40 -1
View File
@@ -291,7 +291,12 @@ impl<'a> InputReader<'a> {
if content_type == Some(ContentType::UTF_16LE) {
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?;
} else if content_type == Some(ContentType::UTF_16BE) {
} else if content_type == Some(ContentType::UTF_16BE)
// In UTF-16BE a newline is encoded as 0x00 0x0A, so `read_until(b'\n')` above
// may already have read a complete first line. Only keep reading if it did not
// (e.g. the 0x0A byte was part of another character such as U+4E0A).
&& !(first_line.len() % 2 == 0 && first_line.ends_with(&[0x00, 0x0A]))
{
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?;
}
@@ -375,6 +380,7 @@ fn read_utf16_line<R: BufRead>(
}
buf.extend_from_slice(&temp);
if buf.len() >= 2
&& buf.len().is_multiple_of(2)
&& buf[buf.len() - 2] == preceded_by_char
&& buf[buf.len() - 1] == read_until_char
{
@@ -611,6 +617,39 @@ fn unbuffered_eof_handling() {
assert!(buffer.is_empty());
}
#[test]
fn utf16_newlines_are_code_unit_aligned() {
let cases: &[&[&[u8]]] = &[
&[b"\xFF\xFE\x85\x0A\x00\x25\x0A\x00", b"\x7A\x00"],
&[
b"\xFF\xFE\x73\x00\x0A\x00",
b"\x85\x0A\x00\x25\x0A\x00",
b"\x7A\x00",
],
&[
b"\xFE\xFF\x4E\x0A\x00\x0A",
b"\x25\x00\x0A\x85\x00\x0A",
b"\x00\x7A",
],
];
for &lines in cases {
let content = lines.concat();
let mut reader = InputReader::new(&content[..]);
assert_eq!(lines[0], &reader.first_line[..]);
for &line in lines {
let mut buffer = vec![];
assert!(reader.read_line(&mut buffer).unwrap());
assert_eq!(line, &buffer[..]);
}
let mut buffer = vec![];
assert!(!reader.read_line(&mut buffer).unwrap());
assert!(buffer.is_empty());
}
}
#[test]
fn utf16le_issue3367() {
let content = b"\xFF\xFE\x0A\x4E\x00\x4E\x0A\x4F\x00\x52\x0A\x00\
+3 -3
View File
@@ -355,13 +355,13 @@ fn test_parse_context_fail() {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RangeCheckResult {
// Within one of the given ranges
/// Within one of the given ranges
InRange,
// Before the first range or within two ranges
/// Before the first range or within two ranges
BeforeOrBetweenRanges,
// Line number is outside of all ranges and larger than the last range.
/// Line number is outside of all ranges and larger than the last range.
AfterLastRange,
}
+5 -2
View File
@@ -1,7 +1,10 @@
#[macro_export]
macro_rules! bat_warning {
($($arg:tt)*) => ({
use nu_ansi_term::Color::Yellow;
eprintln!("{}: {}", Yellow.paint("[bat warning]"), format!($($arg)*));
eprintln!(
"{}: {}",
$crate::error::paint_prefix(nu_ansi_term::Color::Yellow, "[bat warning]"),
format!($($arg)*)
);
})
}
+81 -1
View File
@@ -108,7 +108,7 @@ pub(crate) fn get_pager(config_pager: Option<&str>) -> Result<Option<Pager>, Par
_ => ("less", PagerSource::Default),
};
let parts = shell_words::split(cmd)?;
let parts = split_command(cmd)?;
match parts.split_first() {
Some((bin, args)) => {
let kind = PagerKind::from_bin(bin);
@@ -136,3 +136,83 @@ pub(crate) fn get_pager(config_pager: Option<&str>) -> Result<Option<Pager>, Par
None => Ok(None),
}
}
/// Splits a pager command into the program and its arguments.
fn split_command(cmd: &str) -> Result<Vec<String>, ParseError> {
if cfg!(windows) {
split_command_windows(cmd)
} else {
shell_words::split(cmd)
}
}
/// Like `shell_words::split`, but `\` is not an escape character, since it is
/// the path separator on Windows: `C:\tools\less.exe` must not become
/// `C:toolsless.exe`. Single and double quotes still group words.
fn split_command_windows(cmd: &str) -> Result<Vec<String>, ParseError> {
let mut words = Vec::new();
let mut word = String::new();
let mut in_word = false;
let mut quote = None;
for c in cmd.chars() {
match quote {
Some(q) if c == q => quote = None,
Some(_) => word.push(c),
None if c == '"' || c == '\'' => {
quote = Some(c);
in_word = true;
}
None if c.is_whitespace() => {
if in_word {
words.push(std::mem::take(&mut word));
in_word = false;
}
}
None => {
word.push(c);
in_word = true;
}
}
}
if quote.is_some() {
return Err(ParseError);
}
if in_word {
words.push(word);
}
Ok(words)
}
#[cfg(test)]
mod tests {
use super::split_command_windows;
#[test]
fn split_command_windows_keeps_backslashes() {
let cases: &[(&str, &[&str])] = &[
(r"C:\tools\less.exe", &[r"C:\tools\less.exe"]),
(r"C:\tools\less.exe -R", &[r"C:\tools\less.exe", "-R"]),
(
r#""C:\Program Files\Less\less.exe" -R"#,
&[r"C:\Program Files\Less\less.exe", "-R"],
),
(
r"'C:\Program Files\Less\less.exe'",
&[r"C:\Program Files\Less\less.exe"],
),
(r#"less --prompt="a b" """#, &["less", "--prompt=a b", ""]),
(" less -R ", &["less", "-R"]),
("", &[]),
];
for (cmd, expected) in cases {
assert_eq!(split_command_windows(cmd).unwrap(), *expected, "{cmd}");
}
}
#[test]
fn split_command_windows_rejects_unclosed_quote() {
assert!(split_command_windows(r#""C:\tools\less.exe"#).is_err());
}
}
+10
View File
@@ -302,11 +302,21 @@ pub fn strip_overstrike(line: &str, first_backspace: usize) -> String {
output
}
/// Specify when to strip ANSI escape sequences from the input.
///
/// Used for both the `--strip-ansi` and the `--sanitize` option, the latter
/// implying the former with the same value.
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub enum StripAnsiMode {
/// Keep escape sequences in the output
#[default]
Never,
/// Strip escape sequences from the output
Always,
/// Strip escape sequences unless the syntax highlighting language is plain
/// text, in which case they may be part of what the input wants to display
Auto,
}
+25 -15
View File
@@ -16,7 +16,7 @@ use content_inspector::ContentType;
use encoding_rs::{UTF_16BE, UTF_16LE};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::assets::{HighlightingAssets, SyntaxReferenceInSet};
use crate::config::Config;
@@ -50,6 +50,14 @@ fn char_width(c: char) -> usize {
c.width().unwrap_or(if c.is_control() { 2 } else { 0 })
}
fn grapheme_width(grapheme: &str) -> usize {
if grapheme.chars().any(char::is_control) {
grapheme.chars().map(char_width).sum()
} else {
grapheme.width()
}
}
const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
raw_sequence: "\x1B[4m",
parameters: "4",
@@ -720,13 +728,15 @@ impl Printer for InteractivePrinter<'_> {
let mut cursor_total: usize = 0;
let mut panel_wrap: Option<String> = None;
// Line highlighting
let highlight_this_line = self
.config
.highlighted_lines
.0
.check(line_number, max_buffered_line_number)
== RangeCheckResult::InRange;
// Line highlighting. It is done purely with escape sequences (background
// color or underline), so it must be skipped when colors are disabled.
let highlight_this_line = self.config.colored_output
&& self
.config
.highlighted_lines
.0
.check(line_number, max_buffered_line_number)
== RangeCheckResult::InRange;
if highlight_this_line && self.config.theme == "ansi" {
self.ansi_style.update(ANSI_UNDERLINE_ENABLE);
@@ -826,7 +836,7 @@ impl Printer for InteractivePrinter<'_> {
let text = self
.preprocess(text.trim_end_matches(['\r', '\n']), &mut cursor_total);
let mut max_width = cursor_max - cursor;
let mut max_width = cursor_max.saturating_sub(cursor);
// line buffer (avoid calling write! for every character)
let mut line_buf = String::with_capacity(max_width * 4);
@@ -839,13 +849,13 @@ impl Printer for InteractivePrinter<'_> {
// For word wrapping, track last whitespace position.
let mut last_ws_idx: Option<usize> = None;
for c in text.chars() {
for grapheme in text.graphemes(true) {
// calculate the displayed width for next character
let cw = char_width(c);
let cw = grapheme_width(grapheme);
current_width += cw;
// Track whitespace positions for word wrapping.
if word_wrap && c.is_whitespace() {
if word_wrap && grapheme.starts_with(char::is_whitespace) {
last_ws_idx = Some(line_buf.len());
}
@@ -917,7 +927,7 @@ impl Printer for InteractivePrinter<'_> {
// Word wrap: carry remainder to next line.
let remainder = line_buf[rs..].to_string();
let rem_width: usize =
remainder.chars().map(char_width).sum();
remainder.graphemes(true).map(grapheme_width).sum();
line_buf.clear();
line_buf.push_str(&remainder);
current_width = rem_width + cw;
@@ -928,7 +938,7 @@ impl Printer for InteractivePrinter<'_> {
last_ws_idx = None;
}
line_buf.push(c);
line_buf.push_str(grapheme);
}
// flush the buffer
@@ -965,7 +975,7 @@ impl Printer for InteractivePrinter<'_> {
write!(
handle,
"{}",
ansi_style.paint(" ".repeat(cursor_max - cursor))
ansi_style.paint(" ".repeat(cursor_max.saturating_sub(cursor)))
)?;
}
writeln!(handle)?;
+122 -2
View File
@@ -1,5 +1,5 @@
use std::{
path::Path,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -24,6 +24,19 @@ pub(crate) enum Case {
Insensitive,
}
/// On Windows, `PathAbs` and `fs::canonicalize` return verbatim paths (`\\?\C:\dir\file`,
/// `\\?\UNC\server\share\file`), which a pattern such as `C:/dir/*` would not match.
fn without_verbatim_prefix(path: &Path) -> Option<PathBuf> {
if !cfg!(windows) {
return None;
}
let path = path.to_str()?;
match path.strip_prefix(r"\\?\UNC\") {
Some(rest) => Some(format!(r"\\{rest}").into()),
None => path.strip_prefix(r"\\?\").map(PathBuf::from),
}
}
fn make_glob_matcher(from: &str, case: Case) -> Result<GlobMatcher> {
let matcher = GlobBuilder::new(from)
.case_insensitive(matches!(case, Case::Insensitive))
@@ -162,9 +175,14 @@ impl<'a> SyntaxMapping<'a> {
pub fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> {
// Try matching on the file name as-is.
let candidate = Candidate::new(&path);
let simplified_path = without_verbatim_prefix(path.as_ref());
let candidate_simplified = simplified_path.as_ref().map(Candidate::new);
let candidate_filename = path.as_ref().file_name().map(Candidate::new);
for (glob, syntax) in self.all_mappings() {
if glob.is_match_candidate(&candidate)
|| candidate_simplified
.as_ref()
.is_some_and(|simplified| glob.is_match_candidate(simplified))
|| candidate_filename
.as_ref()
.is_some_and(|filename| glob.is_match_candidate(filename))
@@ -176,7 +194,10 @@ impl<'a> SyntaxMapping<'a> {
let file_name = path.as_ref().file_name()?;
self.ignored_suffixes
.try_with_stripped_suffix(file_name, |stripped_file_name| {
Ok(self.get_syntax_for(stripped_file_name))
if Path::new(stripped_file_name).file_name().is_none() {
return Ok(self.get_syntax_for(stripped_file_name));
}
Ok(self.get_syntax_for(path.as_ref().with_file_name(stripped_file_name)))
})
.ok()?
}
@@ -246,6 +267,105 @@ mod tests {
);
}
#[cfg(windows)]
#[test]
fn custom_mappings_match_verbatim_paths() {
let mut map = SyntaxMapping::new();
map.insert("C:/dir/*.myconf", MappingTarget::MapTo("INI"))
.unwrap();
map.insert(r"\\server\share\*.myconf", MappingTarget::MapTo("TOML"))
.unwrap();
map.insert(r"\\?\C:\literal\*.myconf", MappingTarget::MapTo("JSON"))
.unwrap();
for (path, expected) in [
(r"C:\dir\file.myconf", Some(MappingTarget::MapTo("INI"))),
(r"\\?\C:\dir\file.myconf", Some(MappingTarget::MapTo("INI"))),
(r"\\?\D:\dir\file.myconf", None),
(
r"\\?\C:\literal\file.myconf",
Some(MappingTarget::MapTo("JSON")),
),
(
r"\\?\UNC\server\share\file.myconf",
Some(MappingTarget::MapTo("TOML")),
),
] {
assert_eq!(map.get_syntax_for(path), expected, "{path}");
}
}
#[test]
fn custom_mappings_with_ignored_suffix_preserve_paths() {
for (pattern, parent) in [
("/path/to/file.demo", "/path/to"),
("/path/to/*.demo", "/path/to"),
("path/to/*.demo", "path/to"),
] {
let mut map = SyntaxMapping::new();
map.insert(pattern, MappingTarget::MapTo("JSON")).unwrap();
map.insert_ignored_suffix(".custom");
for suffix in ["", ".bak", ".bak.old", ".custom.custom"] {
let path = Path::new(parent).join(format!("file.demo{suffix}"));
assert_eq!(
map.get_syntax_for(&path),
Some(MappingTarget::MapTo("JSON")),
"{pattern}: {}",
path.display()
);
}
assert_eq!(map.get_syntax_for("/path/elsewhere/file.demo.bak"), None);
}
}
#[test]
fn custom_mappings_with_ignored_suffix_preserve_precedence() {
for (first, second) in [("/path/to/*.demo", "*.demo"), ("*.demo", "/path/to/*.demo")] {
let mut map = SyntaxMapping::new();
map.insert(first, MappingTarget::MapTo("first")).unwrap();
map.insert(second, MappingTarget::MapTo("second")).unwrap();
assert_eq!(
map.get_syntax_for("/path/to/file.demo.bak"),
Some(MappingTarget::MapTo("first"))
);
map.insert("*.bak", MappingTarget::MapTo("backup")).unwrap();
assert_eq!(
map.get_syntax_for("/path/to/file.demo.bak"),
Some(MappingTarget::MapTo("backup"))
);
}
}
#[test]
fn custom_mappings_with_empty_stripped_filename() {
let mut map = SyntaxMapping::new();
map.insert("to", MappingTarget::MapTo("parent")).unwrap();
map.insert_ignored_suffix(".custom");
for name in [
"~", ".bak", ".custom", "~.bak", ".~", "..bak", "..custom", "...bak",
] {
assert_eq!(map.get_syntax_for(Path::new("/path/to").join(name)), None);
}
map.insert("", MappingTarget::MapTo("empty")).unwrap();
assert_eq!(
map.get_syntax_for("/path/to/~.bak"),
Some(MappingTarget::MapTo("empty"))
);
for (pattern, name) in [(".", ".~"), (".", "..bak"), ("..", "...bak")] {
let mut map = SyntaxMapping::new();
map.insert(pattern, MappingTarget::MapTo("dot")).unwrap();
assert_eq!(
map.get_syntax_for(Path::new("/path/to").join(name)),
Some(MappingTarget::MapTo("dot"))
);
}
}
#[test]
fn custom_mappings_override_builtin() {
let mut map = SyntaxMapping::new();
@@ -0,0 +1,4 @@
# kubectl's config file has no extension. The format is YAML.
# https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/
[mappings]
"YAML" = ["**/.kube/config"]
@@ -1,5 +1,6 @@
# see `man quadlet`
[mappings]
"INI" = [
"*.quadlets",
"**/containers/systemd/**/*.{container,volume,network,kube,image,build,pod,artifact}",
]
+1 -1
View File
@@ -2,7 +2,7 @@
pub enum WrappingMode {
Character,
Word,
// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
/// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
NoWrapping(bool),
}
File diff suppressed because it is too large. Load diff
+184
View File
@@ -0,0 +1,184 @@
// Package example demonstrates CUE syntax.
package example
import (
 "strings"
 "list"
 yamlpkg "encoding/yaml"
)
// Definitions and constraints
#Port: int & >0 & <=65535
#Service: {
 name!: string & =~"^[a-z][a-z0-9-]*$"
 image: string
 replicas?: uint | *1
 ports: [...#Port]
 env: [string]: string
 labels: {
 app: name
 ...
 }
 _hidden: true
 #nested: bool
}
// Concrete values
services: [Name=string]: #Service & {name: Name}
services: frontend: {
 image: "nginx:1.25"
 replicas: 3
 ports: [80, 443]
 env: {
 LOG_LEVEL: "info"
 TIMEOUT: "30s"
 }
}
services: backend: {
 image: "registry.example.com/backend:\(version)"
 ports: [8080]
 env: DATABASE_URL: "postgres://db:5432/app"
}
version: "v1.2.3"
// Numbers
numbers: {
 decimal: 1_000_000
 float: 3.14159
 exponent: 6.022e23
 hex: 0xFF_FF
 octal: 0o755
 binary: 0b1010_1010
 multiplier: 1.5Gi
 kilo: 4K
}
// Strings and bytes
strs: {
 simple: "hello, world"
 escaped: "tab\tnewline\nunicode\u00e9"
 bytes: 'raw bytes \x00\xff'
 raw: #"no \(interpolation) here"#
 multiline: """
 multiple
 lines with \(version)
 """
 multiBytes: '''
 byte
 lines
 '''
}
// Builtin types and values
types: {
 n: null
 b: bool | *false
 i: int8 | int16 | int32 | int64
 f: float32 | float64
 s: string
 y: bytes
 r: rune
 t: _
 x: number
}
// Bottom (error) value
bottom: _|_ | 1
// Operators and builtins
ops: {
 sum: 1 + 2*3 - 4/2
 divided: div(7, 2)
 modulo: mod(7, 2)
 quotient: quo(-7, 2)
 rest: rem(-7, 2)
 eq: 1 == 1
 ne: "a" != "b"
 both: true && false
 either: true || false
 not: !true
 match: "abc" =~ "^a"
 nomatch: "abc" !~ "^z"
 length: len([1, 2, 3])
 closed: close({a: 1})
 upper: strings.ToUpper("cue")
 sorted: list.Sort([3, 1, 2], list.Ascending)
 disj: or([1, 2, 3])
 conj: and([int, >0])
 asYAML: yamlpkg.Marshal({a: 1})
}
// Nested calls, grouping and interpolated calls
calls: {
 allPorts: list.Concat([
 [22, 25],
 list.Concat([for _, s in services {s.ports}]),
 ])
 nested: len(strings.Split("a,b", ","))
 grouped: ((1 + 2) * 3)
 interp: "x-\(strings.ToUpper("y"))-\(len([1, 2]))"
}
// Comprehensions
let prefix = "svc-"
serviceNames: [for k, v in services if v.replicas > 1 {prefix + k}]
portMap: {
 for name, svc in services {
 for i, p in svc.ports {
 "\(name)-\(i)": p
 }
 }
}
byPort: {
 for name, svc in services
 if len(svc.ports) > 0
 let first = svc.ports[0] {
 "\(first)": name
 (name + "-image"): svc.image
 }
}
if version != "" {
 hasVersion: true
}
// Aliases, optional and required fields, pattern constraints
X=outer: {
 inner: 1
 ref: X.inner
}
optional?: string
required!: int
[=~"^x-"]: string
// Attributes
config: {
 port: int @tag(port, type=int)
 debug: bool @json(",omitempty") @yaml("debug")
}
// Embedding and default values
#Base: {
 kind: *"base" | "derived"
}
derived: #Base & {kind: "derived"}
#Limits: {
 cpu: {min: number, max: number} | *{min: 0, max: 1}
 tags: [...string] | *[]
}
// Lists
emptyList: []
openList: [1, 2, ...]
typedList: [...string]
nestedList: [[1, 2], [3, 4]]
@@ -0,0 +1,5 @@
{a
 /* comment */
}
{a /* comment */}
+184
View File
@@ -0,0 +1,184 @@
// Package example demonstrates CUE syntax.
package example
import (
"strings"
"list"
yamlpkg "encoding/yaml"
)
// Definitions and constraints
#Port: int & >0 & <=65535
#Service: {
name!: string & =~"^[a-z][a-z0-9-]*$"
image: string
replicas?: uint | *1
ports: [...#Port]
env: [string]: string
labels: {
app: name
...
}
_hidden: true
#nested: bool
}
// Concrete values
services: [Name=string]: #Service & {name: Name}
services: frontend: {
image: "nginx:1.25"
replicas: 3
ports: [80, 443]
env: {
LOG_LEVEL: "info"
TIMEOUT: "30s"
}
}
services: backend: {
image: "registry.example.com/backend:\(version)"
ports: [8080]
env: DATABASE_URL: "postgres://db:5432/app"
}
version: "v1.2.3"
// Numbers
numbers: {
decimal: 1_000_000
float: 3.14159
exponent: 6.022e23
hex: 0xFF_FF
octal: 0o755
binary: 0b1010_1010
multiplier: 1.5Gi
kilo: 4K
}
// Strings and bytes
strs: {
simple: "hello, world"
escaped: "tab\tnewline\nunicode\u00e9"
bytes: 'raw bytes \x00\xff'
raw: #"no \(interpolation) here"#
multiline: """
multiple
lines with \(version)
"""
multiBytes: '''
byte
lines
'''
}
// Builtin types and values
types: {
n: null
b: bool | *false
i: int8 | int16 | int32 | int64
f: float32 | float64
s: string
y: bytes
r: rune
t: _
x: number
}
// Bottom (error) value
bottom: _|_ | 1
// Operators and builtins
ops: {
sum: 1 + 2*3 - 4/2
divided: div(7, 2)
modulo: mod(7, 2)
quotient: quo(-7, 2)
rest: rem(-7, 2)
eq: 1 == 1
ne: "a" != "b"
both: true && false
either: true || false
not: !true
match: "abc" =~ "^a"
nomatch: "abc" !~ "^z"
length: len([1, 2, 3])
closed: close({a: 1})
upper: strings.ToUpper("cue")
sorted: list.Sort([3, 1, 2], list.Ascending)
disj: or([1, 2, 3])
conj: and([int, >0])
asYAML: yamlpkg.Marshal({a: 1})
}
// Nested calls, grouping and interpolated calls
calls: {
allPorts: list.Concat([
[22, 25],
list.Concat([for _, s in services {s.ports}]),
])
nested: len(strings.Split("a,b", ","))
grouped: ((1 + 2) * 3)
interp: "x-\(strings.ToUpper("y"))-\(len([1, 2]))"
}
// Comprehensions
let prefix = "svc-"
serviceNames: [for k, v in services if v.replicas > 1 {prefix + k}]
portMap: {
for name, svc in services {
for i, p in svc.ports {
"\(name)-\(i)": p
}
}
}
byPort: {
for name, svc in services
if len(svc.ports) > 0
let first = svc.ports[0] {
"\(first)": name
(name + "-image"): svc.image
}
}
if version != "" {
hasVersion: true
}
// Aliases, optional and required fields, pattern constraints
X=outer: {
inner: 1
ref: X.inner
}
optional?: string
required!: int
[=~"^x-"]: string
// Attributes
config: {
port: int @tag(port, type=int)
debug: bool @json(",omitempty") @yaml("debug")
}
// Embedding and default values
#Base: {
kind: *"base" | "derived"
}
derived: #Base & {kind: "derived"}
#Limits: {
cpu: {min: number, max: number} | *{min: 0, max: 1}
tags: [...string] | *[]
}
// Lists
emptyList: []
openList: [1, 2, ...]
typedList: [...string]
nestedList: [[1, 2], [3, 4]]
@@ -0,0 +1,5 @@
{a
/* comment */
}
{a /* comment */}