1
0
mirror of https://github.com/sharkdp/bat synced 2026-08-05 19:11:42 +00:00

Compare commits

...

184 Commits

Author SHA1 Message Date
auto-merge-dependabot-prs[bot] 2ba8db9c14 Merge pull request #3874 from sharkdp/dependabot/cargo/minus-5.7.2
build(deps): bump minus from 5.7.1 to 5.7.2
2026-08-01 04:01:25 +00:00
dependabot[bot] d344844486 build(deps): bump minus from 5.7.1 to 5.7.2
Bumps [minus](https://github.com/AMythicDev/minus) from 5.7.1 to 5.7.2.
- [Changelog](https://github.com/AMythicDev/minus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AMythicDev/minus/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: minus
  dependency-version: 5.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 03:50:26 +00:00
auto-merge-dependabot-prs[bot] aeb0e4c457 Merge pull request #3869 from sharkdp/dependabot/cargo/gix-0.86.0
build(deps): bump gix from 0.85.0 to 0.86.0
2026-08-01 03:47:33 +00:00
dependabot[bot] 1cf12d49bd build(deps): bump gix from 0.85.0 to 0.86.0
Bumps [gix](https://github.com/GitoxideLabs/gitoxide) from 0.85.0 to 0.86.0.
- [Release notes](https://github.com/GitoxideLabs/gitoxide/releases)
- [Changelog](https://github.com/GitoxideLabs/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/GitoxideLabs/gitoxide/compare/gix-v0.85.0...gix-v0.86.0)

---
updated-dependencies:
- dependency-name: gix
  dependency-version: 0.86.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 03:33:08 +00:00
auto-merge-dependabot-prs[bot] 7cb212646c Merge pull request #3871 from sharkdp/dependabot/cargo/terminal-colorsaurus-1.0.3
build(deps): bump terminal-colorsaurus from 1.0.0 to 1.0.3
2026-08-01 03:28:34 +00:00
dependabot[bot] 6573ca9307 build(deps): bump terminal-colorsaurus from 1.0.0 to 1.0.3
Bumps [terminal-colorsaurus](https://github.com/tautropfli/terminal-colorsaurus) from 1.0.0 to 1.0.3.
- [Release notes](https://github.com/tautropfli/terminal-colorsaurus/releases)
- [Changelog](https://github.com/tautropfli/terminal-colorsaurus/blob/main/changelog.md)
- [Commits](https://github.com/tautropfli/terminal-colorsaurus/compare/1.0.0...1.0.3)

---
updated-dependencies:
- dependency-name: terminal-colorsaurus
  dependency-version: 1.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 03:09:37 +00:00
auto-merge-dependabot-prs[bot] f04dc80bc7 Merge pull request #3873 from sharkdp/dependabot/cargo/execute-0.3.0
build(deps): bump execute from 0.2.15 to 0.3.0
2026-08-01 03:06:47 +00:00
dependabot[bot] e6ac6ce08d build(deps): bump execute from 0.2.15 to 0.3.0
Bumps [execute](https://github.com/magiclen/execute) from 0.2.15 to 0.3.0.
- [Commits](https://github.com/magiclen/execute/compare/v0.2.15...v0.3.0)

---
updated-dependencies:
- dependency-name: execute
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 02:38:14 +00:00
auto-merge-dependabot-prs[bot] 949f18e23a Merge pull request #3868 from sharkdp/dependabot/cargo/proc-macro2-1.0.107
build(deps): bump proc-macro2 from 1.0.106 to 1.0.107
2026-08-01 02:19:43 +00:00
dependabot[bot] 969b33dad1 build(deps): bump proc-macro2 from 1.0.106 to 1.0.107
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.106 to 1.0.107.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.106...1.0.107)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-version: 1.0.107
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 02:03:54 +00:00
Keith Hall 73dc3258be Merge pull request #3862 from lenamonj/fix/sanitize-bidi-control-set
Fix --sanitize passing through the bidi control characters U+200E, U+200F and U+061C
2026-07-30 00:27:20 +03:00
lenamonj df38225653 Add CHANGELOG entry for #3862 2026-07-29 17:02:33 -04:00
lenamonj c2cc79daa7 Fix --sanitize passing through the bidi control characters U+200E, U+200F and U+061C 2026-07-29 17:02:11 -04:00
Keith Hall 0230cba42d Merge pull request #3828 from cyphercodes/fix-list-languages-paging-never
Respect paging mode for --list-languages
2026-07-29 23:49:08 +03:00
Keith Hall 227e0fa00f Merge branch 'master' into fix-list-languages-paging-never 2026-07-29 23:33:51 +03:00
auto-merge-dependabot-prs[bot] 384f2f5d13 Merge pull request #3821 from sharkdp/dependabot/cargo/regex-1.12.4
build(deps): bump regex from 1.12.3 to 1.13.1
2026-07-29 20:23:23 +00:00
dependabot[bot] 060ccb1385 build(deps): bump regex from 1.12.3 to 1.13.1
Bumps [regex](https://github.com/rust-lang/regex) from 1.12.3 to 1.13.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.3...1.13.1)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 20:09:58 +00:00
auto-merge-dependabot-prs[bot] feabb25209 Merge pull request #3823 from sharkdp/dependabot/cargo/serde_with-3.21.0
build(deps): bump serde_with from 3.19.0 to 3.21.0
2026-07-29 20:06:40 +00:00
dependabot[bot] 839f2300b7 build(deps): bump serde_with from 3.19.0 to 3.21.0
Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.19.0 to 3.21.0.
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.19.0...v3.21.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 19:46:45 +00:00
auto-merge-dependabot-prs[bot] ea0742babc Merge pull request #3820 from sharkdp/dependabot/cargo/quote-1.0.46
build(deps): bump quote from 1.0.45 to 1.0.47
2026-07-29 19:40:36 +00:00
dependabot[bot] bfa6e59978 build(deps): bump quote from 1.0.45 to 1.0.47
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.45 to 1.0.47.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.45...1.0.47)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.46
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 19:17:50 +00:00
auto-merge-dependabot-prs[bot] 9710a231d8 Merge pull request #3819 from sharkdp/dependabot/cargo/bytesize-2.4.0
build(deps): bump bytesize from 2.3.1 to 2.4.2
2026-07-29 19:14:35 +00:00
dependabot[bot] a9b67b2d99 build(deps): bump bytesize from 2.3.1 to 2.4.2
Bumps [bytesize](https://github.com/bytesize-rs/bytesize) from 2.3.1 to 2.4.2.
- [Release notes](https://github.com/bytesize-rs/bytesize/releases)
- [Changelog](https://github.com/bytesize-rs/bytesize/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bytesize-rs/bytesize/compare/bytesize-v2.3.1...bytesize-v2.4.2)

---
updated-dependencies:
- dependency-name: bytesize
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 18:54:55 +00:00
auto-merge-dependabot-prs[bot] b22be63fec Merge pull request #3822 from sharkdp/dependabot/cargo/unicode-segmentation-1.13.3
build(deps): bump unicode-segmentation from 1.13.2 to 1.13.3
2026-07-29 18:50:14 +00:00
Keith Hall 3a7a750969 Merge branch 'master' into fix-list-languages-paging-never 2026-07-29 21:25:10 +03:00
dependabot[bot] be5343c742 build(deps): bump unicode-segmentation from 1.13.2 to 1.13.3
Bumps [unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation) from 1.13.2 to 1.13.3.
- [Commits](https://github.com/unicode-rs/unicode-segmentation/commits)

---
updated-dependencies:
- dependency-name: unicode-segmentation
  dependency-version: 1.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 18:21:51 +00:00
Keith Hall c371044276 Merge pull request #3861 from TyceHerrman/fix/cargo-audit-2026-07
chore(deps): resolve current cargo audit failures
2026-07-29 21:18:39 +03:00
Tyce Herrman d449ce5b7e chore(deps): resolve current cargo audit failures 2026-07-29 12:10:05 -04:00
cyphercodes 218afc30ac Respect paging mode for list languages 2026-07-04 06:31:26 +03:00
Keith Hall 78951393e2 Merge pull request #3729 from curious-rabbit/sanitize
add --sanitize option
2026-07-01 23:15:58 +03:00
curious-rabbit 0a52e4321f improve patch 2026-07-01 07:54:51 +02:00
curious-rabbit d72163160e add sanitize option 2026-07-01 07:09:49 +02:00
auto-merge-dependabot-prs[bot] 971f9679da Merge pull request #3817 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/LESS-d076a4d
build(deps): bump assets/syntaxes/02_Extra/LESS from `836b47e` to `d076a4d`
2026-07-01 02:16:46 +00:00
dependabot[bot] 57d436861a build(deps): bump assets/syntaxes/02_Extra/LESS
Bumps [assets/syntaxes/02_Extra/LESS](https://github.com/danro/LESS-sublime) from `836b47e` to `d076a4d`.
- [Release notes](https://github.com/danro/LESS-sublime/releases)
- [Commits](https://github.com/danro/LESS-sublime/compare/836b47ec61a9c6a6445b4007e8353337fe63e2c9...d076a4dd416bcdb155bacf90cda72f7bf1c8abbb)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/LESS
  dependency-version: d076a4dd416bcdb155bacf90cda72f7bf1c8abbb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 02:03:33 +00:00
Keith Hall bb4f48a124 Merge pull request #3816 from adnrivera/fix/ignored-suffix-firstline
Fix --ignored-suffix to fall back to first-line detection
2026-07-01 00:40:18 +03:00
Adrian Rivera c75cc01eef Fix --ignored-suffix to fall back to first-line detection
When an ignored suffix is also a registered extension (e.g. `.txt` maps to
"Plain Text"), `get_syntax_for_file_extension` matched the raw extension
before attempting the suffix strip. As a result `--ignored-suffix .txt` had
no effect on such files and first-line/shebang detection was never reached,
so a shell script saved as `*.txt` stayed unhighlighted.

Strip ignored suffixes first and detect on the remainder; only fall back to
the raw extension when no ignored suffix applies. When a suffix is stripped
but the remainder has no syntax, return `None` so the caller can use
first-line detection. Default behavior (no `--ignored-suffix`) is unchanged.

See #2745.
2026-06-30 12:20:14 -07:00
Keith Hall 835bd2a756 Merge pull request #3793 from Matei02355/fix-sml-definition-link
docs: fix SML syntax source link
2026-06-28 22:25:36 +03:00
Keith Hall 3409674d9c Merge branch 'master' into fix-sml-definition-link 2026-06-28 22:15:42 +03:00
Keith Hall 7de0c4ba47 Merge pull request #3809 from dhruvkb/patch-1
Include `.code-workspace` as a JSON extension
2026-06-27 22:21:18 +03:00
Keith Hall 20a818fe14 Merge branch 'master' into patch-1 2026-06-27 21:58:52 +03:00
Keith Hall 30b50a9eed Merge pull request #3814 from injust/dnf-repo
Add syntax mapping for DNF config
2026-06-27 21:57:37 +03:00
injust 088f1e4253 Add changelog entry 2026-06-27 12:50:29 -04:00
injust 53769d03b0 Add syntax mapping for DNF config 2026-06-27 12:50:28 -04:00
Keith Hall b7ae4585ad Merge pull request #3812 from greymoth-jp/fix/list-languages-narrow-width-underflow
fix: avoid usize underflow in --list-languages at narrow terminal width
2026-06-27 06:12:21 +03:00
Keith Hall a4609d1778 Merge branch 'master' into fix/list-languages-narrow-width-underflow 2026-06-27 06:03:16 +03:00
Keith Hall 8f70e0cf00 Merge pull request #3703 from blinxen/master
Replace libgit2 with gitoxide (#2)
2026-06-27 05:53:22 +03:00
blinxen e34595b53e Update gix to 0.85 2026-06-26 20:28:29 +02:00
blinxen 6ba745b74b cargo fmt 2026-06-26 20:27:49 +02:00
blinxen 98f09fd6f3 Add test with fauly git repo version 2026-06-26 20:27:49 +02:00
blinxen 69f40d9d8a Regenerate Cargo.lock after rebase 2026-06-26 20:27:49 +02:00
blinxen 6e6137ce10 Update to gix 0.83 2026-06-26 20:27:49 +02:00
blinxen df164cc2e3 fix immediate dereference of reference 2026-06-26 20:27:49 +02:00
blinxen f9eed8d619 cargo fmt 2026-06-26 20:27:49 +02:00
blinxen d695ad8f88 Add more tests for git diff 2026-06-26 20:27:49 +02:00
blinxen e60d184f97 Use histogram instead of myers for diffing 2026-06-26 20:27:49 +02:00
blinxen e2568c52cd Use correct PR number 2026-06-26 20:27:49 +02:00
blinxen c5e6f6aae3 Replace libgit2 with gitoxide 2026-06-26 20:27:49 +02:00
greymoth 8f93137246 doc(changelog): add #3812 bugfix entry 2026-06-26 22:13:51 +09:00
greymoth-jp fa00fb8ce8 fix(list-languages): clamp desired_width to avoid usize underflow at tiny --terminal-width
`get_languages` computed `desired_width = term_width - longest - separator.len()`.
When `--terminal-width` is smaller than the longest language name (e.g. 1), this
underflows `usize`: in overflow-checked builds it panics ("attempt to subtract with
overflow"), and in release it wraps to ~usize::MAX, silently disabling the extension
line-wrapping so `--terminal-width` is ignored for `--list-languages`.

Use `saturating_sub`, mirroring the fix applied to `print_snip` in #3804 (the snip
separator had the same `term_width - ...` underflow pattern). Output at normal widths
is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:24:01 +09:00
Dhruv Bhanushali d117bf2320 Summarise the change in the CHANGELOG.md file 2026-06-23 08:08:14 +04:00
Dhruv Bhanushali f4d836ee87 Include .code-workspace as a JSON extension 2026-06-23 08:01:50 +04:00
auto-merge-dependabot-prs[bot] 04576fd5a5 Merge pull request #3783 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/vscode-wgsl-a285c38
build(deps): bump assets/syntaxes/02_Extra/vscode-wgsl from `acf2671` to `a285c38`
2026-06-17 17:51:31 +00:00
dependabot[bot] e7025d9bdb build(deps): bump assets/syntaxes/02_Extra/vscode-wgsl
Bumps [assets/syntaxes/02_Extra/vscode-wgsl](https://github.com/PolyMeilex/vscode-wgsl) from `acf2671` to `a285c38`.
- [Release notes](https://github.com/PolyMeilex/vscode-wgsl/releases)
- [Commits](https://github.com/PolyMeilex/vscode-wgsl/compare/acf26718d7a327377641e31d8f9a9dab376efa84...a285c38f74eba2eb5c5a06be8d95b9f581338509)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/vscode-wgsl
  dependency-version: a285c38f74eba2eb5c5a06be8d95b9f581338509
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-17 17:38:53 +00:00
Keith Hall 6d4a3cef53 Merge pull request #3789 from CosmicHorrorDev/caddy
feat: add syntax highlighting for `Caddyfile`
2026-06-17 20:35:17 +03:00
Cosmic Horror 6b8887133d feat: add syntax highlighting for Caddyfile 2026-06-17 04:44:53 -06:00
Keith Hall 5bb35b4bed Merge pull request #3804 from leeewee/fix-snip-terminal-width-1-panic
Fix capacity-overflow panic in snip separator at --terminal-width 1
2026-06-16 20:19:08 +03:00
weili d28aa4a8b4 Fix capacity-overflow panic in print_snip at --terminal-width 1
`InteractivePrinter::print_snip` computes the snip separator width as
`term_width - panel_count - snip_left_count - title_count`. At `--terminal-width 1`
the panel is disabled and `title_count == 2`, so the subtraction underflows
`usize` (release has no overflow-checks, so it wraps to ~usize::MAX); `str::repeat`
then aborts with "capacity overflow" whenever a snip separator is emitted (two or
more disjoint line ranges, or a diff gap) — which the default style does.

Use `saturating_sub` for the repeat counts so they clamp to 0 instead of
underflowing. Added an integration test.
2026-06-16 07:06:29 +00:00
Matei6942 e9b552d5d0 docs: fix SML syntax source link 2026-06-05 17:40:39 +03:00
auto-merge-dependabot-prs[bot] af1f53d9a9 Merge pull request #3786 from sharkdp/dependabot/cargo/nix-0.31.3
build(deps): bump nix from 0.31.2 to 0.31.3
2026-06-01 05:15:08 +00:00
dependabot[bot] 8033b1e7be build(deps): bump nix from 0.31.2 to 0.31.3
Bumps [nix](https://github.com/nix-rust/nix) from 0.31.2 to 0.31.3.
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.31.2...v0.31.3)

---
updated-dependencies:
- dependency-name: nix
  dependency-version: 0.31.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 05:03:33 +00:00
auto-merge-dependabot-prs[bot] 01408ce094 Merge pull request #3785 from sharkdp/dependabot/cargo/predicates-3.1.4
build(deps): bump predicates from 3.1.3 to 3.1.4
2026-06-01 05:00:15 +00:00
dependabot[bot] bcd1d3f45b build(deps): bump predicates from 3.1.3 to 3.1.4
Bumps [predicates](https://github.com/assert-rs/predicates-rs) from 3.1.3 to 3.1.4.
- [Changelog](https://github.com/assert-rs/predicates-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/assert-rs/predicates-rs/compare/v3.1.3...v3.1.4)

---
updated-dependencies:
- dependency-name: predicates
  dependency-version: 3.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 04:48:18 +00:00
auto-merge-dependabot-prs[bot] 4cd5db2a78 Merge pull request #3787 from sharkdp/dependabot/cargo/indexmap-2.14.0
build(deps): bump indexmap from 2.13.0 to 2.14.0
2026-06-01 04:44:39 +00:00
dependabot[bot] 67aa491313 build(deps): bump indexmap from 2.13.0 to 2.14.0
Bumps [indexmap](https://github.com/indexmap-rs/indexmap) from 2.13.0 to 2.14.0.
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.13.0...2.14.0)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 04:28:21 +00:00
auto-merge-dependabot-prs[bot] e490c0f322 Merge pull request #3782 from sharkdp/dependabot/cargo/minus-5.7.1
build(deps): bump minus from 5.6.1 to 5.7.1
2026-06-01 04:24:38 +00:00
dependabot[bot] 2d29ecb939 build(deps): bump minus from 5.6.1 to 5.7.1
Bumps [minus](https://github.com/AMythicDev/minus) from 5.6.1 to 5.7.1.
- [Changelog](https://github.com/AMythicDev/minus/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AMythicDev/minus/compare/v5.6.1...v5.7.1)

---
updated-dependencies:
- dependency-name: minus
  dependency-version: 5.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 03:59:42 +00:00
auto-merge-dependabot-prs[bot] 707b5949fd Merge pull request #3781 from sharkdp/dependabot/cargo/plist-1.9.0
build(deps): bump plist from 1.7.0 to 1.9.0
2026-06-01 03:50:11 +00:00
dependabot[bot] fd53693e1f build(deps): bump plist from 1.7.0 to 1.9.0
Bumps [plist](https://github.com/ebarnard/rust-plist) from 1.7.0 to 1.9.0.
- [Release notes](https://github.com/ebarnard/rust-plist/releases)
- [Changelog](https://github.com/ebarnard/rust-plist/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ebarnard/rust-plist/compare/v1.7.0...v1.9.0)

---
updated-dependencies:
- dependency-name: plist
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 03:30:20 +00:00
Keith Hall f3d0773468 Merge pull request #3761 from mvanhorn/fix/3760-completion-skip-pager
fix(completions): force --no-paging on bat invocations in completion scripts (#3760)
2026-05-20 07:00:37 +03:00
Matt Van Horn 66e336bcfb fix(completions): force --no-paging on bat invocations in completion scripts (#3760)
Every shell completion script (bash, zsh, fish, PowerShell) shells out
to bat to enumerate languages/themes for tab completion candidates. If
the user has wired bat into LESSOPEN (e.g. LESSOPEN='|-bat -f -pp %s'),
bat's normal pager auto-detection can engage when stdout looks like a
terminal at completion time and reflect ANSI escape sequences back into
the candidate list. The result is the issue's reproducer: tab completion
expands 'Per' to '\033[38;2;248;248;242mPerl' instead of 'Perl'.

The list-languages/list-themes calls are always meant to be machine-
readable, so they should never page. Pass --no-paging explicitly to
every bat invocation inside the four completion files. The flag is the
public alias for --paging=never (already documented in bat --help) and
is the same form completion scripts elsewhere in the codebase use.

Touches the four completion files only; no production code changes.
2026-05-19 04:19:31 -07:00
Keith Hall ce7ad9adca Merge pull request #3758 from injust/patch-1
Include `.ssh/` subdirectories in SSH Config syntax mapping
2026-05-18 20:26:18 +03:00
Keith Hall f9fa590c3d Merge branch 'master' into patch-1 2026-05-18 20:11:48 +03:00
Keith Hall 1e1c3773fb Merge pull request #3759 from injust/patch-2
Add Ghostty syntax mapping
2026-05-18 20:10:26 +03:00
Justin Su 0ef0342ff2 Add changelog entry 2026-05-18 03:33:50 -04:00
Justin Su de396794cf Add Ghostty syntax mapping 2026-05-18 03:29:12 -04:00
Justin Su 43c4957e18 Add changelog entry 2026-05-18 03:08:39 -04:00
Justin Su c9fa10e76b Include .ssh/ subdirectories in SSH Config syntax mapping 2026-05-18 02:45:50 -04:00
Keith Hall 2cec8cdf25 Merge pull request #3737 from truffle-dev/fix/zsh-completion-language-only
fix: only offer language names in zsh tab completion for `-l`
2026-05-11 20:32:12 +03:00
truffle 138d70fd4c fix(zsh): drop redundant awk pipeline in language completion
`bat --list-languages` already emits each entry in `name:matchers`
form, which is the format `_describe` consumes directly. The previous
awk script split each line on `:` and re-emitted `$1:$2`, which is
byte-identical to the input.

Verified with `diff <(bat --list-languages) <(bat --list-languages |
awk -F: '{ printf("%s:%s\\n", $1, $2) }')` against the current
syntax set.
2026-05-10 14:10:54 +00:00
truffle 98df25434e fix: reference PR number in changelog entry 2026-05-10 09:20:40 +00:00
truffle 5f952066fb fix: only offer language names in zsh tab completion for -l
The previous awk script in `bat.zsh.in` split each line of
`bat --list-languages` on `:` or `,` and emitted every field as a
completion candidate, including the second column. That column lists
file matchers, which can be plain extensions (`rs`), globs (`*.rs`),
absolute paths (`/etc/profile`), or filenames (`Containerfile`). None
of those parse as `-l` arguments, so completing them produces
`unknown syntax` errors.

Switch to splitting on `:` only and emit the language name as the
completion value with the file-matcher list as its description, which
matches the bash completion's behavior.

Closes #3735.
2026-05-10 09:19:54 +00:00
Keith Hall f74082cee8 Merge pull request #3734 from kimjune01/fix/zsh-completion-force-plain
fix: pass --color=never --decorations=never in zsh completions
2026-05-09 13:52:47 +03:00
June Kim a0c95618c4 fix: pass --color=never --decorations=never in zsh completions
When BAT_OPTS contains --color=always or --decorations=always, the zsh
completion script's calls to --list-languages and --list-themes produce
ANSI escape codes that corrupt tab completion results.

Pass --color=never --decorations=never explicitly so completion output
is always plain text regardless of user config.

Fixes #3733
2026-05-09 03:35:54 -07:00
Keith Hall f57c1b6b1d Merge pull request #3726 from curious-rabbit/less
fix command injection in LESS template
2026-05-07 05:34:13 +03:00
Keith Hall 517de0463b Merge branch 'master' into less 2026-05-07 05:23:26 +03:00
curious-rabbit 3be4e30e00 improve patch 2026-05-06 10:58:02 +02:00
auto-merge-dependabot-prs[bot] 15fdc7637a Merge pull request #3717 from sharkdp/dependabot/cargo/terminal-colorsaurus-1.0.3
build(deps): bump terminal-colorsaurus from 1.0.1 to 1.0.3
2026-05-06 03:58:05 +00:00
dependabot[bot] 72fc697372 build(deps): bump terminal-colorsaurus from 1.0.1 to 1.0.3
Bumps [terminal-colorsaurus](https://github.com/tautropfli/terminal-colorsaurus) from 1.0.1 to 1.0.3.
- [Release notes](https://github.com/tautropfli/terminal-colorsaurus/releases)
- [Changelog](https://github.com/tautropfli/terminal-colorsaurus/blob/main/changelog.md)
- [Commits](https://github.com/tautropfli/terminal-colorsaurus/compare/1.0.1...1.0.3)

---
updated-dependencies:
- dependency-name: terminal-colorsaurus
  dependency-version: 1.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 03:45:27 +00:00
auto-merge-dependabot-prs[bot] b8d2781784 Merge pull request #3715 from sharkdp/dependabot/cargo/flate2-1.1.9
build(deps): bump flate2 from 1.1.2 to 1.1.9
2026-05-06 03:41:52 +00:00
dependabot[bot] 787a36acb6 build(deps): bump flate2 from 1.1.2 to 1.1.9
Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.1.2 to 1.1.9.
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.1.2...1.1.9)

---
updated-dependencies:
- dependency-name: flate2
  dependency-version: 1.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 03:29:10 +00:00
auto-merge-dependabot-prs[bot] a79701a43e Merge pull request #3712 from sharkdp/dependabot/cargo/regex-1.12.3
build(deps): bump regex from 1.12.2 to 1.12.3
2026-05-06 03:26:32 +00:00
dependabot[bot] 3bcbc45ab0 build(deps): bump regex from 1.12.2 to 1.12.3
Bumps [regex](https://github.com/rust-lang/regex) from 1.12.2 to 1.12.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.12.3)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 03:09:47 +00:00
auto-merge-dependabot-prs[bot] 7953df44ea Merge pull request #3711 from sharkdp/dependabot/cargo/clap-4.6.1
build(deps): bump clap from 4.5.60 to 4.6.1
2026-05-06 03:06:46 +00:00
dependabot[bot] 443bcb9287 build(deps): bump clap from 4.5.60 to 4.6.1
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.60 to 4.6.1.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.60...clap_complete-v4.6.1)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 02:44:23 +00:00
auto-merge-dependabot-prs[bot] 0523b70473 Merge pull request #3714 from sharkdp/dependabot/cargo/bugreport-0.6.0
build(deps): bump bugreport from 0.5.1 to 0.6.0
2026-05-06 02:41:47 +00:00
dependabot[bot] f776d1ad3f build(deps): bump bugreport from 0.5.1 to 0.6.0
Bumps [bugreport](https://github.com/sharkdp/bugreport) from 0.5.1 to 0.6.0.
- [Release notes](https://github.com/sharkdp/bugreport/releases)
- [Commits](https://github.com/sharkdp/bugreport/compare/v0.5.1...v0.6.0)

---
updated-dependencies:
- dependency-name: bugreport
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 02:33:22 +00:00
Keith Hall cfc6d9c415 Merge pull request #3728 from Daeraxa/fix-fedora-section
Fix README Fedora section links
2026-05-06 04:59:37 +03:00
Daeraxa 89d7c86b19 update fedora package link 2026-05-05 23:53:13 +01:00
curious-rabbit c437ad4d27 fix command injection in LESS template 2026-05-06 00:46:35 +02:00
Daeraxa 1c6e763d75 Update readme to fix Fedora section 2026-05-05 23:42:30 +01:00
Keith Hall baebdd1789 Merge pull request #3623 from zachvalenta/makefile-syntax-for-justfiles
Makefile syntax for justfiles
2026-05-02 14:04:55 +03:00
Keith Hall f7b84a3b41 Merge branch 'master' into makefile-syntax-for-justfiles 2026-05-02 13:56:45 +03:00
auto-merge-dependabot-prs[bot] b483f90612 Merge pull request #3716 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/typst-syntax-highlight-5f71d12
build(deps): bump assets/syntaxes/02_Extra/typst-syntax-highlight from `363f0e7` to `5f71d12`
2026-05-01 03:03:58 +00:00
dependabot[bot] 6edad56b00 build(deps): bump assets/syntaxes/02_Extra/typst-syntax-highlight
Bumps [assets/syntaxes/02_Extra/typst-syntax-highlight](https://github.com/hyrious/typst-syntax-highlight) from `363f0e7` to `5f71d12`.
- [Release notes](https://github.com/hyrious/typst-syntax-highlight/releases)
- [Commits](https://github.com/hyrious/typst-syntax-highlight/compare/363f0e767c938c615a14912c302db7936f025fc2...5f71d12fa129165bbe51aa867292555cdff6eb75)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/typst-syntax-highlight
  dependency-version: 5f71d12fa129165bbe51aa867292555cdff6eb75
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 02:43:12 +00:00
auto-merge-dependabot-prs[bot] e2174379ed Merge pull request #3713 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/PureScript-1773f4f
build(deps): bump assets/syntaxes/02_Extra/PureScript from `5acebc1` to `1773f4f`
2026-05-01 02:38:41 +00:00
dependabot[bot] 816aea0625 build(deps): bump assets/syntaxes/02_Extra/PureScript
Bumps [assets/syntaxes/02_Extra/PureScript](https://github.com/tellnobody1/sublime-purescript-syntax) from `5acebc1` to `1773f4f`.
- [Release notes](https://github.com/tellnobody1/sublime-purescript-syntax/releases)
- [Commits](https://github.com/tellnobody1/sublime-purescript-syntax/compare/5acebc18503697be09df047591964e68e80fcf8e...1773f4fddb08560d6bcb354901088e61e9ea0908)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/PureScript
  dependency-version: 1773f4fddb08560d6bcb354901088e61e9ea0908
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 02:11:34 +00:00
Keith Hall 7fcab0616a Merge pull request #3700 from optimistiCli/list-themes-term-probe-fix
Fixed hardcoded terminal probing when `--list-themes` is called
2026-04-30 18:09:32 +03:00
Keith Hall e157ac911b Merge branch 'master' into list-themes-term-probe-fix 2026-04-30 17:58:24 +03:00
Keith Hall b29e1d55c4 Merge pull request #3709 from mvanhorn/osc/3228-readme-style-list
docs(README): list available --style components in Output style section
2026-04-30 14:15:25 +03:00
Keith Hall 12ee94f530 Merge branch 'master' into list-themes-term-probe-fix 2026-04-30 14:13:47 +03:00
Matt Van Horn cbe7919451 docs(README): list available --style components in Output style section
The Output style section explained how to combine values into --style
without listing what those values are, so the recommended path was
either to read the man page or grep the source. Adds two tables: the
four pre-defined styles (default, full, auto, plain) and the eight
individual components (changes, header, header-filename, header-filesize,
grid, rule, numbers, snip), copying the descriptions from `bat --help`
verbatim. Also calls out the default-enabled set explicitly so the
"by default..." sentence in the existing tip block has a referent.

Closes #3228
2026-04-30 01:54:42 -07:00
David Peter 10b4f07a87 Remove sponsors 2026-04-30 10:18:58 +02:00
David Peter ebf469b99a Update README 2026-04-30 10:16:33 +02:00
Keith Hall 8fbc96a5f2 Merge pull request #3654 from cuiweixie/fix/lessclose-nonzero-warning
fix: correct $LESSCLOSE exit-status warning
2026-04-30 08:19:42 +03:00
Keith Hall 54bfc33248 Merge branch 'master' into fix/lessclose-nonzero-warning 2026-04-30 08:10:40 +03:00
Keith Hall 91aac95db2 Merge pull request #3691 from curious-rabbit/master
sanitize filenames
2026-04-28 23:24:00 +03:00
zach valenta 5bf932d511 Merge branch 'master' into makefile-syntax-for-justfiles 2026-04-28 14:41:15 -04:00
Keith Hall 5977245a23 Merge branch 'master' into fix/lessclose-nonzero-warning 2026-04-28 19:24:00 +03:00
Keith Hall 3f98e6f10f Merge branch 'master' into master 2026-04-28 19:22:51 +03:00
auto-merge-dependabot-prs[bot] 78963986e0 Merge pull request #3666 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/Idris2-4d8eb35
build(deps): bump assets/syntaxes/02_Extra/Idris2 from `bbfe50e` to `4d8eb35`
2026-04-27 19:41:31 +00:00
dependabot[bot] de64d3a0eb build(deps): bump assets/syntaxes/02_Extra/Idris2
Bumps [assets/syntaxes/02_Extra/Idris2](https://github.com/buzden/sublime-syntax-idris2) from `bbfe50e` to `4d8eb35`.
- [Commits](https://github.com/buzden/sublime-syntax-idris2/compare/bbfe50e023e0edc74f5e0c003eb946528d49279f...4d8eb35a38254d422030e77b4933530008dd3c6e)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/Idris2
  dependency-version: 4d8eb35a38254d422030e77b4933530008dd3c6e
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 19:28:44 +00:00
Keith Hall 78cb14af05 Merge pull request #3699 from guille/master
Improve Kotlin syntax
2026-04-27 22:26:10 +03:00
Keith Hall 8c8ed90d6d Merge branch 'master' into master 2026-04-27 22:20:33 +03:00
guille 492c387ce7 CHANGELOG: reference PR not issue 2026-04-27 22:16:09 +03:00
guille bceb260e91 Improve Kotlin syntax 2026-04-27 22:16:09 +03:00
Keith Hall 8cadefb5cb Merge pull request #3706 from lawrence3699/fix/initial-read-errors
Propagate initial input read errors
2026-04-27 21:46:10 +03:00
lawrence3699 64567c4819 Propagate initial input read errors 2026-04-28 01:11:48 +10:00
Keith Hall de2f21562a Merge pull request #3704 from sharkdp/fix/zsh-completion-language-word-split
Fix/zsh completion language word split
2026-04-27 06:07:37 +03:00
Yoshi Tacke 5722311b2e docs(changelog): add bugfix entry for zsh completion fix 2026-04-27 05:55:48 +03:00
YoshKoz f39d63b85d fix(zsh): use newline-splitting for language completions to fix word-splitting on names with spaces
Language names like "HTML (Jinja2)" and "Apache Conf" contain spaces, which
caused $() command substitution to word-split them into garbage tokens. Use
${(f)"$(...)"}  (split on newlines only) to match the pattern already used
for theme completions on line 100.

Fixes #2897
2026-04-27 05:55:48 +03:00
Ish West 4144059e7a Fixed CHANGELOG.md entry format 2026-04-25 21:30:03 +02:00
Ish West 044d445adc Updated CHANGELOG.md 2026-04-25 21:17:02 +02:00
Ish West c3df0e6a88 Fixed a bug with hardcoded terminal probing when --list-themes is called 2026-04-25 17:37:36 +02:00
curious-rabbit 71c894e843 santize filenames 2026-04-20 14:25:49 +02:00
Keith Hall 93c52a4f88 Merge pull request #3692 from barry3406/fix/remove-crt-windows
windows: statically link the CRT to remove vcruntime dependency
2026-04-20 05:09:54 +03:00
Barry ea7fafca1e windows: statically link the CRT to remove vcruntime dependency
Add a .cargo/config.toml that sets target-feature=+crt-static for the
i686, x86_64, and aarch64 MSVC targets so bat.exe no longer depends on
the vcruntime DLL. Mirrors the fix applied to fd in sharkdp/fd#1891.

Closes #3634
2026-04-19 14:19:00 -07:00
Keith Hall e782ec125c Merge pull request #3647 from mvanhorn/osc/3513-tcl-shebang
feat: add tclsh, wish, and expect shebang detection for Tcl syntax
2026-04-19 05:52:52 +03:00
Matt Van Horn 0ecdeb28d4 test(Tcl): regenerate shebang golden files to match patched syntax
The Tcl.sublime-syntax.patch adds first_line_match so bat recognizes
tclsh/wish/expect shebang files as Tcl. With the patch applied, the
leading '#!/...' and '# comment' lines get tokenized as Tcl comments
and rendered in Monokai-Extended's comment color instead of default
foreground.

The goldens were generated against unpatched bat in the original
commit, so CI's 'Run tests with updated syntaxes and themes' job (which
runs assets/create.sh before the regression test) disagreed. Regenerate
against the patched build so the regression test passes.
2026-04-18 09:42:26 -07:00
Matt Van Horn 77ea750e66 test(Tcl): add shebang source files for regression test
CI reported FileNotFoundError for Tcl/{tclsh,expect,wish}_shebang in
tests/syntax-tests/source/Tcl/. The highlighted files existed but the
source files had been omitted, so create_highlighted_versions.py had
nothing to read from.

Source files match the shebang regression test inputs at
tests/examples/regression_tests/issue_3647_*.
2026-04-18 06:58:45 -07:00
Matt Van Horn cafad6b036 test: add highlighted outputs for Tcl shebang regression tests
Generate highlighted test outputs for tclsh, wish, and expect
shebang detection files to prevent regressions.
2026-04-18 06:39:51 -07:00
Matt Van Horn e070d105b5 test: add shebang regression tests and move changelog to Syntaxes section
Add extensionless regression test files for tclsh, wish, and expect
shebangs so syntect bumps don't silently break first-line detection.
Move changelog entry from Features to Syntaxes per reviewer suggestion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 06:38:44 -07:00
Matt Van Horn 1f89178fce fix: reference PR number in changelog entry 2026-04-18 06:38:27 -07:00
Matt Van Horn 52763e0205 feat: add tclsh, wish, and expect shebang detection for Tcl syntax
Add first_line_match to the Tcl syntax definition via a patch file,
enabling automatic Tcl highlighting for scripts with tclsh, wish,
or expect shebangs.

Fixes #3513
2026-04-18 06:38:18 -07:00
Keith Hall f822bd05ce Merge pull request #3689 from janlarres/python-uv
[Python] Support uv as script runner in shebang
2026-04-13 20:28:02 +03:00
Jan Larres e89c515e9a [Python] Support uv as script runner in shebang 2026-04-13 17:56:05 +12:00
Keith Hall a36b02d90f Merge pull request #3688 from orbisai0security/fix-fix-shell-injection-subprocess-call
fix: sanitize subprocess call in generate_snapshots.py
2026-04-12 16:04:47 +03:00
Kira Security Bot 6876a782da Apply code changes: @orbisai0security can you address code review comm... 2026-04-12 12:37:43 +00:00
orbisai0security db647c9813 fix: V-001 security vulnerability
Automated security fix generated by Orbis Security AI
2026-04-12 11:43:12 +00:00
Keith Hall 33045c3558 Merge pull request #3687 from officialasishkumar/support-bat-width-env
Support BAT_WIDTH for terminal width
2026-04-12 04:59:03 +03:00
Asish Kumar 01174b31f5 Update changelog entry for #3687
Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-12 01:13:08 +05:30
Asish Kumar a9137fab05 Support BAT_WIDTH as an alias for --terminal-width
Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-12 01:12:55 +05:30
Keith Hall a995764d23 Merge pull request #3686 from officialasishkumar/fix-zip-binary-detection
Detect ZIP archives as binary content
2026-04-10 20:30:27 +03:00
Asish Kumar b3aec318cf Update changelog entry for #3686 2026-04-10 14:37:00 +00:00
Asish Kumar 2459aa9404 Detect ZIP archives as binary content 2026-04-10 14:35:33 +00:00
Keith Hall 111aa2e10e Merge pull request #3682 from ltrzesniewski/slnx
Add .NET `slnx` extension
2026-04-08 05:20:55 +03:00
Lucas Trzesniewski c64b6761fe Add .NET slnx extension
This is the new XML-based format for solution files.
2026-04-07 23:25:40 +02:00
Keith Hall 11efacbe64 Merge pull request #3661 from sharkdp/diagnostic_themes
Update --diagnostic output with BAT_THEME_LIGHT/DARK env vars and detected theme type
2026-04-02 21:44:28 +03:00
Keith Hall 204ca18d71 Merge branch 'master' into diagnostic_themes 2026-04-02 21:36:24 +03:00
auto-merge-dependabot-prs[bot] a158f12106 Merge pull request #3669 from sharkdp/dependabot/cargo/quote-1.0.45
build(deps): bump quote from 1.0.40 to 1.0.45
2026-04-01 04:07:40 +00:00
dependabot[bot] 956f0aeb08 build(deps): bump quote from 1.0.40 to 1.0.45
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.40 to 1.0.45.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.40...1.0.45)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 03:54:15 +00:00
auto-merge-dependabot-prs[bot] 1a6073d2e8 Merge pull request #3668 from sharkdp/dependabot/cargo/tempfile-3.27.0
build(deps): bump tempfile from 3.23.0 to 3.27.0
2026-04-01 03:51:21 +00:00
dependabot[bot] 57e99e9bcd build(deps): bump tempfile from 3.23.0 to 3.27.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.23.0 to 3.27.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.23.0...v3.27.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 03:38:08 +00:00
auto-merge-dependabot-prs[bot] 68eaa96e06 Merge pull request #3664 from sharkdp/dependabot/cargo/toml-1.1.1spec-1.1.0
build(deps): bump toml from 0.9.8 to 1.1.1+spec-1.1.0
2026-04-01 03:34:35 +00:00
dependabot[bot] de9ca4ef7b build(deps): bump toml from 0.9.8 to 1.1.1+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.8 to 1.1.1+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.8...toml-v1.1.1)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.1+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 03:13:41 +00:00
auto-merge-dependabot-prs[bot] 7bb905436b Merge pull request #3667 from sharkdp/dependabot/cargo/unicode-width-0.2.2
build(deps): bump unicode-width from 0.2.1 to 0.2.2
2026-04-01 03:11:34 +00:00
dependabot[bot] f820ad1050 build(deps): bump unicode-width from 0.2.1 to 0.2.2
Bumps [unicode-width](https://github.com/unicode-rs/unicode-width) from 0.2.1 to 0.2.2.
- [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.2.1...v0.2.2)

---
updated-dependencies:
- dependency-name: unicode-width
  dependency-version: 0.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 02:41:02 +00:00
auto-merge-dependabot-prs[bot] 259e1266a8 Merge pull request #3665 from sharkdp/dependabot/cargo/unicode-segmentation-1.13.2
build(deps): bump unicode-segmentation from 1.12.0 to 1.13.2
2026-04-01 02:37:51 +00:00
dependabot[bot] 31b0a35719 build(deps): bump unicode-segmentation from 1.12.0 to 1.13.2
Bumps [unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation) from 1.12.0 to 1.13.2.
- [Commits](https://github.com/unicode-rs/unicode-segmentation/compare/v1.12.0...v1.13.2)

---
updated-dependencies:
- dependency-name: unicode-segmentation
  dependency-version: 1.13.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 02:03:59 +00:00
Keith Hall 3e4169b8c3 cargo fmt 2026-03-31 22:35:21 +03:00
Keith Hall ba223289ce Update --diagnostic output with detected terminal color scheme type 2026-03-31 22:23:10 +03:00
Keith Hall abab6533e4 Update --diagnostic output with BAT_THEME_LIGHT/DARK env vars 2026-03-31 22:22:31 +03:00
Weixie Cui 5c3b8040db fix: add changelog entry for LESSCLOSE warning fix 2026-03-26 11:11:19 +08:00
Weixie Cui 7ff1fb3278 fix: warn when $LESSCLOSE fails, not when it succeeds
The Drop cleanup for Preprocessed inverted the exit-status check, so bat
emitted a warning when LESSCLOSE exited successfully and stayed silent on
failure. Align the condition with the intended behavior.
2026-03-26 11:10:58 +08:00
zachvalenta 652d50e92e Makefile syntax for justfiles 2026-03-08 21:06:19 -04:00
64 changed files with 3355 additions and 1408 deletions
+11
View File
@@ -0,0 +1,11 @@
# On Windows MSVC, statically link the C runtime so that the resulting EXE does
# not depend on the vcruntime DLL.
#
# See: https://github.com/sharkdp/bat/issues/3634
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
+6 -3
View File
@@ -49,9 +49,6 @@
[submodule "assets/themes/zenburn"] [submodule "assets/themes/zenburn"]
path = assets/themes/zenburn path = assets/themes/zenburn
url = https://github.com/colinta/zenburn.git url = https://github.com/colinta/zenburn.git
[submodule "assets/syntaxes/Kotlin"]
path = assets/syntaxes/02_Extra/Kotlin
url = https://github.com/vkostyukov/kotlin-sublime-package
[submodule "assets/syntaxes/Elm"] [submodule "assets/syntaxes/Elm"]
path = assets/syntaxes/02_Extra/Elm path = assets/syntaxes/02_Extra/Elm
url = https://github.com/elm-community/SublimeElmLanguageSupport url = https://github.com/elm-community/SublimeElmLanguageSupport
@@ -281,3 +278,9 @@
[submodule "assets/syntaxes/02_Extra/COBOL"] [submodule "assets/syntaxes/02_Extra/COBOL"]
path = assets/syntaxes/02_Extra/COBOL path = assets/syntaxes/02_Extra/COBOL
url = https://github.com/adukhan99/sublime_cobol.git url = https://github.com/adukhan99/sublime_cobol.git
[submodule "assets/syntaxes/02_Extra/Kotlin"]
path = assets/syntaxes/02_Extra/Kotlin
url = https://github.com/guille/sublime-kotlin
[submodule "assets/syntaxes/02_Extra/Caddy"]
path = assets/syntaxes/02_Extra/Caddy
url = https://github.com/caddyserver/sublimetext.git
+30
View File
@@ -3,21 +3,39 @@
## Other ## Other
- 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 instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536
- Add .NET slnx extension, see #3682 (@ltrzesniewski)
## Features ## Features
- 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)
- Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn)
- Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355)
- Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey)
- Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar)
- Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula) - Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula)
- Added an initial `flake.nix` for a ready made development environment; see #3578 (@vorburger) - Added an initial `flake.nix` for a ready made development environment; see #3578 (@vorburger)
- Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX) - Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX)
- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk)
- Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) - Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir)
- Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger) - Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger)
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
## Bugfixes ## Bugfixes
- Fix `--list-languages` respecting `--paging=never`, see #3828 (@cyphercodes)
- Fix `--sanitize` passing through the bidi control characters U+200E, U+200F and U+061C, see #3862 (@lenamonj)
- `--strip-ansi`: also strip 8-bit C1 introducers (U+0090, U+0098, U+009B, U+009D, U+009E, U+009F) and DCS/SOS/PM/APC sequence bodies, which previously passed through. See #3729 (@curious-rabbit)
- Fix `--ignored-suffix` not falling back to first-line/shebang detection when the ignored suffix is also a registered extension (e.g. `--ignored-suffix .txt` on a shebang script), see #2745 and #3816 (@adnrivera)
- Fix `capacity overflow` panic when printing a snip separator at `--terminal-width=1` with multiple line ranges. Closes #3803, see #3804 (@leeewee)
- Pass `--no-paging` to `bat` invocations inside the bash / zsh / fish / PowerShell shell completion scripts so that shell-level pager wiring (e.g. `LESSOPEN='|-bat -f -pp %s'`) cannot inject ANSI escape sequences into the completion candidates. Closes #3760 (@mvanhorn)
- Quote filenames before substituting them into `$LESSOPEN` / `$LESSCLOSE` templates, preventing shell injection when a filename contains shell metacharacters, see #3726 (@curious-rabbit)
- Fix `--list-themes` unconditionally probing the terminal via OSC 10/11 even when `--theme` was set to an explicit value, see #3700 (regression introduced in bc42149a). (@optimistiCli)
- Fix inverted `$LESSCLOSE` warning so bat warns on nonzero exit, not on success. See #3654 (@cuiweixie)
- Sanitize control characters in filenames before displaying them in the file header, error messages, and the terminal title, preventing ANSI escape injection via crafted filenames. Closes #3054, see #3691 (@curious-rabbit)
- Report initial input read errors instead of treating them as empty input. Closes #3002, see #3706 (@lawrence3699)
- Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar)
- Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) - Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu)
- Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) - Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn)
- Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman) - Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman)
@@ -32,21 +50,33 @@
- Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) - Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl)
- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) - Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l)
- Fixed syntax tests path, see #3610 (@foxfromworld) - Fixed syntax tests path, see #3610 (@foxfromworld)
- Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz)
- Fix zsh tab completion offering invalid `-l` arguments (file globs, paths, hidden filenames) sourced from the second column of `--list-languages`. Closes #3735, see #3737 (@truffle-dev)
- Fix `usize` underflow in `--list-languages` when `--terminal-width` is smaller than the longest language name, see #3812 (@greymoth-jp)
## Other ## Other
- Use git version of cross. See #3533 (@OctopusET) - Use git version of cross. See #3533 (@OctopusET)
- Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) - Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX)
- Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall) - Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall)
- Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon) - Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon)
- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406)
- Replace `libgit2` with a pure Rust implementation of git called `gitoxide`, see PR #3703 (@blinxen)
## Syntaxes ## Syntaxes
- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn)
- Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake) - Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake)
- Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall) - Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall)
- Add syntax highlighting support for COBOL, see #3584 (@adukhan99) - Add syntax highlighting support for COBOL, see #3584 (@adukhan99)
- Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire) - Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire)
- Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp) - Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp)
- Map all ignore dotfiles to Git Ignore syntax #3636 (@victor-gp) - Map all ignore dotfiles to Git Ignore syntax #3636 (@victor-gp)
- Improved Kotlin syntax, see #3699 (@guille)
- Include subdirectories in SSH Config syntax mapping, see #3758 (@injust)
- Add Ghostty syntax mapping, see #3759 (@injust)
- 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)
## Themes ## Themes
Generated
+1591 -691
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -31,8 +31,7 @@ minimal-application = [
"regex-onig", "regex-onig",
"wild", "wild",
] ]
git = ["git2"] # Support indicating git modifications git = ["gix"] # Support indicating git modifications
vendored-libgit2 = ["git2/vendored-libgit2"]
paging = [ "shell-words", "grep-cli", "minus"] # Support applying a pager on the output paging = [ "shell-words", "grep-cli", "minus"] # Support applying a pager on the output
lessopen = ["execute"] # Support $LESSOPEN preprocessor lessopen = ["execute"] # Support $LESSOPEN preprocessor
build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"] build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"]
@@ -52,11 +51,11 @@ thiserror = "2.0"
wild = { version = "2.2", optional = true } wild = { version = "2.2", optional = true }
content_inspector = "0.2.4" content_inspector = "0.2.4"
shell-words = { version = "1.1.1", optional = true } shell-words = { version = "1.1.1", optional = true }
minus = { version = "5.6", optional = true, features = [ minus = { version = "5.7", optional = true, features = [
"dynamic_output", "dynamic_output",
"search", "search",
] } ] }
unicode-width = "0.2.1" unicode-width = "0.2.2"
globset = "0.4" globset = "0.4"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
@@ -64,22 +63,23 @@ serde_yaml = "0.9.28"
semver = "1.0" semver = "1.0"
path_abs = { version = "0.5", default-features = false } path_abs = { version = "0.5", default-features = false }
clircle = { version = "0.6.1", default-features = false } clircle = { version = "0.6.1", default-features = false }
bugreport = { version = "0.5.0", optional = true } bugreport = { version = "0.6.0", optional = true }
etcetera = { version = "0.11.0", optional = true } etcetera = { version = "0.11.0", optional = true }
grep-cli = { version = "0.1.12", optional = true } grep-cli = { version = "0.1.12", optional = true }
regex = { version = "1.12.2", optional = true } regex = { version = "1.12.3", optional = true }
walkdir = { version = "2.5", optional = true } walkdir = { version = "2.5", optional = true }
bytesize = { version = "2.3.1" } bytesize = { version = "2.3.1" }
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
execute = { version = "0.2.15", optional = true } execute = { version = "0.3.0", optional = true }
terminal-colorsaurus = "1.0" terminal-colorsaurus = "1.0"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.13.2"
itertools = "0.14.0" itertools = "0.14.0"
[dependencies.git2] [dependencies.gix]
version = "0.20" version = "0.86"
optional = true optional = true
default-features = false default-features = false
features = ["sha1", "blob-diff"]
[dependencies.syntect] [dependencies.syntect]
version = "5.3.0" version = "5.3.0"
@@ -87,20 +87,20 @@ default-features = false
features = ["parsing"] features = ["parsing"]
[dependencies.clap] [dependencies.clap]
version = "4.5.60" version = "4.6.1"
optional = true optional = true
features = ["wrap_help", "cargo"] features = ["wrap_help", "cargo"]
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
plist = "1.7.0" plist = "1.9.0"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.16" assert_cmd = "2.0.16"
expect-test = "1.5.0" expect-test = "1.5.0"
serial_test = { version = "2.0.0", default-features = false } serial_test = { version = "2.0.0", default-features = false }
predicates = "3.1.3" predicates = "3.1.4"
wait-timeout = "0.2.1" wait-timeout = "0.2.1"
tempfile = "3.23.0" tempfile = "3.27.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[target.'cfg(unix)'.dev-dependencies] [target.'cfg(unix)'.dev-dependencies]
@@ -108,22 +108,22 @@ nix = { version = "0.31", default-features = false, features = ["term"] }
[build-dependencies] [build-dependencies]
anyhow = "1.0.97" anyhow = "1.0.97"
indexmap = { version = "2.13.0", features = ["serde"] } indexmap = { version = "2.14.0", features = ["serde"] }
itertools = "0.14.0" itertools = "0.14.0"
once_cell = "1.20" once_cell = "1.20"
prettyplease = "0.2.37" prettyplease = "0.2.37"
proc-macro2 = "1.0.106" proc-macro2 = "1.0.106"
quote = "1.0.40" quote = "1.0.45"
regex = "1.12.2" regex = "1.12.3"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_with = { version = "3.17.0", default-features = false, features = ["macros"] } serde_with = { version = "3.17.0", default-features = false, features = ["macros"] }
syn = { version = "2.0.104", features = ["full"] } syn = { version = "2.0.104", features = ["full"] }
toml = { version = "0.9.8", features = ["preserve_order"] } toml = { version = "1.1.1", features = ["preserve_order"] }
walkdir = "2.5" walkdir = "2.5"
[build-dependencies.clap] [build-dependencies.clap]
version = "4.5.60" version = "4.6.1"
optional = true optional = true
features = ["wrap_help", "cargo"] features = ["wrap_help", "cargo"]
+29 -21
View File
@@ -19,20 +19,6 @@
[<a href="doc/README-ru.md">Русский</a>] [<a href="doc/README-ru.md">Русский</a>]
</p> </p>
### Sponsors
A special *thank you* goes to our biggest <a href="doc/sponsors.md">sponsors</a>:<br>
<p>
<a href="https://www.warp.dev/bat">
<img src="doc/sponsors/warp-logo.png" width="200" alt="Warp">
<br>
<strong>Warp, the intelligent terminal</strong>
<br>
<sub>Available on MacOS, Linux, Windows</sub>
</a>
</p>
### Syntax highlighting ### Syntax highlighting
`bat` supports syntax highlighting for a large number of programming and markup `bat` supports syntax highlighting for a large number of programming and markup
@@ -208,12 +194,6 @@ Note that the [Manpage syntax](assets/syntaxes/02_Extra/Manpage.sublime-syntax)
The [`prettybat`](https://github.com/eth-p/bat-extras/blob/master/doc/prettybat.md) script is a wrapper that will format code and print it with `bat`. The [`prettybat`](https://github.com/eth-p/bat-extras/blob/master/doc/prettybat.md) script is a wrapper that will format code and print it with `bat`.
#### `Warp`
<a href="https://app.warp.dev/drive/folder/-Bat-Warp-Pack-lxhe7HrEwgwpG17mvrFSz1">
<img src="doc/sponsors/warp-pack-header.png" alt="Warp">
</a>
#### Highlighting `--help` messages #### Highlighting `--help` messages
You can use `bat` to colorize help text: `$ cp --help | bat -plhelp` You can use `bat` to colorize help text: `$ cp --help | bat -plhelp`
@@ -327,7 +307,7 @@ pacman -S bat
### On Fedora ### On Fedora
You can install [the `bat` package](https://koji.fedoraproject.org/koji/packageinfo?packageID=27506) from the official [Fedora Modular](https://docs.fedoraproject.org/en-US/modularity/using-modules/) repository. You can install [the `bat` package](https://packages.fedoraproject.org/pkgs/rust-bat/bat/) from the official sources:
```bash ```bash
dnf install bat dnf install bat
@@ -527,6 +507,30 @@ and line numbers but no grid and no file header. Set the `BAT_STYLE` environment
variable to make these changes permanent or use `bat`'s variable to make these changes permanent or use `bat`'s
[configuration file](#configuration-file). [configuration file](#configuration-file).
By default, `bat` enables `changes`, `grid`, `header-filename`, `numbers`, and `snip`.
The available pre-defined styles are:
| Style | Description |
|-------|-------------|
| `default` | Enables the recommended style components listed above. |
| `full` | Enables all available components. |
| `auto` | Same as `default`, unless the output is piped. |
| `plain` | Disables all available components. |
The available individual components are:
| Component | Description |
|-----------|-------------|
| `changes` | Show Git modification markers. |
| `header` | Alias for `header-filename`. |
| `header-filename` | Show filenames before the content. |
| `header-filesize` | Show file sizes before the content. |
| `grid` | Vertical/horizontal lines to separate the side bar and header from the content. |
| `rule` | Horizontal lines to delimit files. |
| `numbers` | Show line numbers in the side bar. |
| `snip` | Draw separation lines between distinct line ranges. |
>[!tip] >[!tip]
> If you specify a default style in `bat`'s config file, you can change which components > If you specify a default style in `bat`'s config file, you can change which components
> are displayed during a single run of `bat` using the `--style` command-line argument. > are displayed during a single run of `bat` using the `--style` command-line argument.
@@ -839,6 +843,10 @@ If your version of `bat` supports the `--strip-ansi=auto` option, it can be used
before syntax highlighting. Alternatively, you may disable both syntax highlighting and wrapping by before syntax highlighting. Alternatively, you may disable both syntax highlighting and wrapping by
passing the `--color=never --wrap=never` options to `bat`. passing the `--color=never --wrap=never` options to `bat`.
For untrusted input, the `--sanitize=auto|always|never` option additionally replaces terminal-active
control bytes and Unicode bidi / zero-width formatting characters with the Unicode replacement
character. It implies `--strip-ansi` at the same value.
> [!NOTE] > [!NOTE]
> The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text. > The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text.
+8 -2
View File
@@ -14,12 +14,12 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
$ArrayPrint = @('unicode', 'caret') $ArrayPrint = @('unicode', 'caret')
function Get-MyThemes(){ function Get-MyThemes(){
$themes = {{PROJECT_EXECUTABLE}} --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object $themes = {{PROJECT_EXECUTABLE}} --no-paging --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object
return $themes return $themes
} }
function Get-MyLanguages(){ function Get-MyLanguages(){
$themes = {{PROJECT_EXECUTABLE}} --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object $themes = {{PROJECT_EXECUTABLE}} --no-paging --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object
return $themes return $themes
} }
@@ -100,6 +100,11 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)} ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
break break
} }
'*;--sanitize' {
$ArrayWhen |
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
break
}
'*;--strip-ansi' { '*;--strip-ansi' {
$ArrayWhen | $ArrayWhen |
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)} ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
@@ -158,6 +163,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
[CompletionResult]::new('--ignored-suffix' , 'ignored-suffix' , [CompletionResultType]::ParameterName, 'Ignore extension. For example: ''bat --ignored-suffix ".dev" my_file.json.dev'' will use JSON syntax, and ignore ''.dev''') [CompletionResult]::new('--ignored-suffix' , 'ignored-suffix' , [CompletionResultType]::ParameterName, 'Ignore extension. For example: ''bat --ignored-suffix ".dev" my_file.json.dev'' will use JSON syntax, and ignore ''.dev''')
[CompletionResult]::new('--squeeze-blank' , 'squeeze-blank' , [CompletionResultType]::ParameterName, 'Squeeze consecutive empty lines into a single empty line.') [CompletionResult]::new('--squeeze-blank' , 'squeeze-blank' , [CompletionResultType]::ParameterName, 'Squeeze consecutive empty lines into a single empty line.')
[CompletionResult]::new('--squeeze-limit' , 'squeeze-limit' , [CompletionResultType]::ParameterName, 'Set the maximum number of consecutive empty lines to be printed.') [CompletionResult]::new('--squeeze-limit' , 'squeeze-limit' , [CompletionResultType]::ParameterName, 'Set the maximum number of consecutive empty lines to be printed.')
[CompletionResult]::new('--sanitize' , 'sanitize' , [CompletionResultType]::ParameterName, 'Specify when to sanitize untrusted input for safe display. Implies --strip-ansi and also replaces terminal-active and bidi / zero-width bytes. (auto, always, *never*).')
[CompletionResult]::new('--strip-ansi' , 'strip-ansi' , [CompletionResultType]::ParameterName, 'Specify when to strip ANSI escape sequences from the input. The automatic mode will remove escape sequences unless the syntax highlighting language is plain text. (auto, always, *never*).') [CompletionResult]::new('--strip-ansi' , 'strip-ansi' , [CompletionResultType]::ParameterName, 'Specify when to strip ANSI escape sequences from the input. The automatic mode will remove escape sequences unless the syntax highlighting language is plain text. (auto, always, *never*).')
# [CompletionResult]::new('-p' , 'p' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').') # [CompletionResult]::new('-p' , 'p' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').')
[CompletionResult]::new('--plain' , 'plain' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').') [CompletionResult]::new('--plain' , 'plain' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').')
+8 -3
View File
@@ -80,7 +80,7 @@ _bat() {
-l | --language) -l | --language)
local IFS=$'\n' local IFS=$'\n'
COMPREPLY=($(compgen -W "$( COMPREPLY=($(compgen -W "$(
"$1" --list-languages | while IFS=: read -r lang _; do "$1" --no-paging --list-languages | while IFS=: read -r lang _; do
printf "%s\n" "$lang" printf "%s\n" "$lang"
done done
)" -- "$cur")) )" -- "$cur"))
@@ -132,6 +132,10 @@ _bat() {
COMPREPLY=($(compgen -W "auto never always" -- "$cur")) COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
return 0 return 0
;; ;;
--sanitize)
COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
return 0
;;
--completion) --completion)
COMPREPLY=($(compgen -W "bash fish zsh ps1" -- "$cur")) COMPREPLY=($(compgen -W "bash fish zsh ps1" -- "$cur"))
return 0 return 0
@@ -150,14 +154,14 @@ _bat() {
;; ;;
--theme) --theme)
local IFS=$'\n' local IFS=$'\n'
COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --list-themes)" -- "$cur")) COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --no-paging --list-themes)" -- "$cur"))
__bat_escape_completions __bat_escape_completions
return 0 return 0
;; ;;
--theme-dark | \ --theme-dark | \
--theme-light) --theme-light)
local IFS=$'\n' local IFS=$'\n'
COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur")) COMPREPLY=($(compgen -W "$("$1" --no-paging --list-themes)" -- "$cur"))
__bat_escape_completions __bat_escape_completions
return 0 return 0
;; ;;
@@ -221,6 +225,7 @@ _bat() {
--list-themes --list-themes
--squeeze-blank --squeeze-blank
--squeeze-limit --squeeze-limit
--sanitize
--strip-ansi --strip-ansi
--style --style
--line-range --line-range
+6 -5
View File
@@ -15,11 +15,11 @@ function __bat_complete_files -a token
end end
function __bat_complete_one_language -a comp function __bat_complete_one_language -a comp
command $bat --list-languages | string split -f1 : | string match -e "$comp" command $bat --no-paging --list-languages | string split -f1 : | string match -e "$comp"
end end
function __bat_complete_list_languages function __bat_complete_list_languages
for spec in (command $bat --list-languages) for spec in (command $bat --no-paging --list-languages)
set -l name (string split -f1 : $spec) set -l name (string split -f1 : $spec)
for ext in (string split -f2 : $spec | string split ,) for ext in (string split -f2 : $spec | string split ,)
test -n "$ext"; or continue test -n "$ext"; or continue
@@ -221,6 +221,7 @@ complete -c $bat -s s -l squeeze-blank -d "Squeeze consecutive empty lines into
complete -c $bat -l squeeze-limit -x -d "Set the maximum number of consecutive empty lines to be printed" -n __bat_no_excl_args complete -c $bat -l squeeze-limit -x -d "Set the maximum number of consecutive empty lines to be printed" -n __bat_no_excl_args
complete -c $bat -l strip-ansi -x -a "auto never always" -d "Specify when to strip ANSI escape sequences from the input" -n __bat_no_excl_args complete -c $bat -l strip-ansi -x -a "auto never always" -d "Specify when to strip ANSI escape sequences from the input" -n __bat_no_excl_args
complete -c $bat -l sanitize -x -a "auto never always" -d "Specify when to sanitize untrusted input for safe display" -n __bat_no_excl_args
complete -c $bat -s p -l plain -d "Disable decorations" -n __bat_no_excl_args complete -c $bat -s p -l plain -d "Disable decorations" -n __bat_no_excl_args
@@ -234,11 +235,11 @@ complete -c $bat -l tabs -x -a "$tabs_opts" -d "Set tab width" -n __bat_no_excl_
complete -c $bat -l terminal-width -x -d "Set terminal <width>, +<offset>, or -<offset>" -n __bat_no_excl_args complete -c $bat -l terminal-width -x -d "Set terminal <width>, +<offset>, or -<offset>" -n __bat_no_excl_args
complete -c $bat -l theme -x -a "$special_themes(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args complete -c $bat -l theme -x -a "$special_themes(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args
complete -c $bat -l theme-dark -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args complete -c $bat -l theme-dark -x -a "(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args
complete -c $bat -l theme-light -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args complete -c $bat -l theme-light -x -a "(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args
complete -c $bat -s u -l unbuffered -d "Enable unbuffered input reading for streaming use cases" -n __bat_no_excl_args complete -c $bat -s u -l unbuffered -d "Enable unbuffered input reading for streaming use cases" -n __bat_no_excl_args
+9 -3
View File
@@ -54,6 +54,7 @@ _{{PROJECT_EXECUTABLE}}_main() {
--squeeze-blank'[squeeze consecutive empty lines into a single empty line]' --squeeze-blank'[squeeze consecutive empty lines into a single empty line]'
--squeeze-limit='[set the maximum number of consecutive empty lines]:limit:' --squeeze-limit='[set the maximum number of consecutive empty lines]:limit:'
--strip-ansi='[specify when to strip ANSI escape sequences]:when:(auto never always)' --strip-ansi='[specify when to strip ANSI escape sequences]:when:(auto never always)'
--sanitize='[specify when to sanitize untrusted input for safe display]:when:(auto never always)'
--style='[comma-separated list of style elements to display]: : _values "style [default]" --style='[comma-separated list of style elements to display]: : _values "style [default]"
default auto full plain changes header header-filename header-filesize grid rule numbers snip' default auto full plain changes header header-filename header-filesize grid rule numbers snip'
\*{-r+,--line-range=}'[only print the specified line range]:start\:end' \*{-r+,--line-range=}'[only print the specified line range]:start\:end'
@@ -90,20 +91,25 @@ _{{PROJECT_EXECUTABLE}}_main() {
languages) languages)
local IFS=$'\n' local IFS=$'\n'
local -a languages local -a languages
languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') ) # `--list-languages` emits one `name:matchers` line per language,
# which `_describe` parses as `value:description`. Only the
# language name is offered as the completion value; the matchers
# show up as the menu description. See
# https://github.com/sharkdp/bat/issues/3735.
languages=( ${(f)"$({{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-languages)"} )
_describe 'language' languages && ret=0 _describe 'language' languages && ret=0
;; ;;
themes) themes)
local -a themes expl local -a themes expl
themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-themes)"} )
_wanted themes expl 'theme' compadd -a themes && ret=0 _wanted themes expl 'theme' compadd -a themes && ret=0
;; ;;
theme_preferences) theme_preferences)
local -a themes expl local -a themes expl
themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-themes)"} )
_wanted themes expl 'theme' compadd -a themes && ret=0 _wanted themes expl 'theme' compadd -a themes && ret=0
;; ;;
+9
View File
@@ -2,6 +2,15 @@ diff --git syntaxes/01_Packages/Python/Python.sublime-syntax syntaxes/01_Package
index 2acd86d8..86257f7b 100644 index 2acd86d8..86257f7b 100644
--- syntaxes/01_Packages/Python/Python.sublime-syntax --- syntaxes/01_Packages/Python/Python.sublime-syntax
+++ syntaxes/01_Packages/Python/Python.sublime-syntax +++ syntaxes/01_Packages/Python/Python.sublime-syntax
@@ -25,7 +31,7 @@ file_extensions:
- wscript
- bazel
- bzl
-first_line_match: ^#!\s*/.*\bpython(\d(\.\d)?)?\b
+first_line_match: ^#!\s*/.*\b(python(\d(\.\d)?)?|uv)\b
scope: source.python
variables:
@@ -988,10 +988,6 @@ contexts: @@ -988,10 +988,6 @@ contexts:
- match: \} - match: \}
scope: punctuation.section.mapping-or-set.end.python scope: punctuation.section.mapping-or-set.end.python
+12
View File
@@ -0,0 +1,12 @@
diff --git syntaxes/01_Packages/TCL/Tcl.sublime-syntax syntaxes/01_Packages/TCL/Tcl.sublime-syntax
index 1234567..abcdefg 100644
--- syntaxes/01_Packages/TCL/Tcl.sublime-syntax
+++ syntaxes/01_Packages/TCL/Tcl.sublime-syntax
@@ -3,6 +3,7 @@
# http://www.sublimetext.com/docs/3/syntax.html
name: Tcl
file_extensions:
- tcl
+first_line_match: ^\#!.*\b(tclsh|wish|expect)\b
scope: source.tcl
variables:
-398
View File
@@ -1,398 +0,0 @@
%YAML 1.2
---
# http://www.sublimetext.com/docs/3/syntax.html
name: Kotlin
file_extensions:
- kt
- kts
scope: source.Kotlin
contexts:
main:
- include: comments
- match: '^\s*(package)\b(?:\s*([^ ;$]+)\s*)?'
captures:
1: keyword.other.kotlin
2: entity.name.package.kotlin
- include: imports
- include: statements
classes:
- match: (?<!::)(?=\b(?:companion|class|object|interface)\b)
push:
- match: '(?=$|\})'
pop: true
- include: comments
- match: \b(companion\s*)?(class|object|interface)\b
captures:
1: storage.modifier.kotlin
2: storage.modifier.kotlin
push:
- match: '(?=<|\{|\(|:|$)'
pop: true
- include: comments
- match: \w+
scope: entity.name.type.class.kotlin
- match: <
push:
- match: ">"
pop: true
- include: generics
- match: \(
push:
- match: \)
pop: true
- include: parameters
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: "(?={|$)"
pop: true
- match: \w+
scope: entity.other.inherited-class.kotlin
- match: \(
push:
- match: \)
pop: true
- include: expressions
- match: '\{'
push:
- match: '\}'
pop: true
- include: statements
comments:
- match: /\*
captures:
0: punctuation.definition.comment.kotlin
push:
- meta_scope: comment.block.kotlin
- match: \*/
captures:
0: punctuation.definition.comment.kotlin
pop: true
- match: \s*((//).*$\n?)
captures:
1: comment.line.double-slash.kotlin
2: punctuation.definition.comment.kotlin
constants:
- match: \b(true|false|null|this|super)\b
scope: constant.language.kotlin
- match: '\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)([LlFf])?\b'
scope: constant.numeric.kotlin
- match: '\b([A-Z][A-Z0-9_]+)\b'
scope: constant.other.kotlin
expressions:
- match: \(
push:
- match: \)
pop: true
- include: expressions
- include: types
- include: strings
- include: constants
- include: comments
- include: keywords
functions:
- match: (?=\s*\b(?:fun)\b)
push:
- match: '(?=$|\})'
pop: true
- match: \b(fun)\b
captures:
1: keyword.other.kotlin
push:
- match: (?=\()
pop: true
- match: <
push:
- match: ">"
pop: true
- include: generics
- match: '([\.<\?>\w]+\.)?(\w+)'
captures:
2: entity.name.function.kotlin
- match: \(
push:
- match: \)
pop: true
- include: parameters
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: "(?={|=|$)"
pop: true
- include: types
- match: '\{'
push:
- match: '(?=\})'
pop: true
- include: statements
- match: (=)
captures:
1: keyword.operator.assignment.kotlin
push:
- match: (?=$)
pop: true
- include: expressions
generics:
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: (?=,|>)
pop: true
- include: types
- include: keywords
- match: \w+
scope: storage.type.generic.kotlin
getters-and-setters:
- match: \b(get)\b\s*\(\s*\)
captures:
1: entity.name.function.kotlin
push:
- match: '\}|(?=\bset\b)|$'
pop: true
- match: (=)
captures:
1: keyword.operator.assignment.kotlin
push:
- match: (?=$|\bset\b)
pop: true
- include: expressions
- match: '\{'
push:
- match: '\}'
pop: true
- include: expressions
- match: \b(set)\b\s*(?=\()
captures:
1: entity.name.function.kotlin
push:
- match: '\}|(?=\bget\b)|$'
pop: true
- match: \(
push:
- match: \)
pop: true
- include: parameters
- match: (=)
captures:
1: keyword.operator.assignment.kotlin
push:
- match: (?=$|\bset\b)
pop: true
- include: expressions
- match: '\{'
push:
- match: '\}'
pop: true
- include: expressions
imports:
- match: '^\s*(import)\s+[^ $]+\s+(as)?'
captures:
1: keyword.other.kotlin
2: keyword.other.kotlin
keywords:
- match: \b(var|val|public|private|protected|abstract|final|sealed|enum|open|attribute|annotation|override|inline|vararg|in|out|internal|data|tailrec|operator|infix|const|yield|typealias|typeof|reified|suspend)\b
scope: storage.modifier.kotlin
- match: \b(try|catch|finally|throw)\b
scope: keyword.control.catch-exception.kotlin
- match: \b(if|else|while|for|do|return|when|where|break|continue)\b
scope: keyword.control.kotlin
- match: \b(in|is|!in|!is|as|as\?|assert)\b
scope: keyword.operator.kotlin
- match: (==|!=|===|!==|<=|>=|<|>)
scope: keyword.operator.comparison.kotlin
- match: (=)
scope: keyword.operator.assignment.kotlin
- match: (::)
scope: keyword.operator.kotlin
- match: (:)
scope: keyword.operator.declaration.kotlin
- match: \b(by)\b
scope: keyword.other.by.kotlin
- match: (\?\.)
scope: keyword.operator.safenav.kotlin
- match: (\.)
scope: keyword.operator.dot.kotlin
- match: (\?:)
scope: keyword.operator.elvis.kotlin
- match: (\-\-|\+\+)
scope: keyword.operator.increment-decrement.kotlin
- match: (\+=|\-=|\*=|\/=)
scope: keyword.operator.arithmetic.assign.kotlin
- match: (\.\.)
scope: keyword.operator.range.kotlin
- match: (\-|\+|\*|\/|%)
scope: keyword.operator.arithmetic.kotlin
- match: (!|&&|\|\|)
scope: keyword.operator.logical.kotlin
- match: (;)
scope: punctuation.terminator.kotlin
namespaces:
- match: \b(namespace)\b
scope: keyword.other.kotlin
- match: '\{'
push:
- match: '\}'
pop: true
- include: statements
parameters:
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: (?=,|\)|=)
pop: true
- include: types
- match: (=)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: (?=,|\))
pop: true
- include: expressions
- include: keywords
- match: \w+
scope: variable.parameter.function.kotlin
statements:
- include: namespaces
- include: typedefs
- include: classes
- include: functions
- include: variables
- include: getters-and-setters
- include: expressions
strings:
- match: '"""'
captures:
0: punctuation.definition.string.begin.kotlin
push:
- meta_scope: string.quoted.third.kotlin
- match: '"""'
captures:
0: punctuation.definition.string.end.kotlin
pop: true
- match: '(\$\w+|\$\{[^\}]+\})'
scope: variable.parameter.template.kotlin
- match: \\.
scope: constant.character.escape.kotlin
- match: '"'
captures:
0: punctuation.definition.string.begin.kotlin
push:
- meta_scope: string.quoted.double.kotlin
- match: '"'
captures:
0: punctuation.definition.string.end.kotlin
pop: true
- match: '(\$\w+|\$\{[^\}]+\})'
scope: variable.parameter.template.kotlin
- match: \\.
scope: constant.character.escape.kotlin
- match: "'"
captures:
0: punctuation.definition.string.begin.kotlin
push:
- meta_scope: string.quoted.single.kotlin
- match: "'"
captures:
0: punctuation.definition.string.end.kotlin
pop: true
- match: \\.
scope: constant.character.escape.kotlin
- match: "`"
captures:
0: punctuation.definition.string.begin.kotlin
push:
- meta_scope: string.quoted.single.kotlin
- match: "`"
captures:
0: punctuation.definition.string.end.kotlin
pop: true
typedefs:
- match: (?=\s*(?:type))
push:
- match: (?=$)
pop: true
- match: \b(type)\b
scope: keyword.other.kotlin
- match: <
push:
- match: ">"
pop: true
- include: generics
- include: expressions
types:
- match: \b(Nothing|Any|Unit|String|CharSequence|Int|Boolean|Char|Long|Double|Float|Short|Byte|dynamic)\b
scope: storage.type.buildin.kotlin
- match: \b(IntArray|BooleanArray|CharArray|LongArray|DoubleArray|FloatArray|ShortArray|ByteArray)\b
scope: storage.type.buildin.array.kotlin
- match: \b(Array|Collection|List|Map|Set|MutableList|MutableMap|MutableSet|Sequence)<\b
captures:
1: storage.type.buildin.collection.kotlin
push:
- match: ">"
pop: true
- include: types
- include: keywords
- match: \w+<
push:
- match: ">"
pop: true
- include: types
- include: keywords
- match: '\{'
push:
- match: '\}'
pop: true
- include: statements
- match: \(
push:
- match: \)
pop: true
- include: types
- match: (->)
scope: keyword.operator.declaration.kotlin
variables:
- match: (?=\s*\b(?:var|val)\b)
push:
- match: (?=:|=|(\b(by)\b)|$)
pop: true
- match: \b(var|val)\b
captures:
1: keyword.other.kotlin
push:
- match: (?=:|=|(\b(by)\b)|$)
pop: true
- match: <
push:
- match: ">"
pop: true
- include: generics
- match: '([\.<\?>\w]+\.)?(\w+)'
captures:
2: entity.name.variable.kotlin
- match: (:)
captures:
1: keyword.operator.declaration.kotlin
push:
- match: (?==|$)
pop: true
- include: types
- include: getters-and-setters
- match: \b(by)\b
captures:
1: keyword.other.kotlin
push:
- match: (?=$)
pop: true
- include: expressions
- match: (=)
captures:
1: keyword.operator.assignment.kotlin
push:
- match: (?=$)
pop: true
- include: expressions
- include: getters-and-setters
+1 -1
View File
@@ -97,7 +97,7 @@ The following files have been manually modified after converting from a `.tmLang
as it is not kept in a standalone repository. The file is generated from as it is not kept in a standalone repository. The file is generated from
https://github.com/open-policy-agent/opa/blob/master/misc/syntax/textmate/Rego.tmLanguage https://github.com/open-policy-agent/opa/blob/master/misc/syntax/textmate/Rego.tmLanguage
* `SML.sublime_syntax` has been added manually from * `SML.sublime_syntax` has been added manually from
https://github.com/seanjames777/SML-Language-Definitiona as it is not https://github.com/seanjames777/SML-Language-Definition as it is not
kept in a standalone repository. The file generated is from kept in a standalone repository. The file generated is from
https://github.com/seanjames777/SML-Language-Definition/blob/master/sml.tmLanguage https://github.com/seanjames777/SML-Language-Definition/blob/master/sml.tmLanguage
* `Cabal.sublime_syntax` has been added manually from * `Cabal.sublime_syntax` has been added manually from
+10 -1
View File
@@ -77,7 +77,8 @@ Options:
--terminal-width <width> --terminal-width <width>
Explicitly set the width of the terminal instead of determining it automatically. If Explicitly set the width of the terminal instead of determining it automatically. If
prefixed with '+' or '-', the value will be treated as an offset to the actual terminal prefixed with '+' or '-', the value will be treated as an offset to the actual terminal
width. See also: '--wrap'. width. This can also be configured via the BAT_WIDTH environment variable (e.g. export
BAT_WIDTH="100"). See also: '--wrap'.
-n, --number -n, --number
Only show line numbers, no other decorations. This is an alias for '--style=numbers' Only show line numbers, no other decorations. This is an alias for '--style=numbers'
@@ -163,6 +164,14 @@ Options:
escape sequences unless the syntax highlighting language is plain text. Possible values: escape sequences unless the syntax highlighting language is plain text. Possible values:
auto, always, *never*. auto, always, *never*.
--sanitize <when>
Specify when to sanitize input bytes for safe terminal display. Implies --strip-ansi to
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). Tab, LF, FF, and CRLF pass through. Useful for
displaying untrusted file content (e.g. file-manager preview panes). Possible values:
auto, always, *never*.
--style <components> --style <components>
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..) Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
to display in addition to the file contents. The argument is a comma-separated list of to display in addition to the file contents. The argument is a comma-separated list of
-2
View File
@@ -10,5 +10,3 @@ No issue will have a different priority based on sponsorship status of the
reporter. reporter.
Contributions from anybody are most welcomed, please see our [`CONTRIBUTING.md`](../CONTRIBUTING.md) guide. Contributions from anybody are most welcomed, please see our [`CONTRIBUTING.md`](../CONTRIBUTING.md) guide.
If you want to see our biggest sponsors, check the top of [`README.md`](../README.md#sponsors).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

+58 -10
View File
@@ -158,7 +158,9 @@ impl HighlightingAssets {
let syntax_match = mapping.get_syntax_for(path); let syntax_match = mapping.get_syntax_for(path);
if let Some(MappingTarget::MapToUnknown) = syntax_match { if let Some(MappingTarget::MapToUnknown) = syntax_match {
return Err(Error::UndetectedSyntax(path.to_string_lossy().into())); return Err(Error::UndetectedSyntax(
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()),
));
} }
if let Some(MappingTarget::MapTo(syntax_name)) = syntax_match { if let Some(MappingTarget::MapTo(syntax_name)) = syntax_match {
@@ -175,13 +177,17 @@ impl HighlightingAssets {
) { ) {
(Some(syntax), _) => Ok(syntax), (Some(syntax), _) => Ok(syntax),
(_, Some(MappingTarget::MapExtensionToUnknown)) => { (_, Some(MappingTarget::MapExtensionToUnknown)) => Err(Error::UndetectedSyntax(
Err(Error::UndetectedSyntax(path.to_string_lossy().into())) crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()),
} )),
_ => self _ => self
.get_syntax_for_file_extension(file_name, &mapping.ignored_suffixes)? .get_syntax_for_file_extension(file_name, &mapping.ignored_suffixes)?
.ok_or_else(|| Error::UndetectedSyntax(path.to_string_lossy().into())), .ok_or_else(|| {
Error::UndetectedSyntax(crate::preprocessor::sanitize_for_terminal(
&path.to_string_lossy(),
))
}),
} }
} }
@@ -341,15 +347,15 @@ impl HighlightingAssets {
file_name: &OsStr, file_name: &OsStr,
ignored_suffixes: &IgnoredSuffixes, ignored_suffixes: &IgnoredSuffixes,
) -> Result<Option<SyntaxReferenceInSet<'_>>> { ) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let mut syntax = self.find_syntax_by_extension(Path::new(file_name).extension())?; let stripped =
if syntax.is_none() {
syntax =
ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| { ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
// Note: recursion
self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes) self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes)
.map(Some)
})?; })?;
match stripped {
Some(syntax) => Ok(syntax),
None => self.find_syntax_by_extension(Path::new(file_name).extension()),
} }
Ok(syntax)
} }
fn get_first_line_syntax( fn get_first_line_syntax(
@@ -680,6 +686,48 @@ mod tests {
); );
} }
#[test]
fn syntax_detection_ignored_suffix_falls_back_to_first_line() {
let mut test = SyntaxDetectionTest::new();
// By default a `.txt` file uses Plain Text, even with a shebang: the
// `.txt` extension wins and first-line detection is not reached.
assert_eq!(
test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"),
"Plain Text"
);
// Once `.txt` is an ignored suffix it is stripped before detection. The
// stripped name (`test`) has no extension, so detection falls back to the
// first line -- even though `.txt` is itself a registered extension that
// would otherwise match as Plain Text. See #2745.
test.syntax_mapping.insert_ignored_suffix(".txt");
assert_eq!(
test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"),
"Bourne Again Shell (bash)"
);
assert_eq!(
test.syntax_for_file_with_content("test.txt", "<?php"),
"PHP"
);
// A `.txt` file without a recognizable first line keeps no syntax (the
// caller renders it as plain text); we must not resurrect the shadowed
// `.txt` -> Plain Text match here.
assert_eq!(
test.syntax_for_file_with_content("notes.txt", "just some prose"),
"!no syntax!"
);
// Stripping that exposes a real extension still works: `.dev` is ignored,
// and the remaining `.json` extension is detected as usual.
test.syntax_mapping.insert_ignored_suffix(".dev");
assert_eq!(
test.syntax_for_file_with_content("config.json.dev", ""),
"JSON"
);
}
#[test] #[test]
fn syntax_detection_is_case_insensitive() { fn syntax_detection_is_case_insensitive() {
let mut test = SyntaxDetectionTest::new(); let mut test = SyntaxDetectionTest::new();
+34 -9
View File
@@ -38,6 +38,15 @@ pub fn env_no_color() -> bool {
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty()) env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
} }
fn parse_strip_ansi_value(raw: Option<&str>, flag_name: &str) -> StripAnsiMode {
match raw {
Some("never") | None => StripAnsiMode::Never,
Some("always") => StripAnsiMode::Always,
Some("auto") => StripAnsiMode::Auto,
_ => unreachable!("other values for {flag_name} are not allowed"),
}
}
enum HelpType { enum HelpType {
Short, Short,
Long, Long,
@@ -458,16 +467,32 @@ impl App {
4 4
}, },
), ),
strip_ansi: match self strip_ansi: {
.matches let sanitize = parse_strip_ansi_value(
self.matches
.get_one::<String>("sanitize")
.map(|s| s.as_str()),
"--sanitize",
);
let strip_ansi = parse_strip_ansi_value(
self.matches
.get_one::<String>("strip-ansi") .get_one::<String>("strip-ansi")
.map(|s| s.as_str()) .map(|s| s.as_str()),
{ "--strip-ansi",
Some("never") => StripAnsiMode::Never, );
Some("always") => StripAnsiMode::Always, // --sanitize implies --strip-ansi to the same value.
Some("auto") => StripAnsiMode::Auto, if sanitize != StripAnsiMode::Never {
_ => unreachable!("other values for --strip-ansi are not allowed"), sanitize
} else {
strip_ansi
}
}, },
sanitize: parse_strip_ansi_value(
self.matches
.get_one::<String>("sanitize")
.map(|s| s.as_str()),
"--sanitize",
),
quiet_empty: self.matches.get_flag("quiet-empty"), quiet_empty: self.matches.get_flag("quiet-empty"),
unbuffered: self.matches.get_flag("unbuffered"), unbuffered: self.matches.get_flag("unbuffered"),
theme: theme(self.theme_options()).to_string(), theme: theme(self.theme_options()).to_string(),
@@ -645,7 +670,7 @@ impl App {
Ok(styled_components) Ok(styled_components)
} }
fn theme_options(&self) -> ThemeOptions { pub(crate) fn theme_options(&self) -> ThemeOptions {
Self::theme_options_from_matches(&self.matches) Self::theme_options_from_matches(&self.matches)
} }
+24 -2
View File
@@ -240,6 +240,7 @@ pub fn build_app(interactive_output: bool) -> Command {
.arg( .arg(
Arg::new("terminal-width") Arg::new("terminal-width")
.long("terminal-width") .long("terminal-width")
.overrides_with("terminal-width")
.value_name("width") .value_name("width")
.hide_short_help(true) .hide_short_help(true)
.allow_hyphen_values(true) .allow_hyphen_values(true)
@@ -255,10 +256,13 @@ pub fn build_app(interactive_output: bool) -> Command {
}) })
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
}) })
.help( .help("Explicitly set the width of the terminal instead of determining it automatically.")
.long_help(
"Explicitly set the width of the terminal instead of determining it \ "Explicitly set the width of the terminal instead of determining it \
automatically. If prefixed with '+' or '-', the value will be treated \ automatically. If prefixed with '+' or '-', the value will be treated \
as an offset to the actual terminal width. See also: '--wrap'.", as an offset to the actual terminal width. This can also be configured \
via the BAT_WIDTH environment variable (e.g. export BAT_WIDTH=\"100\"). \
See also: '--wrap'.",
), ),
) )
.arg( .arg(
@@ -476,6 +480,24 @@ pub fn build_app(interactive_output: bool) -> Command {
language is plain text. Possible values: auto, always, *never*.") language is plain text. Possible values: auto, always, *never*.")
.hide_short_help(true) .hide_short_help(true)
) )
.arg(
Arg::new("sanitize")
.long("sanitize")
.overrides_with("sanitize")
.value_name("when")
.value_parser(["auto", "always", "never"])
.default_value("never")
.hide_default_value(true)
.help("Sanitize untrusted input for safe display (auto, always, *never*)")
.long_help("Specify when to sanitize input bytes for safe terminal display. \
Implies --strip-ansi to 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). Tab, LF, FF, and CRLF pass through. Useful \
for displaying untrusted file content (e.g. file-manager preview panes). \
Possible values: auto, always, *never*.")
.hide_short_help(true)
)
.arg( .arg(
Arg::new("style") Arg::new("style")
.long("style") .long("style")
+1
View File
@@ -153,6 +153,7 @@ fn get_args_from_str(content: &str) -> Result<Vec<OsString>, shell_words::ParseE
pub fn get_args_from_env_vars() -> Vec<OsString> { pub fn get_args_from_env_vars() -> Vec<OsString> {
[ [
("--tabs", "BAT_TABS"), ("--tabs", "BAT_TABS"),
("--terminal-width", "BAT_WIDTH"),
("--theme", bat::theme::env::BAT_THEME), ("--theme", bat::theme::env::BAT_THEME),
("--theme-dark", bat::theme::env::BAT_THEME_DARK), ("--theme-dark", bat::theme::env::BAT_THEME_DARK),
("--theme-light", bat::theme::env::BAT_THEME_LIGHT), ("--theme-light", bat::theme::env::BAT_THEME_LIGHT),
+44 -13
View File
@@ -17,7 +17,6 @@ use std::path::Path;
use std::process; use std::process;
use bat::output::{OutputHandle, OutputType}; use bat::output::{OutputHandle, OutputType};
use bat::theme::DetectColorScheme;
use nu_ansi_term::Color::Green; use nu_ansi_term::Color::Green;
use nu_ansi_term::Style; use nu_ansi_term::Style;
@@ -39,7 +38,7 @@ use bat::{
error::*, error::*,
input::Input, input::Input,
style::{StyleComponent, StyleComponents}, style::{StyleComponent, StyleComponents},
theme::{color_scheme, default_theme, ColorScheme}, theme::{default_theme, theme, ColorScheme, ThemeOptions},
MappingTarget, PagingMode, MappingTarget, PagingMode,
}; };
@@ -153,7 +152,14 @@ pub fn get_languages(config: &Config, cache_dir: &Path) -> Result<String> {
let comma_separator = ", "; let comma_separator = ", ";
let separator = " "; let separator = " ";
// Line-wrapping for the possible file extension overflow. // Line-wrapping for the possible file extension overflow.
let desired_width = config.term_width - longest - separator.len(); // Clamp instead of subtracting: a tiny `--terminal-width` (smaller than the
// longest language name) would otherwise underflow `usize` and wrap to a huge
// value, silently disabling wrapping (and panicking in debug/overflow-checked
// builds). Mirrors the `saturating_sub` fix applied to `print_snip` in #3804.
let desired_width = config
.term_width
.saturating_sub(longest)
.saturating_sub(separator.len());
let style = if config.colored_output { let style = if config.colored_output {
Green.normal() Green.normal()
@@ -197,7 +203,7 @@ pub fn list_themes(
cfg: &Config, cfg: &Config,
config_dir: &Path, config_dir: &Path,
cache_dir: &Path, cache_dir: &Path,
detect_color_scheme: DetectColorScheme, theme_options: ThemeOptions,
) -> Result<()> { ) -> 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, cache_dir)?;
let mut config = cfg.clone(); let mut config = cfg.clone();
@@ -206,7 +212,7 @@ pub fn list_themes(
config.language = Some("Rust"); config.language = Some("Rust");
config.style_components = StyleComponents(style); config.style_components = StyleComponents(style);
let default_theme_name = default_theme(color_scheme(detect_color_scheme).unwrap_or_default()); let default_theme_name = theme(theme_options).to_string();
let mut buf = String::new(); let mut buf = String::new();
let mut handle = OutputHandle::FmtWrite(&mut buf); let mut handle = OutputHandle::FmtWrite(&mut buf);
@@ -259,7 +265,9 @@ pub fn list_themes(
fn set_terminal_title_to(new_terminal_title: String) { fn set_terminal_title_to(new_terminal_title: String) {
let osc_command_for_setting_terminal_title = "\x1b]0;"; let osc_command_for_setting_terminal_title = "\x1b]0;";
let osc_end_command = "\x07"; let osc_end_command = "\x07";
print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}"); // 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(); io::stdout().flush().unwrap();
} }
@@ -285,7 +293,28 @@ fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Resu
#[cfg(feature = "bugreport")] #[cfg(feature = "bugreport")]
fn invoke_bugreport(app: &App, cache_dir: &Path) { fn invoke_bugreport(app: &App, cache_dir: &Path) {
use bugreport::{bugreport, collector::*, format::Markdown}; use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry};
struct ColorSchemeCollector;
impl Collector for ColorSchemeCollector {
fn description(&self) -> &str {
"Detected terminal color scheme"
}
fn collect(
&mut self,
_: &bugreport::CrateInfo,
) -> std::result::Result<ReportEntry, CollectionError> {
let color_scheme = bat::theme::color_scheme(bat::theme::DetectColorScheme::Always);
let text = match color_scheme {
Some(bat::theme::ColorScheme::Dark) => "dark",
Some(bat::theme::ColorScheme::Light) => "light",
None => "not detected",
};
Ok(ReportEntry::Text(text.to_string()))
}
}
let pager = bat::config::get_pager_executable( let pager = bat::config::get_pager_executable(
app.matches.get_one::<String>("pager").map(|s| s.as_str()), app.matches.get_one::<String>("pager").map(|s| s.as_str()),
) )
@@ -307,6 +336,9 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
"BAT_STYLE", "BAT_STYLE",
"BAT_TABS", "BAT_TABS",
"BAT_THEME", "BAT_THEME",
"BAT_WIDTH",
bat::theme::env::BAT_THEME_DARK,
bat::theme::env::BAT_THEME_LIGHT,
"COLORTERM", "COLORTERM",
"LANG", "LANG",
"LC_ALL", "LC_ALL",
@@ -326,6 +358,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
custom_assets_metadata, custom_assets_metadata,
)) ))
.info(DirectoryEntries::new("Custom assets", cache_dir)) .info(DirectoryEntries::new("Custom assets", cache_dir))
.info(ColorSchemeCollector)
.info(CompileTimeInformation::default()); .info(CompileTimeInformation::default());
#[cfg(feature = "paging")] #[cfg(feature = "paging")]
@@ -394,14 +427,12 @@ fn run() -> Result<bool> {
if app.matches.get_flag("list-languages") { if app.matches.get_flag("list-languages") {
let languages: String = get_languages(&config, cache_dir)?; let languages: String = get_languages(&config, cache_dir)?;
let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))]; let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))];
let plain_config = Config { let mut plain_config = config.clone();
style_components: StyleComponents::new(StyleComponent::Plain.components(false)), plain_config.style_components =
paging_mode: PagingMode::QuitIfOneScreen, StyleComponents::new(StyleComponent::Plain.components(false));
..Default::default()
};
run_controller(inputs, &plain_config, cache_dir) run_controller(inputs, &plain_config, cache_dir)
} else if app.matches.get_flag("list-themes") { } else if app.matches.get_flag("list-themes") {
list_themes(&config, config_dir, cache_dir, DetectColorScheme::default())?; list_themes(&config, config_dir, cache_dir, app.theme_options())?;
Ok(true) Ok(true)
} else if app.matches.get_flag("config-file") { } else if app.matches.get_flag("config-file") {
println!("{}", config_file().to_string_lossy()); println!("{}", config_file().to_string_lossy());
+3
View File
@@ -111,6 +111,9 @@ pub struct Config<'a> {
// 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, pub strip_ansi: StripAnsiMode,
// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi.
pub sanitize: StripAnsiMode,
/// Whether or not to produce no output when input is empty /// Whether or not to produce no output when input is empty
pub quiet_empty: bool, pub quiet_empty: bool,
+178 -64
View File
@@ -1,12 +1,13 @@
#![cfg(feature = "git")] #![cfg(feature = "git")]
use gix::diff::blob::pipeline::{Mode, WorktreeRoots};
use gix::diff::blob::{Algorithm, HunkIter, ResourceKind};
use gix::object::tree::EntryKind;
use path_abs::PathInfo;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs;
use std::path::Path; use std::path::Path;
use git2::{DiffOptions, IntoCString, Repository}; #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug)]
pub enum LineChange { pub enum LineChange {
Added, Added,
RemovedAbove, RemovedAbove,
@@ -16,68 +17,181 @@ pub enum LineChange {
pub type LineChanges = HashMap<u32, LineChange>; pub type LineChanges = HashMap<u32, LineChange>;
fn collect_changes_from_hunks(hunks: HunkIter) -> Option<LineChanges> {
let mut changes: LineChanges = HashMap::new();
for hunk in hunks {
if hunk.before.is_empty() && !hunk.after.is_empty() {
for line in hunk.after {
changes.insert(line + 1, LineChange::Added);
}
} else if hunk.after.is_empty() && !hunk.before.is_empty() {
if hunk.after.start == 0 {
changes.insert(1, LineChange::RemovedAbove);
} else {
changes.insert(hunk.after.start, LineChange::RemovedBelow);
}
} else {
for line in hunk.after {
changes.insert(line + 1, LineChange::Modified);
}
}
}
Some(changes)
}
pub fn get_git_diff(filename: &Path) -> Option<LineChanges> { pub fn get_git_diff(filename: &Path) -> Option<LineChanges> {
let repo = Repository::discover(filename).ok()?; let filepath_absolute = filename.canonicalize().ok()?;
let repository = gix::discover(filepath_absolute.parent().ok()?).ok()?;
let repo_path_absolute = fs::canonicalize(repo.workdir()?).ok()?; let repo_path_absolute = repository.workdir()?.canonicalize().ok()?;
let filepath_relative_to_repo = gix::path::to_unix_separators_on_windows(gix::path::into_bstr(
let filepath_absolute = fs::canonicalize(filename).ok()?; filepath_absolute.strip_prefix(&repo_path_absolute).ok()?,
let filepath_relative_to_repo = filepath_absolute.strip_prefix(&repo_path_absolute).ok()?; ));
let index = repository.index_or_load_from_head_or_empty().ok()?;
let mut diff_options = DiffOptions::new(); let index_entry = index.entry_by_path(filepath_relative_to_repo.as_ref())?;
let pathspec = filepath_relative_to_repo.into_c_string().ok()?; let mut cache = repository
diff_options.pathspec(pathspec); .diff_resource_cache(
diff_options.context_lines(0); Mode::ToGit,
WorktreeRoots {
let diff = repo old_root: None,
.diff_index_to_workdir(None, Some(&mut diff_options)) new_root: repository.workdir().map(Path::to_path_buf),
},
)
.ok()?; .ok()?;
cache
let mut line_changes: LineChanges = HashMap::new(); .set_resource(
index_entry.id,
let mark_section = index_entry.mode.to_tree_entry_mode()?.kind(),
|line_changes: &mut LineChanges, start: u32, end: i32, change: LineChange| { filepath_relative_to_repo.as_ref(),
for line in start..=end as u32 { ResourceKind::OldOrSource,
line_changes.insert(line, change); &repository,
} )
}; .ok()?;
cache
let _ = diff.foreach( .set_resource(
&mut |_, _| true, repository.object_hash().null(),
None, EntryKind::Blob,
Some(&mut |delta, hunk| { filepath_relative_to_repo.as_ref(),
let path = delta.new_file().path().unwrap_or_else(|| Path::new("")); ResourceKind::NewOrDestination,
&repository,
if filepath_relative_to_repo != path { )
return false; .ok()?;
} let diff = gix::diff::blob::diff_with_slider_heuristics(
Algorithm::Histogram,
let old_lines = hunk.old_lines(); &cache.prepare_diff().ok()?.interned_input(),
let new_start = hunk.new_start();
let new_lines = hunk.new_lines();
let new_end = (new_start + new_lines) as i32 - 1;
if old_lines == 0 && new_lines > 0 {
mark_section(&mut line_changes, new_start, new_end, LineChange::Added);
} else if new_lines == 0 && old_lines > 0 {
if new_start == 0 {
mark_section(&mut line_changes, 1, 1, LineChange::RemovedAbove);
} else {
mark_section(
&mut line_changes,
new_start,
new_start as i32,
LineChange::RemovedBelow,
);
}
} else {
mark_section(&mut line_changes, new_start, new_end, LineChange::Modified);
}
true
}),
None,
); );
Some(line_changes) collect_changes_from_hunks(diff.hunks())
}
#[cfg(test)]
mod tests {
use super::{get_git_diff, LineChange};
use std::path::{Path, PathBuf};
use std::process::Command;
fn git(repo: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(repo)
.output()
.expect("git command can run");
assert!(
output.status.success(),
"git {args:?} failed\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn setup_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("can create temporary directory");
let repo = dir.path();
git(repo, &["init"]);
git(repo, &["config", "user.email", "test@test.test"]);
git(repo, &["config", "user.name", "Test"]);
dir
}
fn create_and_track_file(repo: &tempfile::TempDir, filename: &str) -> PathBuf {
let filepath = repo.path().join(filename);
std::fs::write(&filepath, "file\n").expect("can write file");
git(repo.path(), &["add", filename]);
git(repo.path(), &["commit", "-m", "initial"]);
filepath.to_owned()
}
#[test]
fn not_a_git_repository() {
let dir = tempfile::tempdir().expect("can create temporary directory");
let file = dir.path().join("file.txt");
std::fs::write(&file, "line 1\n").expect("can write file");
assert_eq!(get_git_diff(&file), None);
}
#[test]
fn diff_is_calculated_against_index() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
// Add modified file to index -> should find 0 changes but not none
git(repo.path(), &["add", "file.txt"]);
assert_eq!(get_git_diff(&file).expect("empty map").len(), 0);
}
#[test]
fn diff_is_calculated_against_index_2() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
git(repo.path(), &["add", "file.txt"]);
// modify the second line again which should show a single change
std::fs::write(&file, "line 1\nline 2 modified again\n").expect("can write file");
assert_eq!(get_git_diff(&file).expect("one change").len(), 1);
}
#[test]
fn diff_is_calculated_correctly() {
let repo = setup_repo();
create_and_track_file(&repo, "committed.txt");
let filename = "committed2.txt";
let file = create_and_track_file(&repo, filename);
std::fs::write(&file, "file\nline 2 added\n").expect("can write file");
let mut changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&2), Some(&LineChange::Added));
git(repo.path(), &["add", filename]);
std::fs::write(&file, "file\nline 2 modified\n").expect("can write file");
changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&2), Some(&LineChange::Modified));
git(repo.path(), &["add", filename]);
std::fs::write(&file, "line 2 modified\nline 3 added").expect("can write file");
changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&1), Some(&LineChange::RemovedAbove));
assert_eq!(changes.get(&2), Some(&LineChange::Added));
}
#[test]
fn faulty_git_version_does_not_panic() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
// changes are detected
assert_eq!(get_git_diff(&file).expect("one change").len(), 2);
// write invalid repositoryformatversion
git(
repo.path(),
&["config", "core.repositoryformatversion", "one"],
);
// changes are no longer detected
assert_eq!(get_git_diff(&file), None);
}
} }
+84 -27
View File
@@ -207,7 +207,7 @@ impl<'a> Input<'a> {
kind: OpenedInputKind::StdIn, kind: OpenedInputKind::StdIn,
description, description,
metadata: self.metadata, metadata: self.metadata,
reader: InputReader::new(stdin), reader: InputReader::try_new(stdin)?,
}) })
} }
@@ -216,34 +216,34 @@ impl<'a> Input<'a> {
description, description,
metadata: self.metadata, metadata: self.metadata,
reader: { reader: {
let mut file = File::open(&path) let path_display =
.map_err(|e| format!("'{}': {e}", path.to_string_lossy()))?; crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy());
let mut file =
File::open(&path).map_err(|e| format!("'{path_display}': {e}"))?;
if file.metadata()?.is_dir() { if file.metadata()?.is_dir() {
return Err(format!("'{}' is a directory.", path.to_string_lossy()).into()); return Err(format!("'{path_display}' is a directory.").into());
} }
if let Some(stdout) = stdout_identifier { if let Some(stdout) = stdout_identifier {
let input_identifier = Identifier::try_from(file).map_err(|e| { let input_identifier = Identifier::try_from(file)
format!("{}: Error identifying file: {e}", path.to_string_lossy()) .map_err(|e| format!("{path_display}: Error identifying file: {e}"))?;
})?;
if stdout.surely_conflicts_with(&input_identifier) { if stdout.surely_conflicts_with(&input_identifier) {
return Err(format!( return Err(format!(
"IO circle detected. The input from '{}' is also an output. Aborting to avoid infinite loop.", "IO circle detected. The input from '{path_display}' is also an output. Aborting to avoid infinite loop.",
path.to_string_lossy()
) )
.into()); .into());
} }
file = input_identifier.into_inner().expect("The file was lost in the clircle::Identifier, this should not have happened..."); file = input_identifier.into_inner().expect("The file was lost in the clircle::Identifier, this should not have happened...");
} }
InputReader::new(BufReader::new(file)) InputReader::try_new(BufReader::new(file))?
}, },
}), }),
InputKind::CustomReader(reader) => Ok(OpenedInput { InputKind::CustomReader(reader) => Ok(OpenedInput {
description, description,
kind: OpenedInputKind::CustomReader, kind: OpenedInputKind::CustomReader,
metadata: self.metadata, metadata: self.metadata,
reader: InputReader::new(BufReader::new(reader)), reader: InputReader::try_new(BufReader::new(reader))?,
}), }),
} }
} }
@@ -257,28 +257,29 @@ pub(crate) struct InputReader<'a> {
} }
impl<'a> InputReader<'a> { impl<'a> InputReader<'a> {
pub(crate) fn new<R: BufRead + 'a>(mut reader: R) -> InputReader<'a> { #[cfg(test)]
let mut first_line = vec![]; pub(crate) fn new<R: BufRead + 'a>(reader: R) -> InputReader<'a> {
reader.read_until(b'\n', &mut first_line).ok(); Self::try_new(reader).expect("reading the first line failed")
let content_type = if first_line.is_empty() {
None
} else {
Some(content_inspector::inspect(&first_line[..]))
};
if content_type == Some(ContentType::UTF_16LE) {
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A).ok();
} else if content_type == Some(ContentType::UTF_16BE) {
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00).ok();
} }
InputReader { pub(crate) fn try_new<R: BufRead + 'a>(mut reader: R) -> io::Result<InputReader<'a>> {
let mut first_line = vec![];
reader.read_until(b'\n', &mut first_line)?;
let content_type = inspect_content_type(&first_line);
if content_type == Some(ContentType::UTF_16LE) {
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?;
} else if content_type == Some(ContentType::UTF_16BE) {
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?;
}
Ok(InputReader {
inner: Box::new(reader), inner: Box::new(reader),
first_line, first_line,
content_type, content_type,
unbuffered: false, unbuffered: false,
} })
} }
pub(crate) fn read_line(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> { pub(crate) fn read_line(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
@@ -319,6 +320,25 @@ impl<'a> InputReader<'a> {
} }
} }
fn inspect_content_type(first_line: &[u8]) -> Option<ContentType> {
if first_line.is_empty() {
return None;
}
let content_type = content_inspector::inspect(first_line);
if content_type == ContentType::UTF_8 && has_zip_signature(first_line) {
Some(ContentType::BINARY)
} else {
Some(content_type)
}
}
fn has_zip_signature(bytes: &[u8]) -> bool {
[b"PK\x03\x04", b"PK\x05\x06", b"PK\x07\x08"]
.into_iter()
.any(|signature| bytes.starts_with(signature))
}
fn read_utf16_line<R: BufRead>( fn read_utf16_line<R: BufRead>(
reader: &mut R, reader: &mut R,
buf: &mut Vec<u8>, buf: &mut Vec<u8>,
@@ -374,6 +394,43 @@ fn basic() {
assert!(buffer.is_empty()); assert!(buffer.is_empty());
} }
#[test]
fn zip_magic_headers_are_treated_as_binary() {
for content in [b"PK\x03\x04hello", b"PK\x05\x06hello", b"PK\x07\x08hello"] {
let reader = InputReader::new(&content[..]);
assert_eq!(Some(ContentType::BINARY), reader.content_type);
}
}
#[test]
fn non_zip_pk_prefix_is_not_treated_as_binary() {
assert_eq!(
Some(ContentType::UTF_8),
inspect_content_type(b"PK\x03\x03hello")
);
}
#[test]
fn input_open_returns_initial_read_errors() {
struct FailingRead;
impl Read for FailingRead {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::other("initial read failed"))
}
}
let input = Input::from_reader(Box::new(FailingRead));
let result = input.open(io::empty(), None);
assert!(result.is_err());
assert!(result
.err()
.unwrap()
.to_string()
.contains("initial read failed"));
}
#[test] #[test]
fn utf16le() { fn utf16le() {
let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00"; let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00";
+115 -7
View File
@@ -14,6 +14,53 @@ use crate::{
input::{Input, InputKind, InputReader, OpenedInput, OpenedInputKind}, input::{Input, InputKind, InputReader, OpenedInput, OpenedInputKind},
}; };
/// Wrap `s` in POSIX single quotes. An embedded `'` is encoded as `'\''`
/// (close, escaped quote, reopen) since `'...'` has no escape character.
fn shell_quote(s: &str) -> String {
if !s.contains('\'') {
let mut quoted = String::with_capacity(s.len() + 2);
quoted.push('\'');
quoted.push_str(s);
quoted.push('\'');
return quoted;
}
let mut quoted = String::with_capacity(s.len() + 2);
quoted.push('\'');
for c in s.chars() {
if c == '\'' {
quoted.push_str("'\\''");
} else {
quoted.push(c);
}
}
quoted.push('\'');
quoted
}
/// Substitute the first `%s` in a $LESSOPEN/$LESSCLOSE template with a quoted value.
fn shell_substitute(template: &str, replacement: &str) -> String {
template.replacen("%s", &shell_quote(replacement), 1)
}
/// Substitute the first two `%s` occurrences positionally in one pass; chaining
/// `shell_substitute` twice would mis-target if `first` itself contains `%s`.
fn shell_substitute_two(template: &str, first: &str, second: &str) -> String {
let mut out = String::with_capacity(template.len() + first.len() + second.len() + 4);
let mut remaining = template;
let mut to_substitute = [Some(first), Some(second)];
for slot in &mut to_substitute {
if let Some(idx) = remaining.find("%s") {
out.push_str(&remaining[..idx]);
out.push_str(&shell_quote(slot.take().unwrap()));
remaining = &remaining[idx + 2..];
} else {
break;
}
}
out.push_str(remaining);
out
}
/// Preprocess files and/or stdin using $LESSOPEN and $LESSCLOSE /// Preprocess files and/or stdin using $LESSOPEN and $LESSCLOSE
pub(crate) struct LessOpenPreprocessor { pub(crate) struct LessOpenPreprocessor {
lessopen: String, lessopen: String,
@@ -87,7 +134,7 @@ impl LessOpenPreprocessor {
None => return input.open(stdin, stdout_identifier), None => return input.open(stdin, stdout_identifier),
}; };
let mut lessopen_command = shell(self.lessopen.replacen("%s", path_str, 1)); let mut lessopen_command = shell(shell_substitute(&self.lessopen, path_str));
lessopen_command.stdout(Stdio::piped()); lessopen_command.stdout(Stdio::piped());
let lessopen_output = match lessopen_command.execute_output() { let lessopen_output = match lessopen_command.execute_output() {
@@ -122,7 +169,7 @@ impl LessOpenPreprocessor {
let mut stdin_buffer = Vec::new(); let mut stdin_buffer = Vec::new();
stdin.read_to_end(&mut stdin_buffer)?; stdin.read_to_end(&mut stdin_buffer)?;
let mut lessopen_command = shell(self.lessopen.replacen("%s", "-", 1)); let mut lessopen_command = shell(shell_substitute(&self.lessopen, "-"));
lessopen_command.stdout(Stdio::piped()); lessopen_command.stdout(Stdio::piped());
let lessopen_output = match lessopen_command.execute_input_output(&stdin_buffer) let lessopen_output = match lessopen_command.execute_input_output(&stdin_buffer)
@@ -155,7 +202,7 @@ impl LessOpenPreprocessor {
Ok(OpenedInput { Ok(OpenedInput {
kind, kind,
reader: InputReader::new(BufReader::new( reader: InputReader::try_new(BufReader::new(
if matches!(self.kind, LessOpenKind::TempFile) { if matches!(self.kind, LessOpenKind::TempFile) {
let lessopen_string = match String::from_utf8(lessopen_stdout) { let lessopen_string = match String::from_utf8(lessopen_stdout) {
Ok(string) => string, Ok(string) => string,
@@ -181,7 +228,7 @@ impl LessOpenPreprocessor {
lessclose: self lessclose: self
.lessclose .lessclose
.as_ref() .as_ref()
.map(|s| s.replacen("%s", &path_str, 1).replacen("%s", &stdout, 1)), .map(|s| shell_substitute_two(s, &path_str, &stdout)),
} }
} else { } else {
Preprocessed { Preprocessed {
@@ -189,10 +236,10 @@ impl LessOpenPreprocessor {
lessclose: self lessclose: self
.lessclose .lessclose
.as_ref() .as_ref()
.map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)), .map(|s| shell_substitute_two(s, &path_str, "-")),
} }
}, },
)), ))?,
metadata: input.metadata, metadata: input.metadata,
description: input.description, description: input.description,
}) })
@@ -261,7 +308,7 @@ impl Drop for Preprocessed {
} }
}; };
if lessclose_output.status.success() { if !lessclose_output.status.success() {
bat_warning!("$LESSCLOSE exited with nonzero exit code",) bat_warning!("$LESSCLOSE exited with nonzero exit code",)
}; };
} }
@@ -384,4 +431,65 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn shell_quote_plain_filename() {
assert_eq!(super::shell_quote("file.txt"), "'file.txt'");
assert_eq!(super::shell_quote("with space.txt"), "'with space.txt'");
assert_eq!(super::shell_quote(""), "''");
assert_eq!(super::shell_quote("-"), "'-'");
}
#[test]
fn shell_quote_metacharacters_neutralised() {
assert_eq!(super::shell_quote("; rm -rf ~ ;"), "'; rm -rf ~ ;'");
assert_eq!(super::shell_quote("$(payload)"), "'$(payload)'");
assert_eq!(super::shell_quote("`payload`"), "'`payload`'");
assert_eq!(super::shell_quote("a|b&c;d"), "'a|b&c;d'");
assert_eq!(super::shell_quote("..>/dev/null"), "'..>/dev/null'");
assert_eq!(super::shell_quote("\n\t"), "'\n\t'");
}
#[test]
fn shell_quote_embedded_single_quote() {
assert_eq!(super::shell_quote("it's"), "'it'\\''s'");
assert_eq!(super::shell_quote("'"), "''\\'''");
assert_eq!(super::shell_quote("a'b'c"), "'a'\\''b'\\''c'");
}
#[test]
fn shell_substitute_only_replaces_first_percent_s() {
assert_eq!(super::shell_substitute("echo %s", "x"), "echo 'x'");
assert_eq!(super::shell_substitute("echo %s %s", "x"), "echo 'x' %s");
}
#[test]
fn shell_substitute_protects_against_filename_injection() {
let template = "|preproc %s";
let attacker_filename = "; rm -rf ~ ;";
let result = super::shell_substitute(template, attacker_filename);
assert_eq!(result, "|preproc '; rm -rf ~ ;'");
}
#[test]
fn shell_substitute_two_replaces_first_two_placeholders() {
assert_eq!(
super::shell_substitute_two("echo %s and %s done", "a", "b"),
"echo 'a' and 'b' done"
);
// A third %s is left alone.
assert_eq!(
super::shell_substitute_two("echo %s %s %s", "a", "b"),
"echo 'a' 'b' %s"
);
}
#[test]
fn shell_substitute_two_handles_percent_s_in_first_argument() {
// The chained-substitute approach would replace the injected `%s` here.
assert_eq!(
super::shell_substitute_two("a %s b %s c", "%s", "second"),
"a '%s' b 'second' c"
);
}
} }
+1
View File
@@ -54,6 +54,7 @@ mod vscreen;
pub(crate) mod wrapping; pub(crate) mod wrapping;
pub use nonprintable_notation::{BinaryBehavior, NonprintableNotation}; pub use nonprintable_notation::{BinaryBehavior, NonprintableNotation};
pub use preprocessor::sanitize_for_terminal;
pub use preprocessor::StripAnsiMode; pub use preprocessor::StripAnsiMode;
pub use pretty_printer::{Input, PrettyPrinter, Syntax}; pub use pretty_printer::{Input, PrettyPrinter, Syntax};
pub use syntax_mapping::{MappingTarget, SyntaxMapping}; pub use syntax_mapping::{MappingTarget, SyntaxMapping};
+270 -2
View File
@@ -139,16 +139,140 @@ pub fn replace_nonprintable(
/// Strips ANSI escape sequences from the input. /// Strips ANSI escape sequences from the input.
pub fn strip_ansi(line: &str) -> String { pub fn strip_ansi(line: &str) -> String {
let mut buffer = String::with_capacity(line.len()); let mut buffer = String::with_capacity(line.len());
for seq in EscapeSequenceOffsetsIterator::new(line) { for seq in EscapeSequenceOffsetsIterator::new(line) {
if let EscapeSequenceOffsets::Text { .. } = seq { if let EscapeSequenceOffsets::Text { .. } = seq {
buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]); buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]);
} }
} }
buffer buffer
} }
/// Strips ANSI escape sequences and substitutes terminal-active control bytes
/// and visual-spoofing Unicode codepoints (bidi, zero-width) with U+FFFD.
pub fn sanitize(line: &str) -> String {
let stripped = strip_ansi(line);
let mut buffer = String::with_capacity(stripped.len());
let bytes = stripped.as_bytes();
let mut start = 0;
let mut i = 0;
// Skip directly to the next trigger byte instead of testing each one.
while let Some(off) = bytes[i..].iter().position(|&b| is_sanitize_trigger(b)) {
i += off;
let len = sanitize_at(bytes, i, &stripped, &mut buffer, &mut start);
i += len;
}
buffer.push_str(&stripped[start..]);
buffer
}
#[inline]
fn is_sanitize_trigger(b: u8) -> bool {
// C0 controls minus \t \n \f; DEL; UTF-8 leads with dangerous codepoints.
matches!(b, 0x00..=0x08 | 0x0B | 0x0D..=0x1F | 0x7F | 0xC2 | 0xD8 | 0xE2 | 0xEF)
}
/// Substitutes the byte/sequence at `bytes[i]` (or passes it through on
/// false-alarm trigger), flushing the prefix from `start`. Returns bytes consumed.
fn sanitize_at(
bytes: &[u8],
i: usize,
full: &str,
buffer: &mut String,
start: &mut usize,
) -> usize {
buffer.push_str(&full[*start..i]);
let consumed = match bytes[i] {
b'\r' if bytes.get(i + 1) == Some(&b'\n') => {
buffer.push_str("\r\n");
2
}
// 0xC2 leads U+0080..U+00FF; filter the C1 range.
0xC2 if matches!(bytes.get(i + 1), Some(0x80..=0x9F)) => {
buffer.push('\u{FFFD}');
2
}
0xE2 if is_dangerous_e2(bytes, i) => {
buffer.push('\u{FFFD}');
3
}
// 0xEF 0xBB 0xBF = U+FEFF (BOM / zero-width no-break space).
0xEF if bytes.get(i + 1) == Some(&0xBB) && bytes.get(i + 2) == Some(&0xBF) => {
buffer.push('\u{FFFD}');
3
}
// 0xD8 0x9C = U+061C (Arabic letter mark, a bidi control). The rest of
// the 0xD8 block is ordinary Arabic text.
0xD8 if bytes.get(i + 1) == Some(&0x9C) => {
buffer.push('\u{FFFD}');
2
}
// False-alarm trigger: pass the full UTF-8 sequence through.
lead @ (0xC2 | 0xD8 | 0xE2 | 0xEF) => {
let n = utf8_len_from_lead(lead);
buffer.push_str(&full[i..i + n]);
n
}
_ => {
buffer.push('\u{FFFD}');
1
}
};
*start = i + consumed;
consumed
}
#[inline]
fn is_dangerous_e2(bytes: &[u8], i: usize) -> bool {
// U+200B..D (zero-width), U+200E..F (LRM/RLM), U+202A..E (bidi embedding
// and override), U+2066..9 (bidi isolates).
matches!(
(bytes.get(i + 1), bytes.get(i + 2)),
(Some(0x80), Some(0x8B..=0x8F | 0xAA..=0xAE)) | (Some(0x81), Some(0xA6..=0xA9))
)
}
#[inline]
fn utf8_len_from_lead(lead: u8) -> usize {
if lead < 0x80 {
1
} else if lead < 0xE0 {
2
} else if lead < 0xF0 {
3
} else {
4
}
}
/// Escape C0, DEL, and C1 control characters so a string from an untrusted
/// filename or path can be safely written to the terminal.
pub fn sanitize_for_terminal(input: &str) -> String {
if !input
.chars()
.any(|c| matches!(c, '\x00'..='\x08' | '\x0A'..='\x1F' | '\x7F'..='\u{9F}'))
{
return input.to_owned();
}
let mut out = String::with_capacity(input.len() + 8);
for c in input.chars() {
match c {
'\t' => out.push('\t'),
'\x00'..='\x1F' => {
out.push('^');
out.push(char::from_u32(0x40 + c as u32).unwrap_or('?'));
}
'\x7F' => out.push_str("^?"),
'\u{80}'..='\u{9F}' => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{{{:x}}}", c as u32);
}
other => out.push(other),
}
}
out
}
/// Strips overstrike sequences (backspace formatting) from input. /// Strips overstrike sequences (backspace formatting) from input.
/// ///
/// Overstrike formatting is used by man pages and some help output: /// Overstrike formatting is used by man pages and some help output:
@@ -241,6 +365,113 @@ fn test_strip_ansi() {
); );
} }
#[test]
fn test_strip_ansi_8bit_c1_introducers() {
assert_eq!(strip_ansi("a\u{9B}31mRED\u{9B}0mb"), "aREDb");
assert_eq!(strip_ansi("a\x1bP1;0|payload\x1b\\b"), "ab");
assert_eq!(strip_ansi("a\u{90}body\u{9C}b"), "ab");
}
#[test]
fn test_strip_ansi_single_char_esc() {
// strip_ansi must consume both bytes of single-byte ESC sequences (RIS, DECSC, keypad, VT52).
assert_eq!(strip_ansi("a\x1bcb"), "ab");
assert_eq!(strip_ansi("a\x1b7b\x1b8c"), "abc");
assert_eq!(strip_ansi("a\x1b=b\x1b>c"), "abc");
assert_eq!(strip_ansi("a\x1bZb"), "ab");
}
#[test]
fn test_strip_ansi_preserves_control_bytes() {
// strip_ansi removes only ANSI escape sequences; control bytes pass through.
assert_eq!(strip_ansi("safe\rEVIL"), "safe\rEVIL");
assert_eq!(strip_ansi("a\x08b\x07c\x0E\x0Fd"), "a\x08b\x07c\x0E\x0Fd");
}
#[test]
fn test_sanitize_substitutes_dangerous_bytes() {
let r = '\u{FFFD}';
assert_eq!(sanitize("safe\rEVIL"), format!("safe{r}EVIL"));
assert_eq!(sanitize("a\x08b"), format!("a{r}b"));
assert_eq!(sanitize("a\x07b"), format!("a{r}b"));
assert_eq!(sanitize("a\x0Bb"), format!("a{r}b"));
assert_eq!(sanitize("a\x0Eb\x0Fc"), format!("a{r}b{r}c"));
assert_eq!(sanitize("a\u{8D}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{85}b"), format!("a{r}b"));
assert_eq!(sanitize("trailing\r"), format!("trailing{r}"));
assert_eq!(sanitize("a\x7Fb"), format!("a{r}b"));
}
#[test]
fn test_sanitize_substitutes_bidi_and_zero_width() {
let r = '\u{FFFD}';
// Trojan-Source bidi formatting: U+202A..U+202E
assert_eq!(sanitize("a\u{202A}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{202E}b"), format!("a{r}b"));
// Bidi isolates: U+2066..U+2069
assert_eq!(sanitize("a\u{2066}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{2069}b"), format!("a{r}b"));
// Zero-width: U+200B..U+200D
assert_eq!(sanitize("a\u{200B}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{200D}b"), format!("a{r}b"));
// BOM in middle of file: U+FEFF
assert_eq!(sanitize("a\u{FEFF}b"), format!("a{r}b"));
}
#[test]
fn test_sanitize_substitutes_every_bidi_control() {
// Unicode Bidi_Control is exactly these 12 codepoints. Covering only some
// of them leaves a reordering attack available through the rest.
let r = '\u{FFFD}';
for c in [
'\u{061C}', '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}',
'\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}',
] {
assert_eq!(
sanitize(&format!("a{c}b")),
format!("a{r}b"),
"U+{:04X} was not substituted",
c as u32
);
}
}
#[test]
fn test_sanitize_preserves_arabic_sharing_the_alm_lead_byte() {
// U+061C is reached via lead byte 0xD8, which also leads ordinary Arabic.
assert_eq!(
sanitize("\u{0600}\u{061F}\u{06FF}"),
"\u{0600}\u{061F}\u{06FF}"
);
assert_eq!(sanitize("مرحبا بالعالم"), "مرحبا بالعالم");
}
#[test]
fn test_sanitize_preserves_legitimate_bytes() {
assert_eq!(sanitize("crlf\r\nline\r\n"), "crlf\r\nline\r\n");
assert_eq!(sanitize("a\tb\nc"), "a\tb\nc");
assert_eq!(sanitize("plain ascii"), "plain ascii");
assert_eq!(sanitize("üñíçödé"), "üñíçödé");
// FF (U+000C) passes through; section separator in C source / Emacs Lisp.
assert_eq!(sanitize("section1\x0Csection2"), "section1\x0Csection2");
// Common Unicode that shares a UTF-8 lead byte with dangerous codepoints
// must pass through unchanged.
assert_eq!(sanitize("snowman ☃ moon ☾"), "snowman ☃ moon ☾");
assert_eq!(sanitize("emoji 🎉 ☃"), "emoji 🎉 ☃");
assert_eq!(sanitize("0xC2 lead: ÿ ñ ç"), "0xC2 lead: ÿ ñ ç");
assert_eq!(sanitize("CJK 漢字 emoji 🦀"), "CJK 漢字 emoji 🦀");
}
#[test]
fn test_sanitize_strips_ansi() {
let r = '\u{FFFD}';
// sanitize is a strict superset of strip_ansi.
assert_eq!(sanitize("a\x1B[31mb\x1B[0mc"), "abc");
assert_eq!(sanitize("a\u{9B}31mb\u{9B}0mc"), "abc");
// ANSI then dangerous byte: ANSI gone, byte substituted.
assert_eq!(sanitize("\x1B[31mhello\rEVIL"), format!("hello{r}EVIL"));
}
#[test] #[test]
fn test_strip_overstrike() { fn test_strip_overstrike() {
// Bold: X\x08X (same char repeated) // Bold: X\x08X (same char repeated)
@@ -261,3 +492,40 @@ fn test_strip_overstrike() {
// Unicode with overstrike // Unicode with overstrike
assert_eq!(strip_overstrike("ä\x08äöü", 2), "äöü"); assert_eq!(strip_overstrike("ä\x08äöü", 2), "äöü");
} }
#[test]
fn test_sanitize_for_terminal_passthrough() {
assert_eq!(sanitize_for_terminal(""), "");
assert_eq!(sanitize_for_terminal("hello.txt"), "hello.txt");
assert_eq!(sanitize_for_terminal("résumé.pdf"), "résumé.pdf");
assert_eq!(sanitize_for_terminal("日本語.md"), "日本語.md");
assert_eq!(
sanitize_for_terminal("path/with spaces/file.log"),
"path/with spaces/file.log"
);
assert_eq!(sanitize_for_terminal("a\tb"), "a\tb");
}
#[test]
fn test_sanitize_for_terminal_c0_controls() {
assert_eq!(
sanitize_for_terminal("\x1b[31mINJECTED\x1b[0m.txt"),
"^[[31mINJECTED^[[0m.txt"
);
assert_eq!(sanitize_for_terminal("bad\x07rest"), "bad^Grest");
assert_eq!(sanitize_for_terminal("\x00\x08\n\r\x7F"), "^@^H^J^M^?");
assert_eq!(sanitize_for_terminal("\u{9b}31m"), "\\u{9b}31m");
assert_eq!(
sanitize_for_terminal("\u{9d}0;pwned\x07"),
"\\u{9d}0;pwned^G"
);
}
#[test]
fn test_sanitize_for_terminal_idempotent_on_sanitized() {
let dirty = "\x1b]0;pwned\x07file.txt";
let clean = sanitize_for_terminal(dirty);
assert_eq!(sanitize_for_terminal(&clean), clean);
assert!(!clean.contains('\x1b'));
assert!(!clean.contains('\x07'));
}
+9
View File
@@ -192,6 +192,15 @@ impl<'a> PrettyPrinter<'a> {
self self
} }
/// Whether to sanitize untrusted input for safe display (default: never)
///
/// Strips ANSI escape sequences and additionally substitutes terminal-active
/// control bytes and bidi / zero-width codepoints with U+FFFD.
pub fn sanitize(&mut self, mode: StripAnsiMode) -> &mut Self {
self.config.sanitize = mode;
self
}
/// Text wrapping mode (default: do not wrap) /// Text wrapping mode (default: do not wrap)
pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self { pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self {
self.config.wrapping_mode = mode; self.config.wrapping_mode = mode;
+41 -10
View File
@@ -29,7 +29,10 @@ use crate::error::*;
use crate::input::OpenedInput; use crate::input::OpenedInput;
use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult}; use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult};
use crate::output::OutputHandle; use crate::output::OutputHandle;
use crate::preprocessor::{expand_tabs, replace_nonprintable, strip_ansi, strip_overstrike}; use crate::preprocessor::{
expand_tabs, replace_nonprintable, sanitize, sanitize_for_terminal, strip_ansi,
strip_overstrike,
};
use crate::style::StyleComponent; use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::terminal::{as_terminal_escaped, to_ansi_color};
use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator}; use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
@@ -208,6 +211,7 @@ pub(crate) struct InteractivePrinter<'a> {
background_color_highlight: Option<Color>, background_color_highlight: Option<Color>,
consecutive_empty_lines: usize, consecutive_empty_lines: usize,
strip_ansi: bool, strip_ansi: bool,
sanitize: bool,
strip_overstrike: bool, strip_overstrike: bool,
} }
@@ -271,7 +275,9 @@ impl<'a> InteractivePrinter<'a> {
let needs_to_match_syntax = (!is_printing_binary let needs_to_match_syntax = (!is_printing_binary
|| matches!(config.binary, BinaryBehavior::AsText)) || matches!(config.binary, BinaryBehavior::AsText))
&& (config.colored_output || config.strip_ansi == StripAnsiMode::Auto); && (config.colored_output
|| config.strip_ansi == StripAnsiMode::Auto
|| config.sanitize == StripAnsiMode::Auto);
let (is_plain_text, strip_overstrike, highlighter_from_set) = if needs_to_match_syntax { let (is_plain_text, strip_overstrike, highlighter_from_set) = if needs_to_match_syntax {
// Determine the type of syntax for highlighting // Determine the type of syntax for highlighting
@@ -317,6 +323,14 @@ impl<'a> InteractivePrinter<'a> {
_ => false, _ => false,
}; };
let sanitize = match config.sanitize {
_ if config.show_nonprintable => false,
StripAnsiMode::Always => true,
StripAnsiMode::Auto if is_plain_text => false,
StripAnsiMode::Auto => true,
_ => false,
};
Ok(InteractivePrinter { Ok(InteractivePrinter {
panel_width, panel_width,
colors, colors,
@@ -330,6 +344,7 @@ impl<'a> InteractivePrinter<'a> {
background_color_highlight, background_color_highlight,
consecutive_empty_lines: 0, consecutive_empty_lines: 0,
strip_ansi, strip_ansi,
sanitize,
strip_overstrike, strip_overstrike,
}) })
} }
@@ -489,7 +504,7 @@ impl Printer for InteractivePrinter<'_> {
(but will be present if the output of 'bat' is piped). You can use 'bat -A' \ (but will be present if the output of 'bat' is piped). You can use 'bat -A' \
to show the binary file contents.", to show the binary file contents.",
Yellow.paint("[bat warning]"), Yellow.paint("[bat warning]"),
input.description.summary(), sanitize_for_terminal(&input.description.summary()),
)?; )?;
} else if self.config.style_components.grid() { } else if self.config.style_components.grid() {
self.print_horizontal_line(handle, '┬')?; self.print_horizontal_line(handle, '┬')?;
@@ -543,9 +558,11 @@ impl Printer for InteractivePrinter<'_> {
"{}{}{mode}", "{}{}{mode}",
description description
.kind() .kind()
.map(|kind| format!("{kind}: ")) .map(|kind| format!("{}: ", sanitize_for_terminal(kind)))
.unwrap_or_else(|| "".into()), .unwrap_or_else(|| "".into()),
self.colors.header_value.paint(description.title()), self.colors
.header_value
.paint(sanitize_for_terminal(description.title())),
); );
self.print_header_multiline_component(handle, &header_filename) self.print_header_multiline_component(handle, &header_filename)
} }
@@ -599,11 +616,23 @@ impl Printer for InteractivePrinter<'_> {
let title = "8<"; let title = "8<";
let title_count = title.chars().count(); let title_count = title.chars().count();
let snip_left = "".repeat((self.config.term_width - panel_count - (title_count / 2)) / 4); let snip_left = "".repeat(
self.config
.term_width
.saturating_sub(panel_count)
.saturating_sub(title_count / 2)
/ 4,
);
let snip_left_count = snip_left.chars().count(); // Can't use .len() with Unicode. let snip_left_count = snip_left.chars().count(); // Can't use .len() with Unicode.
let snip_right = let snip_right = "".repeat(
"".repeat((self.config.term_width - panel_count - snip_left_count - title_count) / 2); self.config
.term_width
.saturating_sub(panel_count)
.saturating_sub(snip_left_count)
.saturating_sub(title_count)
/ 2,
);
writeln!( writeln!(
handle, handle,
@@ -659,8 +688,10 @@ impl Printer for InteractivePrinter<'_> {
} }
} }
// If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting. // Sanitize is the strict superset; otherwise strip-ansi alone.
if self.strip_ansi { if self.sanitize {
line = sanitize(&line).into()
} else if self.strip_ansi {
line = strip_ansi(&line).into() line = strip_ansi(&line).into()
} }
@@ -1,2 +1,2 @@
[mappings] [mappings]
"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets"] "XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets", "*.slnx"]
@@ -1,3 +1,3 @@
# JSON Lines is a simple variation of JSON #2535 # JSON Lines is a simple variation of JSON #2535
[mappings] [mappings]
"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson"] "JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson", "*.code-workspace"]
@@ -0,0 +1,2 @@
[mappings]
"Makefile" = ["justfile", "Justfile", ".justfile", "*.justfile"]
@@ -1,2 +1,2 @@
[mappings] [mappings]
"SSH Config" = ["**/.ssh/config"] "SSH Config" = ["**/.ssh/**/config"]
@@ -0,0 +1,2 @@
[mappings]
"INI" = ["/etc/dnf/dnf.conf", "/etc/yum.repos.d/*.repo"]
@@ -0,0 +1,2 @@
[mappings]
"INI" = ["**/ghostty/**/*.ghostty", "**/ghostty/themes/*"]
+202 -12
View File
@@ -386,12 +386,23 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
} }
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> { fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
self.chars_take_while(|c| c != '\x1B') self.chars_take_while(|c| !is_sequence_introducer(c))
.map(|(start, end)| EscapeSequenceOffsets::Text { start, end }) .map(|(start, end)| EscapeSequenceOffsets::Text { start, end })
} }
fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> { fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> {
let (start_sequence, c) = self.chars.next().expect("to not be finished"); let (start_sequence, c) = self.chars.next().expect("to not be finished");
// Handle 8-bit C1 introducers as their 7-bit `ESC <x>` equivalents.
match c {
'\u{9B}' => return self.next_csi_body(start_sequence),
'\u{9D}' => return self.next_osc_body(start_sequence),
'\u{90}' | '\u{98}' | '\u{9E}' | '\u{9F}' => {
return self.next_string_terminated_body(start_sequence)
}
_ => {}
}
match self.chars.peek() { match self.chars.peek() {
None => Some(EscapeSequenceOffsets::Unknown { None => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence, start: start_sequence,
@@ -400,12 +411,24 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((_, ']')) => self.next_osc(start_sequence), Some((_, ']')) => self.next_osc(start_sequence),
Some((_, '[')) => self.next_csi(start_sequence), Some((_, '[')) => self.next_csi(start_sequence),
Some((i, c)) => match c { // 7-bit DCS/SOS/PM/APC: `ESC P/X/^/_` introduces a string body.
Some((_, 'P' | 'X' | '^' | '_')) => {
self.chars.next();
self.next_string_terminated_body(start_sequence)
}
Some(&(i, c)) => match c {
'\x20'..='\x2F' => self.next_nf(start_sequence), '\x20'..='\x2F' => self.next_nf(start_sequence),
c => Some(EscapeSequenceOffsets::Unknown { c => {
// Single-byte ESC sequence (RIS, DECSC/DECRC, keypad, VT52 etc.).
let end = match self.chars.next() {
Some((j, fc)) => j + fc.len_utf8(),
None => i + c.len_utf8(),
};
Some(EscapeSequenceOffsets::Unknown {
start: start_sequence, start: start_sequence,
end: i + c.len_utf8(), end,
}), })
}
}, },
} }
} }
@@ -413,12 +436,27 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished"); let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(osc_open_char, ']'); debug_assert_eq!(osc_open_char, ']');
let start_command = osc_open_index + osc_open_char.len_utf8();
Some(self.read_osc_body(start_sequence, start_command))
}
/// OSC body parser entered after the 8-bit introducer U+009D was consumed.
fn next_osc_body(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let start_command = start_sequence + '\u{9D}'.len_utf8();
Some(self.read_osc_body(start_sequence, start_command))
}
fn read_osc_body(
&mut self,
start_sequence: usize,
start_command: usize,
) -> EscapeSequenceOffsets {
let mut start_terminator: usize; let mut start_terminator: usize;
let mut end_sequence: usize; let mut end_sequence: usize;
loop { loop {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) { // ST is BEL, ESC `\\`, or U+009C.
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B' | '\u{9C}')) {
None => { None => {
start_terminator = self.text.len(); start_terminator = self.text.len();
end_sequence = start_terminator; end_sequence = start_terminator;
@@ -437,6 +475,11 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
break; break;
} }
Some((ti, '\u{9C}')) => {
end_sequence = ti + '\u{9C}'.len_utf8();
break;
}
Some((ti, '\x1B')) => { Some((ti, '\x1B')) => {
match self.chars.next() { match self.chars.next() {
Some((i, '\\')) => { Some((i, '\\')) => {
@@ -466,20 +509,82 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
} }
} }
Some(EscapeSequenceOffsets::OSC { EscapeSequenceOffsets::OSC {
start_sequence, start_sequence,
start_command: osc_open_index + osc_open_char.len_utf8(), start_command,
start_terminator, start_terminator,
end: end_sequence, end: end_sequence,
}
}
/// DCS/SOS/PM/APC body parser. Emitted as `Unknown` so the body is stripped.
fn next_string_terminated_body(
&mut self,
start_sequence: usize,
) -> Option<EscapeSequenceOffsets> {
let mut end_sequence: usize;
loop {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B' | '\u{9C}')) {
None => {
end_sequence = self.text.len();
break;
}
Some((_, end)) => {
end_sequence = end;
}
}
match self.chars.next() {
Some((ti, '\x07')) => {
end_sequence = ti + '\x07'.len_utf8();
break;
}
Some((ti, '\u{9C}')) => {
end_sequence = ti + '\u{9C}'.len_utf8();
break;
}
Some((ti, '\x1B')) => match self.chars.next() {
Some((i, '\\')) => {
end_sequence = i + '\\'.len_utf8();
break;
}
None => {
end_sequence = ti + '\x1B'.len_utf8();
break;
}
_ => {}
},
None => break,
Some((_, tc)) => {
panic!("this should not be reached: char {tc:?}")
}
}
}
Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end: end_sequence,
}) })
} }
fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished"); let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(csi_open_char, '['); debug_assert_eq!(csi_open_char, '[');
Some(self.read_csi_body(start_sequence, csi_open_index + csi_open_char.len_utf8()))
}
let start_parameters: usize = csi_open_index + csi_open_char.len_utf8(); /// CSI body parser entered after the 8-bit introducer U+009B was consumed.
fn next_csi_body(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let start_parameters = start_sequence + '\u{9B}'.len_utf8();
Some(self.read_csi_body(start_sequence, start_parameters))
}
fn read_csi_body(
&mut self,
start_sequence: usize,
start_parameters: usize,
) -> EscapeSequenceOffsets {
// Keep iterating while within the range of `0x30-0x3F`. // Keep iterating while within the range of `0x30-0x3F`.
let mut start_intermediates: usize = start_parameters; let mut start_intermediates: usize = start_parameters;
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) { if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) {
@@ -498,13 +603,13 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((i, c)) => i + c.len_utf8(), Some((i, c)) => i + c.len_utf8(),
}; };
Some(EscapeSequenceOffsets::CSI { EscapeSequenceOffsets::CSI {
start_sequence, start_sequence,
start_parameters, start_parameters,
start_intermediates, start_intermediates,
start_final_byte, start_final_byte,
end: end_of_sequence, end: end_of_sequence,
}) }
} }
fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
@@ -543,13 +648,25 @@ impl Iterator for EscapeSequenceOffsetsIterator<'_> {
type Item = EscapeSequenceOffsets; type Item = EscapeSequenceOffsets;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self.chars.peek() { match self.chars.peek() {
Some((_, '\x1B')) => self.next_sequence(), Some((_, c)) if is_sequence_introducer(*c) => self.next_sequence(),
Some((_, _)) => self.next_text(), Some((_, _)) => self.next_text(),
None => None, None => None,
} }
} }
} }
/// True for ESC and the 8-bit C1 sequence introducers (DCS/SOS/CSI/OSC/PM/APC).
#[inline]
fn is_sequence_introducer(c: char) -> bool {
if (c as u32) < 0x80 {
return c == '\x1B';
}
matches!(
c,
'\u{90}' | '\u{98}' | '\u{9B}' | '\u{9D}' | '\u{9E}' | '\u{9F}'
)
}
/// An iterator over ANSI/VT escape sequences within a string. /// An iterator over ANSI/VT escape sequences within a string.
/// ///
/// ## Example /// ## Example
@@ -717,6 +834,79 @@ mod tests {
); );
} }
#[test]
fn test_escape_sequence_offsets_iterator_parses_8bit_csi() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{9B}31m");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 4,
start_final_byte: 4,
end: 5,
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_8bit_osc_with_8bit_st() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{9D}0;title\u{9C}");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::OSC {
start_sequence: 0,
start_command: 2,
start_terminator: 9,
end: 11,
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_7bit_dcs_consumes_body() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1BP1;0;|payload\x1B\\rest");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown { start: 0, end: 16 })
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 16, end: 20 })
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_8bit_dcs_consumes_body() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{90}body\u{9C}rest");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown { start: 0, end: 8 })
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 8, end: 12 })
);
}
#[test]
fn test_escape_sequence_offsets_iterator_truncated_dcs_consumes_to_eof() {
// Unterminated DCS must still be consumed, not emitted as `Text`.
let input = "\x1BPno-terminator";
let mut iter = EscapeSequenceOffsetsIterator::new(input);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown {
start: 0,
end: input.len(),
})
);
assert_eq!(iter.next(), None);
}
#[test] #[test]
fn test_escape_sequence_offsets_iterator_parses_csi() { fn test_escape_sequence_offsets_iterator_parses_csi() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m"); let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m");
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
NAME="${0##*/}"
for i in {1..3}; do
echo "hello $NAME $i"
done
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/expect -f
set timeout 30
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env tclsh
puts "Hello from tclsh"
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/wish
button .b -text "Click"
+309 -1
View File
@@ -305,6 +305,19 @@ fn line_range_multiple() {
.stdout("line 1\nline 2\nline 4\n"); .stdout("line 1\nline 2\nline 4\n");
} }
#[test]
fn snip_at_terminal_width_one_does_not_panic() {
bat()
.arg("multiline.txt")
.arg("--style=snip")
.arg("--color=always")
.arg("--terminal-width=1")
.arg("--line-range=1:2")
.arg("--line-range=4:4")
.assert()
.success();
}
#[test] #[test]
fn line_range_multiple_with_context() { fn line_range_multiple_with_context() {
bat() bat()
@@ -613,12 +626,18 @@ fn list_themes_to_piped_output() {
} }
#[test] #[test]
#[serial]
fn list_languages() { fn list_languages() {
mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
bat() bat()
.env("PAGER", mocked_pagers::from("echo pager-output"))
.arg("--list-languages") .arg("--list-languages")
.arg("--paging=never")
.assert() .assert()
.success() .success()
.stdout(predicate::str::contains("Rust").normalize()); .stdout(predicate::str::contains("Rust").normalize())
.stdout(predicate::str::contains("pager-output").not());
});
} }
#[test] #[test]
@@ -1047,6 +1066,57 @@ fn tabs_4_arg_overrides_env_noconfig() {
); );
} }
#[test]
fn terminal_width_env_var_is_respected() {
let tmp_dir = tempdir().expect("can create temporary directory");
let tmp_path = tmp_dir.path().join("long.txt");
std::fs::write(
&tmp_path,
"0123456789abcdef0123456789abcdef0123456789abcdef\n",
)
.expect("can write temporary file");
bat()
.env("BAT_WIDTH", "20")
.arg(&tmp_path)
.arg("--paging=never")
.arg("--color=never")
.arg("--style=numbers")
.arg("--decorations=always")
.arg("--wrap=character")
.assert()
.success()
.stdout(" 1 0123456789abcde\n f0123456789abcd\n ef0123456789abc\n def\n")
.stderr("");
}
#[test]
fn terminal_width_arg_overrides_env() {
let tmp_dir = tempdir().expect("can create temporary directory");
let tmp_path = tmp_dir.path().join("long.txt");
std::fs::write(
&tmp_path,
"0123456789abcdef0123456789abcdef0123456789abcdef\n",
)
.expect("can write temporary file");
bat()
.env("BAT_WIDTH", "20")
.arg(&tmp_path)
.arg("--paging=never")
.arg("--color=never")
.arg("--style=numbers")
.arg("--decorations=always")
.arg("--wrap=character")
.arg("--terminal-width=10")
.assert()
.success()
.stdout(
" 1 01234\n 56789\n abcde\n f0123\n 45678\n 9abcd\n ef012\n 34567\n 89abc\n def\n",
)
.stderr("");
}
#[test] #[test]
fn fail_non_existing() { fn fail_non_existing() {
bat().arg("non-existing-file").assert().failure(); bat().arg("non-existing-file").assert().failure();
@@ -1474,6 +1544,7 @@ fn diagnostic_sanity_check() {
.assert() .assert()
.success() .success()
.stdout(predicate::str::contains("BAT_PAGER=")) .stdout(predicate::str::contains("BAT_PAGER="))
.stdout(predicate::str::contains("BAT_WIDTH="))
.stderr(""); .stderr("");
} }
@@ -2093,6 +2164,24 @@ fn header_binary() {
.stderr(""); .stderr("");
} }
#[test]
fn header_zip_file_is_binary() {
let tmp_dir = tempdir().expect("can create temporary directory");
let tmp_path = tmp_dir.path().join("test.zip");
std::fs::write(&tmp_path, b"PK\x03\x04hello").expect("can write temporary file");
bat()
.arg(&tmp_path)
.arg("--decorations=always")
.arg("--style=header")
.arg("-r=0:0")
.arg("--file-name=test.zip")
.assert()
.success()
.stdout("File: test.zip <BINARY>\n")
.stderr("");
}
#[test] #[test]
fn header_full_binary() { fn header_full_binary() {
bat() bat()
@@ -3761,6 +3850,158 @@ fn strip_ansi_auto_does_not_strip_ansi_when_plain_text_by_option() {
assert!(output.contains("\x1B[33mYellow")) assert!(output.contains("\x1B[33mYellow"))
} }
#[test]
fn sanitize_implies_strip_ansi() {
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("\x1B[33mYellow\x1B[m")
.assert()
.success()
.stdout("Yellow");
}
#[test]
fn sanitize_strips_osc_clipboard_hijack() {
// OSC 52 sets the system clipboard. A file containing this would silently
// overwrite the user's clipboard if displayed unfiltered.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("safe\x1B]52;c;cm0=\x07payload")
.assert()
.success()
.stdout("safepayload");
}
#[test]
fn sanitize_strips_osc_8_hyperlink_spoof() {
// OSC 8 hyperlinks let displayed text point to an arbitrary URL.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("\x1B]8;;https://evil.example\x07click here\x1B]8;;\x07")
.assert()
.success()
.stdout("click here");
}
#[test]
fn sanitize_strips_window_title_injection() {
// OSC 0/1/2 set the terminal window title.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("hello\x1B]0;evil-title\x07world")
.assert()
.success()
.stdout("helloworld");
}
#[test]
fn sanitize_strips_8bit_csi() {
// 8-bit CSI introducer (U+009B) is the single-codepoint equivalent of ESC [.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("a\u{9B}31mRED\u{9B}0mb")
.assert()
.success()
.stdout("aREDb");
}
#[test]
fn sanitize_substitutes_bare_cr() {
// Bare CR (not part of CRLF) is the line-overwrite forgery vector.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("safe\rEVIL")
.assert()
.success()
.stdout("safe\u{FFFD}EVIL");
}
#[test]
fn sanitize_preserves_crlf() {
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("line1\r\nline2\r\n")
.assert()
.success()
.stdout("line1\r\nline2\r\n");
}
#[test]
fn sanitize_substitutes_bidi_controls() {
// Trojan-Source attack (CVE-2021-42574): U+202E (RLO) reorders display.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("admin\u{202E}check")
.assert()
.success()
.stdout("admin\u{FFFD}check");
}
#[test]
fn sanitize_substitutes_zero_width() {
// Zero-width chars allow invisible content / identifier confusion.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("ad\u{200B}min")
.assert()
.success()
.stdout("ad\u{FFFD}min");
}
#[test]
fn sanitize_preserves_form_feed_in_source() {
// FF (U+000C) is used as a section separator in C source and Emacs Lisp.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("section1\x0Csection2")
.assert()
.success()
.stdout("section1\x0Csection2");
}
#[test]
fn sanitize_preserves_unicode_text() {
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("snowman ☃ CJK 漢字 emoji 🦀")
.assert()
.success()
.stdout("snowman ☃ CJK 漢字 emoji 🦀");
}
// Tests that style components can be removed with `-component`. // Tests that style components can be removed with `-component`.
#[test] #[test]
fn style_components_can_be_removed() { fn style_components_can_be_removed() {
@@ -4132,3 +4373,70 @@ fn plain_without_diff_still_works() {
.success() .success()
.stdout("line 1\nline 2 modified\nline 3\nline 4 added\n"); .stdout("line 1\nline 2 modified\nline 3\nline 4 added\n");
} }
#[test]
fn tcl_shebang_detection_tclsh() {
bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--decorations=always")
.arg("regression_tests/issue_3647_tclsh")
.assert()
.success();
}
#[test]
fn tcl_shebang_detection_wish() {
bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--decorations=always")
.arg("regression_tests/issue_3647_wish")
.assert()
.success();
}
#[test]
fn tcl_shebang_detection_expect() {
bat()
.arg("--color=always")
.arg("--style=plain")
.arg("--decorations=always")
.arg("regression_tests/issue_3647_expect")
.assert()
.success();
}
#[test]
fn ignored_suffix_enables_first_line_detection() {
// A shebang shell script saved with a `.txt` extension is Plain Text by
// default (the extension wins). With `--ignored-suffix .txt` the suffix is
// stripped before detection, so it falls back to the first line and is
// highlighted exactly as if its language were forced to bash. See #2745.
let fixture = "regression_tests/issue_2745.txt";
let common = ["--color=always", "--decorations=never", "--style=plain"];
let stdout = |args: &[&str]| -> Vec<u8> {
let assert = bat()
.args(common)
.args(args)
.arg(fixture)
.assert()
.success();
assert.get_output().stdout.clone()
};
let forced_bash = stdout(&["--language", "bash"]);
let with_ignored_suffix = stdout(&["--ignored-suffix", ".txt"]);
let default = stdout(&[]);
// The fixture really is being highlighted (forcing bash is not a no-op).
assert!(
forced_bash.windows(2).any(|w| w == b"\x1b["),
"forced-bash output should contain ANSI color codes"
);
// With the ignored suffix, detection matches forced bash highlighting...
assert_eq!(with_ignored_suffix, forced_bash);
// ...while the default (extension wins) stays plain and differs.
assert_ne!(default, forced_bash);
}
Vendored Executable → Regular
+12 -10
View File
@@ -4,6 +4,7 @@ import itertools
import subprocess import subprocess
import pathlib import pathlib
import shutil import shutil
from typing import Iterable
def generate_snapshots(): def generate_snapshots():
@@ -19,22 +20,23 @@ def generate_snapshots():
def generate_style_snapshot(style): def generate_style_snapshot(style):
generate_snapshot(style.replace(",", "_"), "--style={}".format(style)) generate_snapshot(style.replace(",", "_"), ["--style={}".format(style)])
def generate_snapshot(name, arguments): def generate_snapshot(name: str, arguments: Iterable[str]):
command = "cargo run -- --paging=never --color=never --decorations=always " output_file = "output/{name}.snapshot.txt".format(name=name)
command += "{args} sample.rs > output/{name}.snapshot.txt".format( command = [
name=name, "cargo", "run", "--", "--paging=never", "--color=never",
args=arguments "--decorations=always", *arguments, "sample.rs"
) ]
print("generating snapshot for {}".format(name)) print("generating snapshot for {}".format(name))
subprocess.call(command, shell=True) with open(output_file, "w") as f:
subprocess.call(command, stdout=f)
def build_bat(): def build_bat():
print("building bat") print("building bat")
subprocess.call("cargo build", cwd="../..", shell=True) subprocess.call(["cargo", "build"], cwd="../..")
def prepare_output_dir(): def prepare_output_dir():
@@ -49,7 +51,7 @@ def modify_sample_file():
def undo_sample_file_modification(): def undo_sample_file_modification():
print("undoing sample.rs modifications") print("undoing sample.rs modifications")
subprocess.call("git checkout -- sample.rs", shell=True) subprocess.call(["git", "checkout", "--", "sample.rs"])
build_bat() build_bat()
+25
View File
@@ -0,0 +1,25 @@
(logging) {
 log {
 output file /var/log/caddy.log
 }
}
# a comment
localhost:8080, example.com {
 root * /var/www/site
 file_server
 reverse_proxy /.well-known/matrix/* localhost:8008 {
 header_up Host {upstream_hostport}
 }
 import logging
}
www.example.com {
 redir https://example.com{uri} permanent
 import logging
}
status.example.com {
 reverse_proxy localhost:3002
 import logging
}
+55 -55
View File
@@ -1,85 +1,85 @@
import kotlin.math.* import kotlin.math.*
data class Example( data class Example(
 val name: String,  val name: String,
 val numbers: List<Int?>  val numbers: List<Int?>
) )
fun interface JokeInterface { fun interface JokeInterface {
 fun isFunny(): Boolean  fun isFunny(): Boolean
} }
abstract class AbstractJoke : JokeInterface { abstract class AbstractJoke : JokeInterface {
 override fun isFunny() = false  override fun isFunny() = false
 abstract fun content(): String  abstract fun content(): String
} }
class Joke : AbstractJoke() { class Joke : AbstractJoke() {
 override fun isFunny(): Boolean {  override fun isFunny(): Boolean {
 return true  return true
 }  }
 override fun content(): String = "content of joke here, haha"  override fun content(): String = "content of joke here, haha"
} }
class DelegatedJoke(val joke: Joke) : JokeInterface by joke { class DelegatedJoke(val joke: Joke) : JokeInterface by joke {
 val number: Long = 123L  val number: Long = 123L
 companion object {  companion object {
 const val someConstant = "some constant text"  const val someConstant = "some constant text"
 }  }
} }
object SomeSingleton object SomeSingleton
sealed class Shape { sealed class Shape {
 abstract fun area(): Double  abstract fun area(): Double
} }
data class Square(val sideLength: Double) : Shape() { data class Square(val sideLength: Double) : Shape() {
 override fun area(): Double = sideLength.pow(2)  override fun area(): Double = sideLength.pow(2)
} }
object Point : Shape() { object Point : Shape() {
 override fun area() = .0  override fun area() = .0
} }
class Circle(val radius: Double) : Shape() { class Circle(val radius: Double) : Shape() {
 override fun area(): Double {  override fun area(): Double {
 return PI * radius * radius  return PI * radius * radius
 }  }
} }
fun String.extensionMethod() = "test" fun String.extensionMethod() = "test"
fun main() { fun main() {
 val name = """  val name = """
 multiline  multiline
 string  string
   
 some numbers: 123123 42  some numbers: 123123 42
 """.trimIndent()  """.trimIndent()
 val example = Example(name = name, numbers = listOf(512, 42, null, -1))  val example = Example(name = name, numbers = listOf(512, 42, null, -1))
 example.numbers  example.numbers
 .filterNotNull()  .filterNotNull()
 .forEach { println(it) }  .forEach { println(it) }
 setOf(Joke(), DelegatedJoke(Joke()).joke)  setOf(Joke(), DelegatedJoke(Joke()).joke)
 .filter(JokeInterface::isFunny)  .filter(JokeInterface::isFunny)
 .map(AbstractJoke::content)  .map(AbstractJoke::content)
 .forEachIndexed { index: Int, joke ->  .forEachIndexed { index: Int, joke ->
 println("I heard a funny joke(#${index + 1}): $joke")  println("I heard a funny joke(#${index + 1}): $joke")
 }  }
 listOf(Square(12.3), Point, Circle(5.2))  listOf(Square(12.3), Point, Circle(5.2))
 .associateWith(Shape::area)  .associateWith(Shape::area)
 .toList()  .toList()
 .sortedBy { it.second }  .sortedBy { it.second }
 .forEach {  .forEach {
 println("${it.first}: ${it.second}")  println("${it.first}: ${it.second}")
 }  }
 println("some string".extensionMethod())  println("some string".extensionMethod())
 require(SomeSingleton::class.simpleName == "SomeSingletonName") { "something does not seem right..." }  require(SomeSingleton::class.simpleName == "SomeSingletonName") { "something does not seem right..." }
} }
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/expect -f
# Expect script detected via expect shebang
set timeout 30
spawn ssh user@host
expect "password:"
send "secret\r"
expect eof
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env tclsh
# Tcl script detected via tclsh shebang
puts "Hello from tclsh"
set x 42
if {$x > 0} {
 puts "positive"
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/wish
# Tk script detected via wish shebang
package require Tk
button .b -text "Click" -command {puts "clicked"}
pack .b
+7
View File
@@ -0,0 +1,7 @@
<Solution>
 <Folder Name="/Build/">
 <File Path="Directory.Build.props" />
 <File Path="projectname.targets" />
 </Folder>
 <Project Path="console.csproj" />
</Solution>
+25
View File
@@ -0,0 +1,25 @@
(logging) {
log {
output file /var/log/caddy.log
}
}
# a comment
localhost:8080, example.com {
root * /var/www/site
file_server
reverse_proxy /.well-known/matrix/* localhost:8008 {
header_up Host {upstream_hostport}
}
import logging
}
www.example.com {
redir https://example.com{uri} permanent
import logging
}
status.example.com {
reverse_proxy localhost:3002
import logging
}
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/expect -f
# Expect script detected via expect shebang
set timeout 30
spawn ssh user@host
expect "password:"
send "secret\r"
expect eof
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env tclsh
# Tcl script detected via tclsh shebang
puts "Hello from tclsh"
set x 42
if {$x > 0} {
puts "positive"
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/wish
# Tk script detected via wish shebang
package require Tk
button .b -text "Click" -command {puts "clicked"}
pack .b
+7
View File
@@ -0,0 +1,7 @@
<Solution>
<Folder Name="/Build/">
<File Path="Directory.Build.props" />
<File Path="projectname.targets" />
</Folder>
<Project Path="console.csproj" />
</Solution>
+37 -28
View File
@@ -1,15 +1,17 @@
use gix::actor::SignatureRef;
use gix::bstr::BString;
use gix::bstr::ByteSlice;
use gix::date::time::Format;
use gix::date::Time;
use gix::objs::tree;
use std::env; use std::env;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use tempfile::TempDir; use tempfile::TempDir;
use git2::build::CheckoutBuilder;
use git2::Repository;
use git2::Signature;
pub struct BatTester { pub struct BatTester {
/// Temporary working directory /// Temporary working directory
temp_dir: TempDir, temp_dir: TempDir,
@@ -59,35 +61,42 @@ impl Default for BatTester {
} }
} }
fn create_sample_directory() -> Result<TempDir, git2::Error> { fn create_sample_directory() -> Result<TempDir, Box<dyn std::error::Error>> {
// Create temp directory and initialize repository // Create temp directory and initialize repository
let temp_dir = TempDir::new().expect("Temp directory"); let temp_dir = TempDir::new().expect("Temp directory");
let repo = Repository::init(&temp_dir)?; let repo = gix::init(&temp_dir)?;
let mut tree = gix::objs::Tree::empty();
// Copy over `sample.rs` // Create sample.rs from snapshot file
let sample_path = temp_dir.path().join("sample.rs"); let blob_id = repo.write_blob_stream(File::open("tests/snapshots/sample.rs")?)?;
println!("{sample_path:?}"); let entry = tree::Entry {
fs::copy("tests/snapshots/sample.rs", &sample_path).expect("successful copy"); mode: tree::EntryMode::from(tree::EntryKind::Blob),
oid: blob_id.object()?.id,
filename: BString::from("sample.rs"),
};
tree.entries.push(entry);
let tree_id = repo.write_object(tree)?;
// Commit let author = SignatureRef {
let mut index = repo.index()?; name: "test".as_bytes().as_bstr(),
index.add_path(Path::new("sample.rs"))?; email: "test@test.test".as_bytes().as_bstr(),
time: &Time::now_local_or_utc().format_or_unix(Format::Raw),
let oid = index.write_tree()?; };
let signature = Signature::now("bat test runner", "bat@test.runner")?; let commit_id = repo.commit_as(
let tree = repo.find_tree(oid)?; author,
let _ = repo.commit( author,
Some("HEAD"), // point HEAD to our new commit "HEAD",
&signature, // author
&signature, // committer
"initial commit", "initial commit",
&tree, tree_id,
&[], gix::commit::NO_PARENT_IDS,
); )?;
let mut opts = CheckoutBuilder::new(); assert_eq!(commit_id, repo.head_id()?);
repo.checkout_head(Some(opts.force()))?;
fs::copy("tests/snapshots/sample.modified.rs", &sample_path).expect("successful copy"); fs::copy(
"tests/snapshots/sample.modified.rs",
temp_dir.path().join("sample.rs"),
)
.expect("successful copy");
Ok(temp_dir) Ok(temp_dir)
} }