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

Compare commits

..

1 Commits

Author SHA1 Message Date
Keith Hall 410e284322 Ignore --wrap=character when output is not interactive
unless it was explicitly provided on the command line as opposed to in a config file / BAT_OPTS env var
2026-03-31 22:51:47 +03:00
64 changed files with 1472 additions and 3346 deletions
-11
View File
@@ -1,11 +0,0 @@
# 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"]
+3 -6
View File
@@ -49,6 +49,9 @@
[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
@@ -278,9 +281,3 @@
[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,39 +3,21 @@
## 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)
@@ -50,33 +32,21 @@
- 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
+680 -1580
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -31,7 +31,8 @@ minimal-application = [
"regex-onig", "regex-onig",
"wild", "wild",
] ]
git = ["gix"] # Support indicating git modifications git = ["git2"] # 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"]
@@ -51,11 +52,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.7", optional = true, features = [ minus = { version = "5.6", optional = true, features = [
"dynamic_output", "dynamic_output",
"search", "search",
] } ] }
unicode-width = "0.2.2" unicode-width = "0.2.1"
globset = "0.4" globset = "0.4"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
@@ -63,23 +64,22 @@ 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.6.0", optional = true } bugreport = { version = "0.5.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.3", optional = true } regex = { version = "1.12.2", 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.3.0", optional = true } execute = { version = "0.2.15", optional = true }
terminal-colorsaurus = "1.0" terminal-colorsaurus = "1.0"
unicode-segmentation = "1.13.2" unicode-segmentation = "1.12.0"
itertools = "0.14.0" itertools = "0.14.0"
[dependencies.gix] [dependencies.git2]
version = "0.86" version = "0.20"
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.6.1" version = "4.5.60"
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.9.0" plist = "1.7.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.4" predicates = "3.1.3"
wait-timeout = "0.2.1" wait-timeout = "0.2.1"
tempfile = "3.27.0" tempfile = "3.23.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.14.0", features = ["serde"] } indexmap = { version = "2.13.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.45" quote = "1.0.40"
regex = "1.12.3" regex = "1.12.2"
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 = "1.1.1", features = ["preserve_order"] } toml = { version = "0.9.8", features = ["preserve_order"] }
walkdir = "2.5" walkdir = "2.5"
[build-dependencies.clap] [build-dependencies.clap]
version = "4.6.1" version = "4.5.60"
optional = true optional = true
features = ["wrap_help", "cargo"] features = ["wrap_help", "cargo"]
+21 -29
View File
@@ -19,6 +19,20 @@
[<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
@@ -194,6 +208,12 @@ 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`
@@ -307,7 +327,7 @@ pacman -S bat
### On Fedora ### On Fedora
You can install [the `bat` package](https://packages.fedoraproject.org/pkgs/rust-bat/bat/) from the official sources: 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.
```bash ```bash
dnf install bat dnf install bat
@@ -507,30 +527,6 @@ 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.
@@ -843,10 +839,6 @@ 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.
+2 -8
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}} --no-paging --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object $themes = {{PROJECT_EXECUTABLE}} --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object
return $themes return $themes
} }
function Get-MyLanguages(){ function Get-MyLanguages(){
$themes = {{PROJECT_EXECUTABLE}} --no-paging --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object $themes = {{PROJECT_EXECUTABLE}} --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object
return $themes return $themes
} }
@@ -100,11 +100,6 @@ 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, $_)}
@@ -163,7 +158,6 @@ 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'').')
+3 -8
View File
@@ -80,7 +80,7 @@ _bat() {
-l | --language) -l | --language)
local IFS=$'\n' local IFS=$'\n'
COMPREPLY=($(compgen -W "$( COMPREPLY=($(compgen -W "$(
"$1" --no-paging --list-languages | while IFS=: read -r lang _; do "$1" --list-languages | while IFS=: read -r lang _; do
printf "%s\n" "$lang" printf "%s\n" "$lang"
done done
)" -- "$cur")) )" -- "$cur"))
@@ -132,10 +132,6 @@ _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
@@ -154,14 +150,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" --no-paging --list-themes)" -- "$cur")) COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --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" --no-paging --list-themes)" -- "$cur")) COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur"))
__bat_escape_completions __bat_escape_completions
return 0 return 0
;; ;;
@@ -225,7 +221,6 @@ _bat() {
--list-themes --list-themes
--squeeze-blank --squeeze-blank
--squeeze-limit --squeeze-limit
--sanitize
--strip-ansi --strip-ansi
--style --style
--line-range --line-range
+5 -6
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 --no-paging --list-languages | string split -f1 : | string match -e "$comp" command $bat --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 --no-paging --list-languages) for spec in (command $bat --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,7 +221,6 @@ 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
@@ -235,11 +234,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 --no-paging --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 --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 --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-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-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 -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 -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
+3 -9
View File
@@ -54,7 +54,6 @@ _{{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'
@@ -91,25 +90,20 @@ _{{PROJECT_EXECUTABLE}}_main() {
languages) languages)
local IFS=$'\n' local IFS=$'\n'
local -a languages local -a languages
# `--list-languages` emits one `name:matchers` line per language, languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') )
# 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}} --no-paging --color=never --decorations=never --list-themes)"} ) themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --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}} --no-paging --color=never --decorations=never --list-themes)"} ) themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} )
_wanted themes expl 'theme' compadd -a themes && ret=0 _wanted themes expl 'theme' compadd -a themes && ret=0
;; ;;
-9
View File
@@ -2,15 +2,6 @@ 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
@@ -1,12 +0,0 @@
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
@@ -0,0 +1,398 @@
%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-Definition as it is not https://github.com/seanjames777/SML-Language-Definitiona 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
+1 -10
View File
@@ -77,8 +77,7 @@ 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. This can also be configured via the BAT_WIDTH environment variable (e.g. export width. See also: '--wrap'.
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'
@@ -164,14 +163,6 @@ 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,3 +10,5 @@ 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.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+10 -58
View File
@@ -158,9 +158,7 @@ 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( return Err(Error::UndetectedSyntax(path.to_string_lossy().into()));
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 {
@@ -177,17 +175,13 @@ impl HighlightingAssets {
) { ) {
(Some(syntax), _) => Ok(syntax), (Some(syntax), _) => Ok(syntax),
(_, Some(MappingTarget::MapExtensionToUnknown)) => Err(Error::UndetectedSyntax( (_, Some(MappingTarget::MapExtensionToUnknown)) => {
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()), Err(Error::UndetectedSyntax(path.to_string_lossy().into()))
)), }
_ => 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(|| { .ok_or_else(|| Error::UndetectedSyntax(path.to_string_lossy().into())),
Error::UndetectedSyntax(crate::preprocessor::sanitize_for_terminal(
&path.to_string_lossy(),
))
}),
} }
} }
@@ -347,15 +341,15 @@ impl HighlightingAssets {
file_name: &OsStr, file_name: &OsStr,
ignored_suffixes: &IgnoredSuffixes, ignored_suffixes: &IgnoredSuffixes,
) -> Result<Option<SyntaxReferenceInSet<'_>>> { ) -> Result<Option<SyntaxReferenceInSet<'_>>> {
let stripped = let mut syntax = self.find_syntax_by_extension(Path::new(file_name).extension())?;
if syntax.is_none() {
syntax =
ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| { 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(
@@ -686,48 +680,6 @@ 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();
+36 -35
View File
@@ -38,15 +38,6 @@ 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,
@@ -59,6 +50,11 @@ pub struct App {
/// (not from config file or environment variables). /// (not from config file or environment variables).
/// This is used to honor the flag when piping output, similar to `cat -n`. /// This is used to honor the flag when piping output, similar to `cat -n`.
number_from_cli: bool, number_from_cli: bool,
/// True if --wrap=character was passed on the command line
/// (not from config file or environment variables).
/// When piping output (non-interactive), --wrap=character is ignored unless
/// it was explicitly provided on the command line.
wrap_character_from_cli: bool,
} }
impl App { impl App {
@@ -99,6 +95,18 @@ impl App {
false false
}); });
// Check if --wrap=character was passed on the command line
// (before merging with config file and environment variables).
// This is needed to honor --wrap=character when piping output, while
// ignoring it when it comes only from the config file or BAT_OPTS.
let cli_args_vec: Vec<_> = wild::args_os().collect();
let wrap_character_from_cli = cli_args_vec
.iter()
.any(|arg| arg.to_string_lossy() == "--wrap=character")
|| cli_args_vec.windows(2).any(|pair| {
pair[0].to_string_lossy() == "--wrap" && pair[1].to_string_lossy() == "character"
});
let matches = Self::matches(interactive_output)?; let matches = Self::matches(interactive_output)?;
if matches.get_flag("help") { if matches.get_flag("help") {
@@ -139,6 +147,7 @@ impl App {
matches, matches,
interactive_output, interactive_output,
number_from_cli, number_from_cli,
wrap_character_from_cli,
}) })
} }
@@ -417,7 +426,15 @@ impl App {
WrappingMode::NoWrapping(true) WrappingMode::NoWrapping(true)
} else { } else {
match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) { match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) {
Some("character") => WrappingMode::Character, Some("character") => {
if !self.interactive_output && !self.wrap_character_from_cli {
// When piping output (non-interactive), ignore --wrap=character
// unless it was explicitly provided on the command line.
WrappingMode::NoWrapping(false)
} else {
WrappingMode::Character
}
}
Some("word") => WrappingMode::Word, Some("word") => WrappingMode::Word,
Some("never") => WrappingMode::NoWrapping(true), Some("never") => WrappingMode::NoWrapping(true),
Some("auto") | None => { Some("auto") | None => {
@@ -467,32 +484,16 @@ impl App {
4 4
}, },
), ),
strip_ansi: { strip_ansi: match self
let sanitize = parse_strip_ansi_value( .matches
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,
// --sanitize implies --strip-ansi to the same value. Some("always") => StripAnsiMode::Always,
if sanitize != StripAnsiMode::Never { Some("auto") => StripAnsiMode::Auto,
sanitize _ => unreachable!("other values for --strip-ansi are not allowed"),
} 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(),
@@ -670,7 +671,7 @@ impl App {
Ok(styled_components) Ok(styled_components)
} }
pub(crate) fn theme_options(&self) -> ThemeOptions { fn theme_options(&self) -> ThemeOptions {
Self::theme_options_from_matches(&self.matches) Self::theme_options_from_matches(&self.matches)
} }
+2 -24
View File
@@ -240,7 +240,6 @@ 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)
@@ -256,13 +255,10 @@ pub fn build_app(interactive_output: bool) -> Command {
}) })
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
}) })
.help("Explicitly set the width of the terminal instead of determining it automatically.") .help(
.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. This can also be configured \ as an offset to the actual terminal width. See also: '--wrap'.",
via the BAT_WIDTH environment variable (e.g. export BAT_WIDTH=\"100\"). \
See also: '--wrap'.",
), ),
) )
.arg( .arg(
@@ -480,24 +476,6 @@ 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,7 +153,6 @@ 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),
+13 -44
View File
@@ -17,6 +17,7 @@ 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;
@@ -38,7 +39,7 @@ use bat::{
error::*, error::*,
input::Input, input::Input,
style::{StyleComponent, StyleComponents}, style::{StyleComponent, StyleComponents},
theme::{default_theme, theme, ColorScheme, ThemeOptions}, theme::{color_scheme, default_theme, ColorScheme},
MappingTarget, PagingMode, MappingTarget, PagingMode,
}; };
@@ -152,14 +153,7 @@ 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.
// Clamp instead of subtracting: a tiny `--terminal-width` (smaller than the let desired_width = config.term_width - longest - separator.len();
// 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()
@@ -203,7 +197,7 @@ pub fn list_themes(
cfg: &Config, cfg: &Config,
config_dir: &Path, config_dir: &Path,
cache_dir: &Path, cache_dir: &Path,
theme_options: ThemeOptions, detect_color_scheme: DetectColorScheme,
) -> 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();
@@ -212,7 +206,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 = theme(theme_options).to_string(); let default_theme_name = default_theme(color_scheme(detect_color_scheme).unwrap_or_default());
let mut buf = String::new(); let mut buf = String::new();
let mut handle = OutputHandle::FmtWrite(&mut buf); let mut handle = OutputHandle::FmtWrite(&mut buf);
@@ -265,9 +259,7 @@ 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";
// Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC. print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}");
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();
} }
@@ -293,28 +285,7 @@ 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, report::ReportEntry}; use bugreport::{bugreport, collector::*, format::Markdown};
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()),
) )
@@ -336,9 +307,6 @@ 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",
@@ -358,7 +326,6 @@ 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")]
@@ -427,12 +394,14 @@ 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 mut plain_config = config.clone(); let plain_config = Config {
plain_config.style_components = style_components: StyleComponents::new(StyleComponent::Plain.components(false)),
StyleComponents::new(StyleComponent::Plain.components(false)); paging_mode: PagingMode::QuitIfOneScreen,
..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, app.theme_options())?; list_themes(&config, config_dir, cache_dir, DetectColorScheme::default())?;
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,9 +111,6 @@ 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,
+58 -172
View File
@@ -1,13 +1,12 @@
#![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;
#[derive(Copy, Clone, Debug, PartialEq, Eq)] use git2::{DiffOptions, IntoCString, Repository};
#[derive(Copy, Clone, Debug)]
pub enum LineChange { pub enum LineChange {
Added, Added,
RemovedAbove, RemovedAbove,
@@ -17,181 +16,68 @@ 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 filepath_absolute = filename.canonicalize().ok()?; let repo = Repository::discover(filename).ok()?;
let repository = gix::discover(filepath_absolute.parent().ok()?).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(
filepath_absolute.strip_prefix(&repo_path_absolute).ok()?,
));
let index = repository.index_or_load_from_head_or_empty().ok()?;
let index_entry = index.entry_by_path(filepath_relative_to_repo.as_ref())?;
let mut cache = repository
.diff_resource_cache(
Mode::ToGit,
WorktreeRoots {
old_root: None,
new_root: repository.workdir().map(Path::to_path_buf),
},
)
.ok()?;
cache
.set_resource(
index_entry.id,
index_entry.mode.to_tree_entry_mode()?.kind(),
filepath_relative_to_repo.as_ref(),
ResourceKind::OldOrSource,
&repository,
)
.ok()?;
cache
.set_resource(
repository.object_hash().null(),
EntryKind::Blob,
filepath_relative_to_repo.as_ref(),
ResourceKind::NewOrDestination,
&repository,
)
.ok()?;
let diff = gix::diff::blob::diff_with_slider_heuristics(
Algorithm::Histogram,
&cache.prepare_diff().ok()?.interned_input(),
);
collect_changes_from_hunks(diff.hunks()) let repo_path_absolute = fs::canonicalize(repo.workdir()?).ok()?;
let filepath_absolute = fs::canonicalize(filename).ok()?;
let filepath_relative_to_repo = filepath_absolute.strip_prefix(&repo_path_absolute).ok()?;
let mut diff_options = DiffOptions::new();
let pathspec = filepath_relative_to_repo.into_c_string().ok()?;
diff_options.pathspec(pathspec);
diff_options.context_lines(0);
let diff = repo
.diff_index_to_workdir(None, Some(&mut diff_options))
.ok()?;
let mut line_changes: LineChanges = HashMap::new();
let mark_section =
|line_changes: &mut LineChanges, start: u32, end: i32, change: LineChange| {
for line in start..=end as u32 {
line_changes.insert(line, change);
}
};
let _ = diff.foreach(
&mut |_, _| true,
None,
Some(&mut |delta, hunk| {
let path = delta.new_file().path().unwrap_or_else(|| Path::new(""));
if filepath_relative_to_repo != path {
return false;
} }
#[cfg(test)] let old_lines = hunk.old_lines();
mod tests { let new_start = hunk.new_start();
use super::{get_git_diff, LineChange}; let new_lines = hunk.new_lines();
use std::path::{Path, PathBuf}; let new_end = (new_start + new_lines) as i32 - 1;
use std::process::Command;
fn git(repo: &Path, args: &[&str]) { if old_lines == 0 && new_lines > 0 {
let output = Command::new("git") mark_section(&mut line_changes, new_start, new_end, LineChange::Added);
.args(args) } else if new_lines == 0 && old_lines > 0 {
.current_dir(repo) if new_start == 0 {
.output() mark_section(&mut line_changes, 1, 1, LineChange::RemovedAbove);
.expect("git command can run"); } else {
assert!( mark_section(
output.status.success(), &mut line_changes,
"git {args:?} failed\nstdout: {}\nstderr: {}", new_start,
String::from_utf8_lossy(&output.stdout), new_start as i32,
String::from_utf8_lossy(&output.stderr) LineChange::RemovedBelow,
); );
} }
} else {
fn setup_repo() -> tempfile::TempDir { mark_section(&mut line_changes, new_start, new_end, LineChange::Modified);
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 { true
let filepath = repo.path().join(filename); }),
std::fs::write(&filepath, "file\n").expect("can write file"); None,
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); Some(line_changes)
}
} }
+22 -79
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::try_new(stdin)?, reader: InputReader::new(stdin),
}) })
} }
@@ -216,34 +216,34 @@ impl<'a> Input<'a> {
description, description,
metadata: self.metadata, metadata: self.metadata,
reader: { reader: {
let path_display = let mut file = File::open(&path)
crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()); .map_err(|e| format!("'{}': {e}", 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!("'{path_display}' is a directory.").into()); return Err(format!("'{}' is a directory.", path.to_string_lossy()).into());
} }
if let Some(stdout) = stdout_identifier { if let Some(stdout) = stdout_identifier {
let input_identifier = Identifier::try_from(file) let input_identifier = Identifier::try_from(file).map_err(|e| {
.map_err(|e| format!("{path_display}: Error identifying file: {e}"))?; format!("{}: Error identifying file: {e}", path.to_string_lossy())
})?;
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 '{path_display}' is also an output. Aborting to avoid infinite loop.", "IO circle detected. The input from '{}' 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::try_new(BufReader::new(file))? InputReader::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::try_new(BufReader::new(reader))?, reader: InputReader::new(BufReader::new(reader)),
}), }),
} }
} }
@@ -257,29 +257,28 @@ pub(crate) struct InputReader<'a> {
} }
impl<'a> InputReader<'a> { impl<'a> InputReader<'a> {
#[cfg(test)] pub(crate) fn new<R: BufRead + 'a>(mut reader: R) -> InputReader<'a> {
pub(crate) fn new<R: BufRead + 'a>(reader: R) -> InputReader<'a> {
Self::try_new(reader).expect("reading the first line failed")
}
pub(crate) fn try_new<R: BufRead + 'a>(mut reader: R) -> io::Result<InputReader<'a>> {
let mut first_line = vec![]; let mut first_line = vec![];
reader.read_until(b'\n', &mut first_line)?; reader.read_until(b'\n', &mut first_line).ok();
let content_type = inspect_content_type(&first_line); let content_type = if first_line.is_empty() {
None
} else {
Some(content_inspector::inspect(&first_line[..]))
};
if content_type == Some(ContentType::UTF_16LE) { if content_type == Some(ContentType::UTF_16LE) {
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?; read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A).ok();
} else if content_type == Some(ContentType::UTF_16BE) { } else if content_type == Some(ContentType::UTF_16BE) {
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?; read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00).ok();
} }
Ok(InputReader { 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> {
@@ -320,25 +319,6 @@ 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>,
@@ -394,43 +374,6 @@ 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";
+7 -115
View File
@@ -14,53 +14,6 @@ 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,
@@ -134,7 +87,7 @@ impl LessOpenPreprocessor {
None => return input.open(stdin, stdout_identifier), None => return input.open(stdin, stdout_identifier),
}; };
let mut lessopen_command = shell(shell_substitute(&self.lessopen, path_str)); let mut lessopen_command = shell(self.lessopen.replacen("%s", path_str, 1));
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() {
@@ -169,7 +122,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(shell_substitute(&self.lessopen, "-")); let mut lessopen_command = shell(self.lessopen.replacen("%s", "-", 1));
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)
@@ -202,7 +155,7 @@ impl LessOpenPreprocessor {
Ok(OpenedInput { Ok(OpenedInput {
kind, kind,
reader: InputReader::try_new(BufReader::new( reader: InputReader::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,
@@ -228,7 +181,7 @@ impl LessOpenPreprocessor {
lessclose: self lessclose: self
.lessclose .lessclose
.as_ref() .as_ref()
.map(|s| shell_substitute_two(s, &path_str, &stdout)), .map(|s| s.replacen("%s", &path_str, 1).replacen("%s", &stdout, 1)),
} }
} else { } else {
Preprocessed { Preprocessed {
@@ -236,10 +189,10 @@ impl LessOpenPreprocessor {
lessclose: self lessclose: self
.lessclose .lessclose
.as_ref() .as_ref()
.map(|s| shell_substitute_two(s, &path_str, "-")), .map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)),
} }
}, },
))?, )),
metadata: input.metadata, metadata: input.metadata,
description: input.description, description: input.description,
}) })
@@ -308,7 +261,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",)
}; };
} }
@@ -431,65 +384,4 @@ 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,7 +54,6 @@ 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};
+2 -270
View File
@@ -139,140 +139,16 @@ 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:
@@ -365,113 +241,6 @@ 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)
@@ -492,40 +261,3 @@ 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,15 +192,6 @@ 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;
+10 -41
View File
@@ -29,10 +29,7 @@ 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::{ use crate::preprocessor::{expand_tabs, replace_nonprintable, strip_ansi, strip_overstrike};
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};
@@ -211,7 +208,6 @@ 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,
} }
@@ -275,9 +271,7 @@ 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.colored_output || config.strip_ansi == StripAnsiMode::Auto);
|| 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
@@ -323,14 +317,6 @@ 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,
@@ -344,7 +330,6 @@ 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,
}) })
} }
@@ -504,7 +489,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]"),
sanitize_for_terminal(&input.description.summary()), 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, '┬')?;
@@ -558,11 +543,9 @@ impl Printer for InteractivePrinter<'_> {
"{}{}{mode}", "{}{}{mode}",
description description
.kind() .kind()
.map(|kind| format!("{}: ", sanitize_for_terminal(kind))) .map(|kind| format!("{kind}: "))
.unwrap_or_else(|| "".into()), .unwrap_or_else(|| "".into()),
self.colors self.colors.header_value.paint(description.title()),
.header_value
.paint(sanitize_for_terminal(description.title())),
); );
self.print_header_multiline_component(handle, &header_filename) self.print_header_multiline_component(handle, &header_filename)
} }
@@ -616,23 +599,11 @@ 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( let snip_left = "".repeat((self.config.term_width - panel_count - (title_count / 2)) / 4);
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 = "".repeat( let snip_right =
self.config "".repeat((self.config.term_width - panel_count - snip_left_count - title_count) / 2);
.term_width
.saturating_sub(panel_count)
.saturating_sub(snip_left_count)
.saturating_sub(title_count)
/ 2,
);
writeln!( writeln!(
handle, handle,
@@ -688,10 +659,8 @@ impl Printer for InteractivePrinter<'_> {
} }
} }
// Sanitize is the strict superset; otherwise strip-ansi alone. // If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting.
if self.sanitize { if self.strip_ansi {
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", "*.slnx"] "XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets"]
@@ -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", "*.code-workspace"] "JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson"]
@@ -1,2 +0,0 @@
[mappings]
"Makefile" = ["justfile", "Justfile", ".justfile", "*.justfile"]
@@ -1,2 +1,2 @@
[mappings] [mappings]
"SSH Config" = ["**/.ssh/**/config"] "SSH Config" = ["**/.ssh/config"]
@@ -1,2 +0,0 @@
[mappings]
"INI" = ["/etc/dnf/dnf.conf", "/etc/yum.repos.d/*.repo"]
@@ -1,2 +0,0 @@
[mappings]
"INI" = ["**/ghostty/**/*.ghostty", "**/ghostty/themes/*"]
+12 -202
View File
@@ -386,23 +386,12 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
} }
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> { fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
self.chars_take_while(|c| !is_sequence_introducer(c)) self.chars_take_while(|c| c != '\x1B')
.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,
@@ -411,24 +400,12 @@ 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),
// 7-bit DCS/SOS/PM/APC: `ESC P/X/^/_` introduces a string body. Some((i, c)) => match c {
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 => { c => Some(EscapeSequenceOffsets::Unknown {
// 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, end: i + c.len_utf8(),
}) }),
}
}, },
} }
} }
@@ -436,27 +413,12 @@ 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 {
// ST is BEL, ESC `\\`, or U+009C. match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
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;
@@ -475,11 +437,6 @@ 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, '\\')) => {
@@ -509,82 +466,20 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
} }
} }
EscapeSequenceOffsets::OSC { Some(EscapeSequenceOffsets::OSC {
start_sequence, start_sequence,
start_command, start_command: osc_open_index + osc_open_char.len_utf8(),
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()))
}
/// CSI body parser entered after the 8-bit introducer U+009B was consumed. let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
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')) {
@@ -603,13 +498,13 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((i, c)) => i + c.len_utf8(), Some((i, c)) => i + c.len_utf8(),
}; };
EscapeSequenceOffsets::CSI { Some(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> {
@@ -648,25 +543,13 @@ 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((_, c)) if is_sequence_introducer(*c) => self.next_sequence(), Some((_, '\x1B')) => 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
@@ -834,79 +717,6 @@ 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
@@ -1,5 +0,0 @@
#!/usr/bin/env bash
NAME="${0##*/}"
for i in {1..3}; do
echo "hello $NAME $i"
done
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/expect -f
set timeout 30
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env tclsh
puts "Hello from tclsh"
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/wish
button .b -text "Click"
+48 -309
View File
@@ -305,19 +305,6 @@ 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()
@@ -626,18 +613,12 @@ 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]
@@ -1066,57 +1047,6 @@ 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();
@@ -1544,7 +1474,6 @@ 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("");
} }
@@ -2164,24 +2093,6 @@ 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()
@@ -3123,6 +3034,53 @@ fn no_wrapping_with_chop_long_lines() {
wrapping_test("--chop-long-lines", false); wrapping_test("--chop-long-lines", false);
} }
#[test]
fn line_wrapping_when_set_to_character() {
wrapping_test("--wrap=character", true);
}
#[test]
fn no_line_wrapping_when_character_wrap_from_config() {
// --wrap=character in a config file should be ignored when output is not interactive
// (e.g. when piping), since wrapping is only useful with a pager.
let tmp_dir = tempdir().expect("can create temporary directory");
let tmp_config_path = tmp_dir.path().join("wrap-character.conf");
std::fs::write(&tmp_config_path, "--wrap=character").expect("can write config file");
let expected = "abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n";
bat_with_config()
.env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap())
.arg("--style=rule")
.arg("--color=never")
.arg("--decorations=always")
.arg("--terminal-width=80")
.arg("long-single-line.txt")
.assert()
.success()
.stdout(expected)
.stderr("");
}
#[test]
fn no_line_wrapping_when_character_wrap_from_bat_opts() {
// --wrap=character in BAT_OPTS should be ignored when output is not interactive,
// since it was not explicitly provided on the command line.
let expected = "abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n";
bat_with_config()
.env("BAT_OPTS", "--wrap=character")
.arg("--style=rule")
.arg("--color=never")
.arg("--decorations=always")
.arg("--terminal-width=80")
.arg("long-single-line.txt")
.assert()
.success()
.stdout(expected)
.stderr("");
}
#[test] #[test]
#[serial] #[serial]
fn wrap_never_flag_respected_with_paging_always() { fn wrap_never_flag_respected_with_paging_always() {
@@ -3850,158 +3808,6 @@ 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() {
@@ -4373,70 +4179,3 @@ 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 Regular → Executable
+10 -12
View File
@@ -4,7 +4,6 @@ import itertools
import subprocess import subprocess
import pathlib import pathlib
import shutil import shutil
from typing import Iterable
def generate_snapshots(): def generate_snapshots():
@@ -20,23 +19,22 @@ 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: str, arguments: Iterable[str]): def generate_snapshot(name, arguments):
output_file = "output/{name}.snapshot.txt".format(name=name) command = "cargo run -- --paging=never --color=never --decorations=always "
command = [ command += "{args} sample.rs > output/{name}.snapshot.txt".format(
"cargo", "run", "--", "--paging=never", "--color=never", name=name,
"--decorations=always", *arguments, "sample.rs" args=arguments
] )
print("generating snapshot for {}".format(name)) print("generating snapshot for {}".format(name))
with open(output_file, "w") as f: subprocess.call(command, shell=True)
subprocess.call(command, stdout=f)
def build_bat(): def build_bat():
print("building bat") print("building bat")
subprocess.call(["cargo", "build"], cwd="../..") subprocess.call("cargo build", cwd="../..", shell=True)
def prepare_output_dir(): def prepare_output_dir():
@@ -51,7 +49,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"]) subprocess.call("git checkout -- sample.rs", shell=True)
build_bat() build_bat()
-25
View File
@@ -1,25 +0,0 @@
(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
@@ -1,7 +0,0 @@
#!/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
@@ -1,7 +0,0 @@
#!/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
@@ -1,5 +0,0 @@
#!/usr/bin/wish
# Tk script detected via wish shebang
package require Tk
button .b -text "Click" -command {puts "clicked"}
pack .b
-7
View File
@@ -1,7 +0,0 @@
<Solution>
 <Folder Name="/Build/">
 <File Path="Directory.Build.props" />
 <File Path="projectname.targets" />
 </Folder>
 <Project Path="console.csproj" />
</Solution>
-25
View File
@@ -1,25 +0,0 @@
(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
@@ -1,7 +0,0 @@
#!/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
@@ -1,7 +0,0 @@
#!/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
@@ -1,5 +0,0 @@
#!/usr/bin/wish
# Tk script detected via wish shebang
package require Tk
button .b -text "Click" -command {puts "clicked"}
pack .b
-7
View File
@@ -1,7 +0,0 @@
<Solution>
<Folder Name="/Build/">
<File Path="Directory.Build.props" />
<File Path="projectname.targets" />
</Folder>
<Project Path="console.csproj" />
</Solution>
+28 -37
View File
@@ -1,17 +1,15 @@
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::PathBuf; use std::path::{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,
@@ -61,42 +59,35 @@ impl Default for BatTester {
} }
} }
fn create_sample_directory() -> Result<TempDir, Box<dyn std::error::Error>> { fn create_sample_directory() -> Result<TempDir, git2::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 = gix::init(&temp_dir)?; let repo = Repository::init(&temp_dir)?;
let mut tree = gix::objs::Tree::empty();
// Create sample.rs from snapshot file // Copy over `sample.rs`
let blob_id = repo.write_blob_stream(File::open("tests/snapshots/sample.rs")?)?; let sample_path = temp_dir.path().join("sample.rs");
let entry = tree::Entry { println!("{sample_path:?}");
mode: tree::EntryMode::from(tree::EntryKind::Blob), fs::copy("tests/snapshots/sample.rs", &sample_path).expect("successful copy");
oid: blob_id.object()?.id,
filename: BString::from("sample.rs"),
};
tree.entries.push(entry);
let tree_id = repo.write_object(tree)?;
let author = SignatureRef { // Commit
name: "test".as_bytes().as_bstr(), let mut index = repo.index()?;
email: "test@test.test".as_bytes().as_bstr(), index.add_path(Path::new("sample.rs"))?;
time: &Time::now_local_or_utc().format_or_unix(Format::Raw),
}; let oid = index.write_tree()?;
let commit_id = repo.commit_as( let signature = Signature::now("bat test runner", "bat@test.runner")?;
author, let tree = repo.find_tree(oid)?;
author, let _ = repo.commit(
"HEAD", Some("HEAD"), // point HEAD to our new commit
&signature, // author
&signature, // committer
"initial commit", "initial commit",
tree_id, &tree,
gix::commit::NO_PARENT_IDS, &[],
)?; );
assert_eq!(commit_id, repo.head_id()?); let mut opts = CheckoutBuilder::new();
repo.checkout_head(Some(opts.force()))?;
fs::copy( fs::copy("tests/snapshots/sample.modified.rs", &sample_path).expect("successful copy");
"tests/snapshots/sample.modified.rs",
temp_dir.path().join("sample.rs"),
)
.expect("successful copy");
Ok(temp_dir) Ok(temp_dir)
} }