mirror of
https://github.com/sharkdp/bat.git
synced 2026-11-01 12:05:56 +00:00
Compare commits
198
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e95c32b6f0 | ||
|
|
f059c8da38 | ||
|
|
8f5727ea38 | ||
|
|
b4fdcec6a1 | ||
|
|
fe901f5051 | ||
|
|
3e2c1bdc8e | ||
|
|
e423744a7d | ||
|
|
a9aa73d215 | ||
|
|
f0a58595bd | ||
|
|
2fd71ad588 | ||
|
|
49d9ac1d6e | ||
|
|
54b1f9c6af | ||
|
|
b841eadf09 | ||
|
|
b49aae8b1f | ||
|
|
f748c1a467 | ||
|
|
d3afbaeb06 | ||
|
|
dbe6ec265d | ||
|
|
bc0a67e07c | ||
|
|
fca6c542d4 | ||
|
|
c2adbf6caa | ||
|
|
f48149b119 | ||
|
|
ca5eeb7c2a | ||
|
|
9438a74e6d | ||
|
|
8c63b37205 | ||
|
|
d50b53d0ef | ||
|
|
62b439dae3 | ||
|
|
e8d05da87a | ||
|
|
edd161aefb | ||
|
|
b95f8d37bf | ||
|
|
0e3074cfe1 | ||
|
|
4d916905d1 | ||
|
|
c47a163f4b | ||
|
|
a56bca503b | ||
|
|
d9559c69f5 | ||
|
|
12cbb20de4 | ||
|
|
a9b112b6fe | ||
|
|
4608fc959a | ||
|
|
dcd725dc4e | ||
|
|
4987f76709 | ||
|
|
156ef1d2f4 | ||
|
|
eca6c5b688 | ||
|
|
7323a7514f | ||
|
|
cf72a432ac | ||
|
|
16b5f99d16 | ||
|
|
b784eaf0ae | ||
|
|
2ed8b010af | ||
|
|
5fee86b97b | ||
|
|
66ad54c27d | ||
|
|
2ed7adc49f | ||
|
|
ea3909bbdf | ||
|
|
0acf9417cd | ||
|
|
b5c8e101f8 | ||
|
|
383abc83c8 | ||
|
|
7928b4be52 | ||
|
|
036a201df9 | ||
|
|
2da852dc78 | ||
|
|
65c311e9fb | ||
|
|
b3c9516a9b | ||
|
|
5f12c62b3e | ||
|
|
7892a8754b | ||
|
|
400c5dc6f1 | ||
|
|
1cedc1b4c3 | ||
|
|
b8430080f0 | ||
|
|
a3f202f367 | ||
|
|
b671e53c2c | ||
|
|
de2e651bf5 | ||
|
|
061fb216cb | ||
|
|
af59a32183 | ||
|
|
c63838efeb | ||
|
|
cecbb80093 | ||
|
|
3ca4a01be6 | ||
|
|
8112350da6 | ||
|
|
a5deaf63ff | ||
|
|
2fb2415731 | ||
|
|
a58f23724a | ||
|
|
bb492fff47 | ||
|
|
a96b9e2230 | ||
|
|
531d54b110 | ||
|
|
db69aab106 | ||
|
|
2ba8db9c14 | ||
|
|
d344844486 | ||
|
|
aeb0e4c457 | ||
|
|
1cf12d49bd | ||
|
|
7cb212646c | ||
|
|
6573ca9307 | ||
|
|
f04dc80bc7 | ||
|
|
e6ac6ce08d | ||
|
|
949f18e23a | ||
|
|
969b33dad1 | ||
|
|
73dc3258be | ||
|
|
df38225653 | ||
|
|
c2cc79daa7 | ||
|
|
0230cba42d | ||
|
|
227e0fa00f | ||
|
|
384f2f5d13 | ||
|
|
060ccb1385 | ||
|
|
feabb25209 | ||
|
|
839f2300b7 | ||
|
|
ea0742babc | ||
|
|
bfa6e59978 | ||
|
|
9710a231d8 | ||
|
|
a9b67b2d99 | ||
|
|
b22be63fec | ||
|
|
3a7a750969 | ||
|
|
be5343c742 | ||
|
|
c371044276 | ||
|
|
d449ce5b7e | ||
|
|
218afc30ac | ||
|
|
78951393e2 | ||
|
|
0a52e4321f | ||
|
|
d72163160e | ||
|
|
971f9679da | ||
|
|
57d436861a | ||
|
|
bb4f48a124 | ||
|
|
c75cc01eef | ||
|
|
835bd2a756 | ||
|
|
3409674d9c | ||
|
|
7de0c4ba47 | ||
|
|
20a818fe14 | ||
|
|
30b50a9eed | ||
|
|
088f1e4253 | ||
|
|
53769d03b0 | ||
|
|
b7ae4585ad | ||
|
|
a4609d1778 | ||
|
|
8f70e0cf00 | ||
|
|
e34595b53e | ||
|
|
6ba745b74b | ||
|
|
98f09fd6f3 | ||
|
|
69f40d9d8a | ||
|
|
6e6137ce10 | ||
|
|
df164cc2e3 | ||
|
|
f9eed8d619 | ||
|
|
d695ad8f88 | ||
|
|
e60d184f97 | ||
|
|
e2568c52cd | ||
|
|
c5e6f6aae3 | ||
|
|
8f93137246 | ||
|
|
fa00fb8ce8 | ||
|
|
d117bf2320 | ||
|
|
f4d836ee87 | ||
|
|
04576fd5a5 | ||
|
|
e7025d9bdb | ||
|
|
6d4a3cef53 | ||
|
|
6b8887133d | ||
|
|
5bb35b4bed | ||
|
|
d28aa4a8b4 | ||
|
|
e9b552d5d0 | ||
|
|
af1f53d9a9 | ||
|
|
8033b1e7be | ||
|
|
01408ce094 | ||
|
|
bcd1d3f45b | ||
|
|
4cd5db2a78 | ||
|
|
67aa491313 | ||
|
|
e490c0f322 | ||
|
|
2d29ecb939 | ||
|
|
707b5949fd | ||
|
|
fd53693e1f | ||
|
|
f3d0773468 | ||
|
|
66e336bcfb | ||
|
|
ce7ad9adca | ||
|
|
f9fa590c3d | ||
|
|
1e1c3773fb | ||
|
|
0ef0342ff2 | ||
|
|
de396794cf | ||
|
|
43c4957e18 | ||
|
|
c9fa10e76b | ||
|
|
530e83a455 | ||
|
|
2cec8cdf25 | ||
|
|
138d70fd4c | ||
|
|
98df25434e | ||
|
|
5f952066fb | ||
|
|
f74082cee8 | ||
|
|
a0c95618c4 | ||
|
|
f57c1b6b1d | ||
|
|
517de0463b | ||
|
|
3be4e30e00 | ||
|
|
15fdc7637a | ||
|
|
72fc697372 | ||
|
|
b8d2781784 | ||
|
|
787a36acb6 | ||
|
|
a79701a43e | ||
|
|
3bcbc45ab0 | ||
|
|
7953df44ea | ||
|
|
443bcb9287 | ||
|
|
0523b70473 | ||
|
|
f776d1ad3f | ||
|
|
cfc6d9c415 | ||
|
|
89d7c86b19 | ||
|
|
c437ad4d27 | ||
|
|
1c6e763d75 | ||
|
|
baebdd1789 | ||
|
|
f7b84a3b41 | ||
|
|
b483f90612 | ||
|
|
6edad56b00 | ||
|
|
e2174379ed | ||
|
|
816aea0625 | ||
|
|
5bf932d511 | ||
|
|
652d50e92e |
No files matched your search
@@ -166,7 +166,8 @@ jobs:
|
||||
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-latest , dpkg_arch: armhf, use-cross: true }
|
||||
- { target: arm-unknown-linux-musleabihf, os: ubuntu-latest , dpkg_arch: musl-linux-armhf, use-cross: true }
|
||||
- { target: i686-pc-windows-msvc , os: windows-2025 , }
|
||||
- { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i386, use-cross: true }
|
||||
# The timeout test's socketpair fails on the Ubuntu 26.04 runner.
|
||||
- { target: i686-unknown-linux-gnu , os: ubuntu-24.04 , dpkg_arch: i386, use-cross: true }
|
||||
- { target: i686-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-i686, use-cross: true }
|
||||
- { target: x86_64-apple-darwin , os: macos-15-intel, }
|
||||
- { target: aarch64-apple-darwin , os: macos-latest , }
|
||||
@@ -457,7 +458,7 @@ jobs:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
||||
- uses: vedantmgoyal9/winget-releaser@b7370d4d152efdce803a26cc6ca9eee58cb2ad54
|
||||
with:
|
||||
identifier: sharkdp.bat
|
||||
installers-regex: '-pc-windows-msvc\.zip$'
|
||||
|
||||
@@ -281,3 +281,6 @@
|
||||
[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
|
||||
@@ -3,11 +3,18 @@
|
||||
|
||||
## Other
|
||||
|
||||
- Pin the i686 GNU CI runner to Ubuntu 24.04 to avoid `socketpair` failures on Ubuntu 26.04, see #4101 (@PerchAnton)
|
||||
- Fix dead Visual C++ Redistributable link in README, see #3895 (@1t1sCooL)
|
||||
- Document `StripAnsiMode` and its variants in rustdoc, see #4067 (@ump45nose)
|
||||
- Update Cargo dependencies to resolve current RustSec advisories, see #3861 (@TyceHerrman)
|
||||
- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536
|
||||
- Add .NET slnx extension, see #3682 (@ltrzesniewski)
|
||||
|
||||
## Features
|
||||
|
||||
- Add `-b` / `--number-nonblank` flag to only number non-blank lines, for `cat -b` compatibility. Closes #3856, see #3857 (@MeGaurav4)
|
||||
- Add a `--sanitize=<auto|always|never>` flag for safe display of untrusted input. It implies `--strip-ansi` at the same value and additionally substitutes terminal-active control bytes (cursor moves, charset switches, beep, etc.) and Unicode bidi / zero-width formatting characters with the Unicode replacement character (U+FFFD). Mitigates Trojan-Source-style spoofing (CVE-2021-42574). See #3729 (@curious-rabbit)
|
||||
- Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta)
|
||||
- Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn)
|
||||
- Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355)
|
||||
- Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey)
|
||||
@@ -21,6 +28,24 @@
|
||||
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
|
||||
|
||||
## Bugfixes
|
||||
- Report `BAT_CONFIG_DIR` in `bat --diagnostic`, so bug reports show a config directory that overrides `$XDG_CONFIG_HOME`. See #4068 (@ump45nose)
|
||||
- Fix `bat` hanging forever on JavaScript (and Svelte) code where an expression is followed by a block comment, e.g. `a /* x */`. Closes #4080, see #4091 (@NavpreetSSidhu)
|
||||
- Fix the first line of UTF-16BE input swallowing the second line, which broke line numbers, `--line-range` and highlighting, see #XXXX (@cindykrafft)
|
||||
- Exit quietly with status 0 instead of panicking when stdout is closed early (broken pipe) on auxiliary output paths such as `--completion`, `--config-file`, `--diagnostic`, `--generate-config-file`, `--set-terminal-title` and `cache --build`/`--clear`, matching the main output path since #232, closes #4034, see #4035 (@Mathjk)
|
||||
- Track strikethrough (SGR 9 / 29) in the ANSI style tracker so it is re-emitted like bold, dim, italic, and underline, and is no longer dropped after the first highlighted token when displaying ANSI input with syntax highlighting, see #4015 (@mikehasa)
|
||||
- Allow boolean flags to be given more than once, so that a flag set in the config file can also be passed on the command line without erroring, see #3912 (@logarithmone1128)
|
||||
- Use parsed CLI arguments to detect number flags, see #3908 (@cuishuang)
|
||||
- Detect binary content beyond the first line, preventing encrypted files with early line breaks from being treated as text. Closes #3554, see #3877 (@Matei02355)
|
||||
- Use the last occurrence of numbering and plain flags in combined short arguments, see #3897 (@cuishuang)
|
||||
- Avoid a spurious Cargo manifest error during installation from Git, see #3899 (@rootsec1)
|
||||
- Avoid repeated scans of long lines in Log syntax highlighting, see #3876 (@Matei02355)
|
||||
- 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)
|
||||
@@ -41,6 +66,8 @@
|
||||
- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l)
|
||||
- Fixed syntax tests path, see #3610 (@foxfromworld)
|
||||
- Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz)
|
||||
- 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
|
||||
- Use git version of cross. See #3533 (@OctopusET)
|
||||
@@ -48,6 +75,7 @@
|
||||
- Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall)
|
||||
- Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon)
|
||||
- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406)
|
||||
- Replace `libgit2` with a pure Rust implementation of git called `gitoxide`, see PR #3703 (@blinxen)
|
||||
|
||||
## Syntaxes
|
||||
|
||||
@@ -59,6 +87,12 @@
|
||||
- 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)
|
||||
- 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)
|
||||
- Add syntax mapping for Podman `.quadlets` files, see #4069 (@injust)
|
||||
|
||||
## Themes
|
||||
|
||||
|
||||
Generated
+1580
-680
File diff suppressed because it is too large.
Load diff
+14
-14
@@ -31,8 +31,7 @@ minimal-application = [
|
||||
"regex-onig",
|
||||
"wild",
|
||||
]
|
||||
git = ["git2"] # Support indicating git modifications
|
||||
vendored-libgit2 = ["git2/vendored-libgit2"]
|
||||
git = ["gix"] # Support indicating git modifications
|
||||
paging = [ "shell-words", "grep-cli", "minus"] # Support applying a pager on the output
|
||||
lessopen = ["execute"] # Support $LESSOPEN preprocessor
|
||||
build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"]
|
||||
@@ -52,7 +51,7 @@ thiserror = "2.0"
|
||||
wild = { version = "2.2", optional = true }
|
||||
content_inspector = "0.2.4"
|
||||
shell-words = { version = "1.1.1", optional = true }
|
||||
minus = { version = "5.6", optional = true, features = [
|
||||
minus = { version = "5.7", optional = true, features = [
|
||||
"dynamic_output",
|
||||
"search",
|
||||
] }
|
||||
@@ -64,22 +63,23 @@ serde_yaml = "0.9.28"
|
||||
semver = "1.0"
|
||||
path_abs = { version = "0.5", default-features = false }
|
||||
clircle = { version = "0.6.1", default-features = false }
|
||||
bugreport = { version = "0.5.0", optional = true }
|
||||
bugreport = { version = "0.6.0", optional = true }
|
||||
etcetera = { version = "0.11.0", optional = true }
|
||||
grep-cli = { version = "0.1.12", optional = true }
|
||||
regex = { version = "1.12.2", optional = true }
|
||||
regex = { version = "1.12.3", optional = true }
|
||||
walkdir = { version = "2.5", optional = true }
|
||||
bytesize = { version = "2.3.1" }
|
||||
encoding_rs = "0.8.35"
|
||||
execute = { version = "0.2.15", optional = true }
|
||||
execute = { version = "0.3.0", optional = true }
|
||||
terminal-colorsaurus = "1.0"
|
||||
unicode-segmentation = "1.13.2"
|
||||
itertools = "0.14.0"
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.20"
|
||||
[dependencies.gix]
|
||||
version = "0.86"
|
||||
optional = true
|
||||
default-features = false
|
||||
features = ["sha1", "blob-diff"]
|
||||
|
||||
[dependencies.syntect]
|
||||
version = "5.3.0"
|
||||
@@ -87,18 +87,18 @@ default-features = false
|
||||
features = ["parsing"]
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.5.60"
|
||||
version = "4.6.1"
|
||||
optional = true
|
||||
features = ["wrap_help", "cargo"]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
plist = "1.7.0"
|
||||
plist = "1.9.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.16"
|
||||
expect-test = "1.5.0"
|
||||
serial_test = { version = "2.0.0", default-features = false }
|
||||
predicates = "3.1.3"
|
||||
predicates = "3.1.4"
|
||||
wait-timeout = "0.2.1"
|
||||
tempfile = "3.27.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -108,13 +108,13 @@ nix = { version = "0.31", default-features = false, features = ["term"] }
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.97"
|
||||
indexmap = { version = "2.13.0", features = ["serde"] }
|
||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
||||
itertools = "0.14.0"
|
||||
once_cell = "1.20"
|
||||
prettyplease = "0.2.37"
|
||||
proc-macro2 = "1.0.106"
|
||||
quote = "1.0.45"
|
||||
regex = "1.12.2"
|
||||
regex = "1.12.3"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_with = { version = "3.17.0", default-features = false, features = ["macros"] }
|
||||
@@ -123,7 +123,7 @@ toml = { version = "1.1.1", features = ["preserve_order"] }
|
||||
walkdir = "2.5"
|
||||
|
||||
[build-dependencies.clap]
|
||||
version = "4.5.60"
|
||||
version = "4.6.1"
|
||||
optional = true
|
||||
features = ["wrap_help", "cargo"]
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ pacman -S bat
|
||||
|
||||
### On Fedora
|
||||
|
||||
You can install [the `bat` package](https://koji.fedoraproject.org/koji/packageinfo?packageID=27506) from the official [Fedora Modular](https://docs.fedoraproject.org/en-US/modularity/using-modules/) repository.
|
||||
You can install [the `bat` package](https://packages.fedoraproject.org/pkgs/rust-bat/bat/) from the official sources:
|
||||
|
||||
```bash
|
||||
dnf install bat
|
||||
@@ -417,7 +417,7 @@ scoop install bat
|
||||
|
||||
You can download prebuilt binaries from the [Release page](https://github.com/sharkdp/bat/releases),
|
||||
|
||||
You will need to install the [Visual C++ Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads) package.
|
||||
You will need to install the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist) package.
|
||||
|
||||
### From binaries
|
||||
|
||||
@@ -794,7 +794,7 @@ Example configuration file:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You will need to install the [Visual C++ Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads) package.
|
||||
You will need to install the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist) package.
|
||||
|
||||
### Paging
|
||||
|
||||
@@ -843,6 +843,10 @@ If your version of `bat` supports the `--strip-ansi=auto` option, it can be used
|
||||
before syntax highlighting. Alternatively, you may disable both syntax highlighting and wrapping by
|
||||
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]
|
||||
> The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text.
|
||||
|
||||
@@ -898,7 +902,7 @@ cargo install --path . --locked --force
|
||||
```
|
||||
|
||||
If you want to build an application that uses `bat`'s pretty-printing
|
||||
features as a library, check out the [the API documentation](https://docs.rs/bat/).
|
||||
features as a library, check out the [API documentation](https://docs.rs/bat/).
|
||||
Note that you have to use either `regex-onig` or `regex-fancy` as a feature
|
||||
when you depend on `bat` as a library.
|
||||
|
||||
|
||||
Vendored
+8
-2
@@ -14,12 +14,12 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
|
||||
$ArrayPrint = @('unicode', 'caret')
|
||||
|
||||
function Get-MyThemes(){
|
||||
$themes = {{PROJECT_EXECUTABLE}} --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object
|
||||
$themes = {{PROJECT_EXECUTABLE}} --no-paging --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object
|
||||
return $themes
|
||||
}
|
||||
|
||||
function Get-MyLanguages(){
|
||||
$themes = {{PROJECT_EXECUTABLE}} --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object
|
||||
$themes = {{PROJECT_EXECUTABLE}} --no-paging --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object
|
||||
return $themes
|
||||
}
|
||||
|
||||
@@ -100,6 +100,11 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
|
||||
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
|
||||
break
|
||||
}
|
||||
'*;--sanitize' {
|
||||
$ArrayWhen |
|
||||
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
|
||||
break
|
||||
}
|
||||
'*;--strip-ansi' {
|
||||
$ArrayWhen |
|
||||
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
|
||||
@@ -158,6 +163,7 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
|
||||
[CompletionResult]::new('--ignored-suffix' , 'ignored-suffix' , [CompletionResultType]::ParameterName, 'Ignore extension. For example: ''bat --ignored-suffix ".dev" my_file.json.dev'' will use JSON syntax, and ignore ''.dev''')
|
||||
[CompletionResult]::new('--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('--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('-p' , 'p' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').')
|
||||
[CompletionResult]::new('--plain' , 'plain' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').')
|
||||
|
||||
Vendored
+8
-3
@@ -80,7 +80,7 @@ _bat() {
|
||||
-l | --language)
|
||||
local IFS=$'\n'
|
||||
COMPREPLY=($(compgen -W "$(
|
||||
"$1" --list-languages | while IFS=: read -r lang _; do
|
||||
"$1" --no-paging --list-languages | while IFS=: read -r lang _; do
|
||||
printf "%s\n" "$lang"
|
||||
done
|
||||
)" -- "$cur"))
|
||||
@@ -132,6 +132,10 @@ _bat() {
|
||||
COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--sanitize)
|
||||
COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--completion)
|
||||
COMPREPLY=($(compgen -W "bash fish zsh ps1" -- "$cur"))
|
||||
return 0
|
||||
@@ -150,14 +154,14 @@ _bat() {
|
||||
;;
|
||||
--theme)
|
||||
local IFS=$'\n'
|
||||
COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --list-themes)" -- "$cur"))
|
||||
COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --no-paging --list-themes)" -- "$cur"))
|
||||
__bat_escape_completions
|
||||
return 0
|
||||
;;
|
||||
--theme-dark | \
|
||||
--theme-light)
|
||||
local IFS=$'\n'
|
||||
COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur"))
|
||||
COMPREPLY=($(compgen -W "$("$1" --no-paging --list-themes)" -- "$cur"))
|
||||
__bat_escape_completions
|
||||
return 0
|
||||
;;
|
||||
@@ -221,6 +225,7 @@ _bat() {
|
||||
--list-themes
|
||||
--squeeze-blank
|
||||
--squeeze-limit
|
||||
--sanitize
|
||||
--strip-ansi
|
||||
--style
|
||||
--line-range
|
||||
|
||||
Vendored
+6
-5
@@ -15,11 +15,11 @@ function __bat_complete_files -a token
|
||||
end
|
||||
|
||||
function __bat_complete_one_language -a comp
|
||||
command $bat --list-languages | string split -f1 : | string match -e "$comp"
|
||||
command $bat --no-paging --list-languages | string split -f1 : | string match -e "$comp"
|
||||
end
|
||||
|
||||
function __bat_complete_list_languages
|
||||
for spec in (command $bat --list-languages)
|
||||
for spec in (command $bat --no-paging --list-languages)
|
||||
set -l name (string split -f1 : $spec)
|
||||
for ext in (string split -f2 : $spec | string split ,)
|
||||
test -n "$ext"; or continue
|
||||
@@ -221,6 +221,7 @@ complete -c $bat -s s -l squeeze-blank -d "Squeeze consecutive empty lines into
|
||||
complete -c $bat -l squeeze-limit -x -d "Set the maximum number of consecutive empty lines to be printed" -n __bat_no_excl_args
|
||||
|
||||
complete -c $bat -l 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
|
||||
|
||||
@@ -234,11 +235,11 @@ complete -c $bat -l tabs -x -a "$tabs_opts" -d "Set tab width" -n __bat_no_excl_
|
||||
|
||||
complete -c $bat -l terminal-width -x -d "Set terminal <width>, +<offset>, or -<offset>" -n __bat_no_excl_args
|
||||
|
||||
complete -c $bat -l theme -x -a "$special_themes(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args
|
||||
complete -c $bat -l theme -x -a "$special_themes(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args
|
||||
|
||||
complete -c $bat -l theme-dark -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args
|
||||
complete -c $bat -l theme-dark -x -a "(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args
|
||||
|
||||
complete -c $bat -l theme-light -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args
|
||||
complete -c $bat -l theme-light -x -a "(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args
|
||||
|
||||
complete -c $bat -s u -l unbuffered -d "Enable unbuffered input reading for streaming use cases" -n __bat_no_excl_args
|
||||
|
||||
|
||||
Vendored
+9
-3
@@ -54,6 +54,7 @@ _{{PROJECT_EXECUTABLE}}_main() {
|
||||
--squeeze-blank'[squeeze consecutive empty lines into a single empty line]'
|
||||
--squeeze-limit='[set the maximum number of consecutive empty lines]:limit:'
|
||||
--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]"
|
||||
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'
|
||||
@@ -90,20 +91,25 @@ _{{PROJECT_EXECUTABLE}}_main() {
|
||||
languages)
|
||||
local IFS=$'\n'
|
||||
local -a languages
|
||||
languages=( ${(f)"$({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }')"} )
|
||||
# `--list-languages` emits one `name:matchers` line per language,
|
||||
# which `_describe` parses as `value:description`. Only the
|
||||
# language name is offered as the completion value; the matchers
|
||||
# show up as the menu description. See
|
||||
# https://github.com/sharkdp/bat/issues/3735.
|
||||
languages=( ${(f)"$({{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-languages)"} )
|
||||
|
||||
_describe 'language' languages && ret=0
|
||||
;;
|
||||
|
||||
themes)
|
||||
local -a themes expl
|
||||
themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} )
|
||||
themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-themes)"} )
|
||||
|
||||
_wanted themes expl 'theme' compadd -a themes && ret=0
|
||||
;;
|
||||
theme_preferences)
|
||||
local -a themes expl
|
||||
themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} )
|
||||
themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-themes)"} )
|
||||
|
||||
_wanted themes expl 'theme' compadd -a themes && ret=0
|
||||
;;
|
||||
|
||||
+10
@@ -12,3 +12,13 @@ index 05a4fed6..78a7bf55 100644
|
||||
scope: source.js
|
||||
variables:
|
||||
bin_digit: '[01_]'
|
||||
@@ -93,7 +93,8 @@ variables:
|
||||
(?! \+\+ | -- )
|
||||
(?=
|
||||
!= |
|
||||
- [-+*/%><=&|^\[(;,.:?] |
|
||||
+ [-+*%><=&|^\[(;,.:?] |
|
||||
+ /(?!\*) |
|
||||
(?:in|instanceof){{identifier_break}}
|
||||
)
|
||||
))
|
||||
+1
Submodule assets/syntaxes/02_Extra/Caddy added at cd7d132da3.
Vendored
+1
-1
Submodule assets/syntaxes/02_Extra/LESS updated: 836b47ec61...d076a4dd41.
Vendored
+1
-1
Submodule assets/syntaxes/02_Extra/PureScript updated: 5acebc1850...1773f4fddb.
+23
-4
@@ -38,22 +38,41 @@ contexts:
|
||||
scope: markup.underline.link.scheme.log
|
||||
push: url-host
|
||||
log_level_lines:
|
||||
- match: (?=.*{{error}})
|
||||
# Prevent retrying each lookahead at every position.
|
||||
- match: ^(?=.*{{error}})
|
||||
push:
|
||||
- error_line_meta
|
||||
- main_pop_at_eol
|
||||
- match: (?=.*{{warning}})
|
||||
- match: ^(?=.*{{warning}})
|
||||
push:
|
||||
- warning_line_meta
|
||||
- main_pop_at_eol
|
||||
- match: (?=.*{{info}})
|
||||
- match: ^(?=.*{{info}})
|
||||
push:
|
||||
- info_line_meta
|
||||
- main_pop_at_eol
|
||||
- match: (?=.*{{debug}})
|
||||
- match: ^(?=.*{{debug}})
|
||||
push:
|
||||
- debug_line_meta
|
||||
- main_pop_at_eol
|
||||
# For syntaxes that embed Log after consuming a prefix. Call this context once.
|
||||
log_level_lines_at_current_position:
|
||||
- match: (?=.*{{error}})
|
||||
set:
|
||||
- error_line_meta
|
||||
- main_pop_at_eol
|
||||
- match: (?=.*{{warning}})
|
||||
set:
|
||||
- warning_line_meta
|
||||
- main_pop_at_eol
|
||||
- match: (?=.*{{info}})
|
||||
set:
|
||||
- info_line_meta
|
||||
- main_pop_at_eol
|
||||
- match: (?=.*{{debug}})
|
||||
set:
|
||||
- debug_line_meta
|
||||
- main_pop_at_eol
|
||||
log_levels:
|
||||
- match: '{{error}}'
|
||||
scope: markup.error.log
|
||||
|
||||
+11
-1
@@ -50,6 +50,17 @@ contexts:
|
||||
- match: (?=\])
|
||||
pop: true
|
||||
text:
|
||||
- include: text_special
|
||||
- match: ''
|
||||
set: classify_log_text
|
||||
classify_log_text:
|
||||
- include: scope:text.log#log_level_lines_at_current_position
|
||||
- match: ''
|
||||
set: text_body
|
||||
text_body:
|
||||
- include: text_special
|
||||
- include: scope:text.log
|
||||
text_special:
|
||||
- match: $
|
||||
pop: true
|
||||
- match: '<\w+>'
|
||||
@@ -62,4 +73,3 @@ contexts:
|
||||
escape: \)$
|
||||
escape_captures:
|
||||
0: punctuation.section.block.end.syslog
|
||||
- include: scope:text.log
|
||||
+1
-1
Submodule assets/syntaxes/02_Extra/typst-syntax-highlight updated: 363f0e767c...5f71d12fa1.
Vendored
+1
-1
Submodule assets/syntaxes/02_Extra/vscode-wgsl updated: acf26718d7...a285c38f74.
+1
-1
@@ -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
|
||||
https://github.com/open-policy-agent/opa/blob/master/misc/syntax/textmate/Rego.tmLanguage
|
||||
* `SML.sublime_syntax` has been added manually from
|
||||
https://github.com/seanjames777/SML-Language-Definitiona as it is not
|
||||
https://github.com/seanjames777/SML-Language-Definition as it is not
|
||||
kept in a standalone repository. The file generated is from
|
||||
https://github.com/seanjames777/SML-Language-Definition/blob/master/sml.tmLanguage
|
||||
* `Cabal.sublime_syntax` has been added manually from
|
||||
|
||||
@@ -83,6 +83,16 @@ Options:
|
||||
-n, --number
|
||||
Only show line numbers, no other decorations. This is an alias for '--style=numbers'
|
||||
|
||||
-b, --number-nonblank
|
||||
Only show line numbers for non-blank lines, no other decorations. This is an alias for
|
||||
'--style=numbers'. Non-blank lines are lines that contain any character before the line
|
||||
ending, including spaces and tabs. When used together with --number (-n),
|
||||
--number-nonblank (-b) takes precedence.
|
||||
|
||||
Example:
|
||||
printf 'alpha\n\nbeta\n' | bat -b
|
||||
numbers 'alpha' and 'beta', but skips the empty line.
|
||||
|
||||
--color <when>
|
||||
Specify when to use colored output. The automatic mode only enables colors if an
|
||||
interactive terminal is detected - colors are automatically disabled if the output goes to
|
||||
@@ -164,6 +174,14 @@ Options:
|
||||
escape sequences unless the syntax highlighting language is plain text. Possible values:
|
||||
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>
|
||||
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
|
||||
|
||||
@@ -33,6 +33,8 @@ Options:
|
||||
Truncate all lines longer than screen width. Alias for '--wrap=never'.
|
||||
-n, --number
|
||||
Show line numbers (alias for '--style=numbers').
|
||||
-b, --number-nonblank
|
||||
Show line numbers for non-blank lines only (alias for '--style=numbers').
|
||||
--color <when>
|
||||
When to use colors (*auto*, never, always).
|
||||
--italic-text <when>
|
||||
|
||||
+52
-10
@@ -347,15 +347,15 @@ impl HighlightingAssets {
|
||||
file_name: &OsStr,
|
||||
ignored_suffixes: &IgnoredSuffixes,
|
||||
) -> Result<Option<SyntaxReferenceInSet<'_>>> {
|
||||
let mut syntax = self.find_syntax_by_extension(Path::new(file_name).extension())?;
|
||||
if syntax.is_none() {
|
||||
syntax =
|
||||
ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
|
||||
// Note: recursion
|
||||
self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes)
|
||||
})?;
|
||||
let stripped =
|
||||
ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| {
|
||||
self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes)
|
||||
.map(Some)
|
||||
})?;
|
||||
match stripped {
|
||||
Some(syntax) => Ok(syntax),
|
||||
None => self.find_syntax_by_extension(Path::new(file_name).extension()),
|
||||
}
|
||||
Ok(syntax)
|
||||
}
|
||||
|
||||
fn get_first_line_syntax(
|
||||
@@ -363,11 +363,11 @@ impl HighlightingAssets {
|
||||
reader: &mut InputReader,
|
||||
) -> Result<Option<SyntaxReferenceInSet<'_>>> {
|
||||
let syntax_set = self.get_syntax_set()?;
|
||||
Ok(String::from_utf8(reader.first_line.clone())
|
||||
Ok(std::str::from_utf8(&reader.first_line)
|
||||
.ok()
|
||||
.and_then(|l| {
|
||||
// Strip UTF-8 BOM if present
|
||||
let line = l.strip_prefix('\u{feff}').unwrap_or(&l);
|
||||
let line = l.strip_prefix('\u{feff}').unwrap_or(l);
|
||||
syntax_set.find_syntax_by_first_line(line)
|
||||
})
|
||||
.map(|syntax| SyntaxReferenceInSet { syntax, syntax_set }))
|
||||
@@ -686,6 +686,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_detection_ignored_suffix_falls_back_to_first_line() {
|
||||
let mut test = SyntaxDetectionTest::new();
|
||||
|
||||
// By default a `.txt` file uses Plain Text, even with a shebang: the
|
||||
// `.txt` extension wins and first-line detection is not reached.
|
||||
assert_eq!(
|
||||
test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"),
|
||||
"Plain Text"
|
||||
);
|
||||
|
||||
// Once `.txt` is an ignored suffix it is stripped before detection. The
|
||||
// stripped name (`test`) has no extension, so detection falls back to the
|
||||
// first line -- even though `.txt` is itself a registered extension that
|
||||
// would otherwise match as Plain Text. See #2745.
|
||||
test.syntax_mapping.insert_ignored_suffix(".txt");
|
||||
assert_eq!(
|
||||
test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"),
|
||||
"Bourne Again Shell (bash)"
|
||||
);
|
||||
assert_eq!(
|
||||
test.syntax_for_file_with_content("test.txt", "<?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]
|
||||
fn syntax_detection_is_case_insensitive() {
|
||||
let mut test = SyntaxDetectionTest::new();
|
||||
|
||||
+25
-15
@@ -1,4 +1,5 @@
|
||||
use std::convert::TryInto;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use syntect::highlighting::ThemeSet;
|
||||
@@ -24,7 +25,7 @@ pub fn build(
|
||||
|
||||
let acknowledgements = build_acknowledgements(source_dir, include_acknowledgements)?;
|
||||
|
||||
print_unlinked_contexts(&syntax_set);
|
||||
print_unlinked_contexts(&syntax_set)?;
|
||||
|
||||
write_assets(
|
||||
&theme_set,
|
||||
@@ -46,16 +47,18 @@ fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> Result
|
||||
if theme_dir.exists() {
|
||||
let res = theme_set.add_from_folder(&theme_dir);
|
||||
if let Err(err) = res {
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
"Failed to load one or more themes from '{}' (reason: '{err}')",
|
||||
theme_dir.to_string_lossy(),
|
||||
);
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
"No themes were found in '{}', using the default set",
|
||||
theme_dir.to_string_lossy()
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
theme_set.try_into()
|
||||
@@ -78,23 +81,25 @@ fn build_syntax_set_builder(
|
||||
if syntax_dir.exists() {
|
||||
syntax_set_builder.add_from_folder(syntax_dir, true)?;
|
||||
} else {
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
"No syntaxes were found in '{}', using the default set.",
|
||||
syntax_dir.to_string_lossy()
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(syntax_set_builder)
|
||||
}
|
||||
|
||||
fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
|
||||
fn print_unlinked_contexts(syntax_set: &SyntaxSet) -> Result<()> {
|
||||
let missing_contexts = syntax_set.find_unlinked_contexts();
|
||||
if !missing_contexts.is_empty() {
|
||||
println!("Some referenced contexts could not be found!");
|
||||
writeln!(io::stdout(), "Some referenced contexts could not be found!")?;
|
||||
for context in missing_contexts {
|
||||
println!("- {context}");
|
||||
writeln!(io::stdout(), "- {context}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_assets(
|
||||
@@ -127,12 +132,13 @@ fn write_assets(
|
||||
)?;
|
||||
}
|
||||
|
||||
print!(
|
||||
write!(
|
||||
io::stdout(),
|
||||
"Writing metadata to folder {} ... ",
|
||||
target_dir.to_string_lossy()
|
||||
);
|
||||
)?;
|
||||
crate::assets_metadata::AssetsMetadata::new(current_version).save_to_folder(target_dir)?;
|
||||
println!("okay");
|
||||
writeln!(io::stdout(), "okay")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -161,10 +167,14 @@ fn asset_to_cache<T: serde::Serialize>(
|
||||
description: &str,
|
||||
compressed: bool,
|
||||
) -> Result<()> {
|
||||
print!("Writing {description} to {} ... ", path.to_string_lossy());
|
||||
write!(
|
||||
io::stdout(),
|
||||
"Writing {description} to {} ... ",
|
||||
path.to_string_lossy()
|
||||
)?;
|
||||
let contents = asset_to_contents(asset, description, compressed)?;
|
||||
std::fs::write(path, &contents[..])
|
||||
.map_err(|_| format!("Could not save {description} to {}", path.to_string_lossy()))?;
|
||||
println!("okay");
|
||||
writeln!(io::stdout(), "okay")?;
|
||||
Ok(())
|
||||
}
|
||||
+60
-40
@@ -38,6 +38,15 @@ pub fn env_no_color() -> bool {
|
||||
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 {
|
||||
Short,
|
||||
Long,
|
||||
@@ -50,6 +59,10 @@ pub struct App {
|
||||
/// (not from config file or environment variables).
|
||||
/// This is used to honor the flag when piping output, similar to `cat -n`.
|
||||
number_from_cli: bool,
|
||||
/// True if -b / --number-nonblank was passed on the command line
|
||||
/// (not from config file or environment variables).
|
||||
/// This is used to honor the flag when piping output, similar to `cat -b`.
|
||||
number_nonblank_from_cli: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -59,36 +72,12 @@ impl App {
|
||||
|
||||
let interactive_output = std::io::stdout().is_terminal();
|
||||
|
||||
// Check if the -n / --number option was passed on the command line
|
||||
// (before merging with config file and environment variables).
|
||||
// This is needed to honor the -n flag when piping output, similar to `cat -n`.
|
||||
// We need to handle both standalone (-n, --number) and combined short flags (-pn, -An, etc.)
|
||||
// Note: We only check if -n appears and is not overridden by -p in the same combined flag.
|
||||
// For combined flags like -np, -p comes after -n and overrides it, so we don't count it.
|
||||
// For combined flags like -pn, -n comes after -p and takes effect.
|
||||
let number_from_cli = wild::args_os().any(|arg| {
|
||||
let arg_str = arg.to_string_lossy();
|
||||
if arg_str == "-n" || arg_str == "--number" {
|
||||
return true;
|
||||
}
|
||||
// Handle combined short flags
|
||||
// Only count -n if it's the LAST flag in the combined form (so -p doesn't override it)
|
||||
// or if -p is not present in the combined form
|
||||
if arg_str.starts_with('-') && !arg_str.starts_with("--") && arg_str.len() > 2 {
|
||||
let chars: Vec<char> = arg_str.chars().skip(1).collect();
|
||||
let n_pos = chars.iter().position(|&c| c == 'n');
|
||||
let p_pos = chars.iter().position(|&c| c == 'p');
|
||||
// -n is in the combined flag and either:
|
||||
// - -p is not present, OR
|
||||
// - -n comes after -p (so -n takes effect)
|
||||
if let Some(n) = n_pos {
|
||||
if p_pos.is_none() || n > p_pos.unwrap() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
// Parse the original command line separately from config and environment arguments.
|
||||
// Using clap here keeps CLI-only detection consistent with the final parser, including
|
||||
// option terminators, combined short flags, and options that take values.
|
||||
let cli_matches = Self::cli_matches(interactive_output);
|
||||
let number_from_cli = cli_matches.get_flag("number");
|
||||
let number_nonblank_from_cli = cli_matches.get_flag("number-nonblank");
|
||||
|
||||
let matches = Self::matches(interactive_output)?;
|
||||
|
||||
@@ -130,6 +119,7 @@ impl App {
|
||||
matches,
|
||||
interactive_output,
|
||||
number_from_cli,
|
||||
number_nonblank_from_cli,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -201,6 +191,10 @@ impl App {
|
||||
args
|
||||
}
|
||||
|
||||
fn cli_matches(interactive_output: bool) -> ArgMatches {
|
||||
clap_app::build_app(interactive_output).get_matches_from(wild::args_os())
|
||||
}
|
||||
|
||||
fn matches(interactive_output: bool) -> Result<ArgMatches> {
|
||||
// Check if we should skip config file processing for special arguments
|
||||
// that don't require full application setup (version, diagnostic)
|
||||
@@ -445,7 +439,8 @@ impl App {
|
||||
.map(|s| s.as_str())
|
||||
== Some("always")
|
||||
|| self.matches.get_flag("force-colorization")
|
||||
|| self.number_from_cli),
|
||||
|| self.number_from_cli
|
||||
|| self.number_nonblank_from_cli),
|
||||
tab_width: self
|
||||
.matches
|
||||
.get_one::<String>("tabs")
|
||||
@@ -458,18 +453,36 @@ impl App {
|
||||
4
|
||||
},
|
||||
),
|
||||
strip_ansi: match self
|
||||
.matches
|
||||
.get_one::<String>("strip-ansi")
|
||||
.map(|s| s.as_str())
|
||||
{
|
||||
Some("never") => StripAnsiMode::Never,
|
||||
Some("always") => StripAnsiMode::Always,
|
||||
Some("auto") => StripAnsiMode::Auto,
|
||||
_ => unreachable!("other values for --strip-ansi are not allowed"),
|
||||
strip_ansi: {
|
||||
let sanitize = parse_strip_ansi_value(
|
||||
self.matches
|
||||
.get_one::<String>("sanitize")
|
||||
.map(|s| s.as_str()),
|
||||
"--sanitize",
|
||||
);
|
||||
let strip_ansi = parse_strip_ansi_value(
|
||||
self.matches
|
||||
.get_one::<String>("strip-ansi")
|
||||
.map(|s| s.as_str()),
|
||||
"--strip-ansi",
|
||||
);
|
||||
// --sanitize implies --strip-ansi to the same value.
|
||||
if sanitize != StripAnsiMode::Never {
|
||||
sanitize
|
||||
} else {
|
||||
strip_ansi
|
||||
}
|
||||
},
|
||||
sanitize: parse_strip_ansi_value(
|
||||
self.matches
|
||||
.get_one::<String>("sanitize")
|
||||
.map(|s| s.as_str()),
|
||||
"--sanitize",
|
||||
),
|
||||
quiet_empty: self.matches.get_flag("quiet-empty"),
|
||||
unbuffered: self.matches.get_flag("unbuffered"),
|
||||
number_nonblank: self.matches.get_flag("number-nonblank")
|
||||
|| self.number_nonblank_from_cli,
|
||||
theme: theme(self.theme_options()).to_string(),
|
||||
visible_lines: match self.matches.try_contains_id("diff").unwrap_or_default()
|
||||
&& self.matches.get_flag("diff")
|
||||
@@ -588,6 +601,13 @@ impl App {
|
||||
])));
|
||||
}
|
||||
|
||||
// Only line numbers for non-blank lines if `--number-nonblank`.
|
||||
if self.matches.get_flag("number-nonblank") || self.number_nonblank_from_cli {
|
||||
return Some(StyleComponents(HashSet::from([
|
||||
StyleComponent::LineNumbers,
|
||||
])));
|
||||
}
|
||||
|
||||
// Plain if `--plain` is specified at least once.
|
||||
if self.matches.get_count("plain") > 0 {
|
||||
let mut components = HashSet::from([StyleComponent::Plain]);
|
||||
|
||||
+15
-10
@@ -1,5 +1,5 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -9,10 +9,11 @@ use bat::assets::HighlightingAssets;
|
||||
use bat::assets_metadata::AssetsMetadata;
|
||||
use bat::error::*;
|
||||
|
||||
pub fn clear_assets(cache_dir: &Path) {
|
||||
clear_asset(cache_dir.join("themes.bin"), "theme set cache");
|
||||
clear_asset(cache_dir.join("syntaxes.bin"), "syntax set cache");
|
||||
clear_asset(cache_dir.join("metadata.yaml"), "metadata file");
|
||||
pub fn clear_assets(cache_dir: &Path) -> Result<()> {
|
||||
clear_asset(cache_dir.join("themes.bin"), "theme set cache")?;
|
||||
clear_asset(cache_dir.join("syntaxes.bin"), "syntax set cache")?;
|
||||
clear_asset(cache_dir.join("metadata.yaml"), "metadata file")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn assets_from_cache_or_binary(
|
||||
@@ -43,15 +44,19 @@ pub fn assets_from_cache_or_binary(
|
||||
Ok(custom_assets.unwrap_or_else(HighlightingAssets::from_binary))
|
||||
}
|
||||
|
||||
fn clear_asset(path: PathBuf, description: &str) {
|
||||
print!("Clearing {description} ... ");
|
||||
fn clear_asset(path: PathBuf, description: &str) -> Result<()> {
|
||||
write!(io::stdout(), "Clearing {description} ... ")?;
|
||||
match fs::remove_file(&path) {
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
println!("skipped (not present)");
|
||||
writeln!(io::stdout(), "skipped (not present)")?;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("could not remove the cache file {path:?}: {err}");
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
"could not remove the cache file {path:?}: {err}"
|
||||
)?;
|
||||
}
|
||||
Ok(_) => println!("okay"),
|
||||
Ok(_) => writeln!(io::stdout(), "okay")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -52,6 +52,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
.long("show-all")
|
||||
.alias("show-nonprintable")
|
||||
.short('A')
|
||||
.overrides_with("show-all")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("language")
|
||||
.help("Show non-printable characters (space, tab, newline, ..).")
|
||||
@@ -97,6 +98,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("plain")
|
||||
.overrides_with("plain")
|
||||
.overrides_with("number")
|
||||
.overrides_with("number-nonblank")
|
||||
.short('p')
|
||||
.long("plain")
|
||||
.action(ArgAction::Count)
|
||||
@@ -170,6 +172,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("diff")
|
||||
.long("diff")
|
||||
.short('d')
|
||||
.overrides_with("diff")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("line-range")
|
||||
.help("Only show lines that have been added/removed/modified.")
|
||||
@@ -234,6 +237,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("chop-long-lines")
|
||||
.long("chop-long-lines")
|
||||
.short('S')
|
||||
.overrides_with("chop-long-lines")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Truncate all lines longer than screen width. Alias for '--wrap=never'."),
|
||||
)
|
||||
@@ -277,6 +281,23 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
'--style=numbers'",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("number-nonblank")
|
||||
.long("number-nonblank")
|
||||
.overrides_with("number-nonblank")
|
||||
.short('b')
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Show line numbers for non-blank lines only (alias for '--style=numbers').")
|
||||
.long_help(
|
||||
"Only show line numbers for non-blank lines, no other decorations. This is an \
|
||||
alias for '--style=numbers'. Non-blank lines are lines that contain any \
|
||||
character before the line ending, including spaces and tabs. When used \
|
||||
together with --number (-n), --number-nonblank (-b) takes precedence.\n\n\
|
||||
Example:\n \
|
||||
printf 'alpha\\n\\nbeta\\n' | bat -b\n \
|
||||
numbers 'alpha' and 'beta', but skips the empty line.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("color")
|
||||
.long("color")
|
||||
@@ -455,6 +476,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("squeeze-blank")
|
||||
.long("squeeze-blank")
|
||||
.short('s')
|
||||
.overrides_with("squeeze-blank")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Squeeze consecutive empty lines.")
|
||||
.long_help("Squeeze consecutive empty lines into a single empty line.")
|
||||
@@ -480,6 +502,24 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
language is plain text. Possible values: auto, always, *never*.")
|
||||
.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::new("style")
|
||||
.long("style")
|
||||
@@ -562,6 +602,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("unbuffered")
|
||||
.short('u')
|
||||
.long("unbuffered")
|
||||
.overrides_with("unbuffered")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Enable unbuffered input reading for streaming use cases.")
|
||||
.long_help(
|
||||
@@ -583,6 +624,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
.arg(
|
||||
Arg::new("no-custom-assets")
|
||||
.long("no-custom-assets")
|
||||
.overrides_with("no-custom-assets")
|
||||
.action(ArgAction::SetTrue)
|
||||
.hide(true)
|
||||
.help("Do not load custom assets"),
|
||||
@@ -663,6 +705,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
Arg::new("quiet-empty")
|
||||
.long("quiet-empty")
|
||||
.short('E')
|
||||
.overrides_with("quiet-empty")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Produce no output when the input is empty.")
|
||||
.long_help(
|
||||
@@ -681,6 +724,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
.arg(
|
||||
Arg::new("set-terminal-title")
|
||||
.long("set-terminal-title")
|
||||
.overrides_with("set-terminal-title")
|
||||
.action(ArgAction::SetTrue)
|
||||
.hide_short_help(true)
|
||||
.help("Sets terminal title to filenames when using a pager."),
|
||||
|
||||
@@ -32,12 +32,13 @@ pub fn config_file() -> PathBuf {
|
||||
pub fn generate_config_file() -> bat::error::Result<()> {
|
||||
let config_file = config_file();
|
||||
if config_file.is_file() {
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
"A config file already exists at: {}",
|
||||
config_file.to_string_lossy()
|
||||
);
|
||||
)?;
|
||||
|
||||
print!("Overwrite? (y/N): ");
|
||||
write!(io::stdout(), "Overwrite? (y/N): ")?;
|
||||
io::stdout().flush()?;
|
||||
let mut decision = String::new();
|
||||
io::stdin().read_line(&mut decision)?;
|
||||
@@ -93,10 +94,11 @@ pub fn generate_config_file() -> bat::error::Result<()> {
|
||||
)
|
||||
})?;
|
||||
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
"Success! Config file written to {}",
|
||||
config_file.to_string_lossy()
|
||||
);
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+35
-23
@@ -74,9 +74,9 @@ fn run_cache_subcommand(
|
||||
#[cfg(feature = "build-assets")]
|
||||
build_assets(matches, config_dir, cache_dir)?;
|
||||
#[cfg(not(feature = "build-assets"))]
|
||||
println!("bat has been built without the 'build-assets' feature. The 'cache --build' option is not available.");
|
||||
writeln!(io::stdout(), "bat has been built without the 'build-assets' feature. The 'cache --build' option is not available.")?;
|
||||
} else if matches.get_flag("clear") {
|
||||
clear_assets(cache_dir);
|
||||
clear_assets(cache_dir)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -152,7 +152,14 @@ pub fn get_languages(config: &Config, cache_dir: &Path) -> Result<String> {
|
||||
let comma_separator = ", ";
|
||||
let separator = " ";
|
||||
// Line-wrapping for the possible file extension overflow.
|
||||
let desired_width = config.term_width - longest - separator.len();
|
||||
// Clamp instead of subtracting: a tiny `--terminal-width` (smaller than the
|
||||
// longest language name) would otherwise underflow `usize` and wrap to a huge
|
||||
// value, silently disabling wrapping (and panicking in debug/overflow-checked
|
||||
// builds). Mirrors the `saturating_sub` fix applied to `print_snip` in #3804.
|
||||
let desired_width = config
|
||||
.term_width
|
||||
.saturating_sub(longest)
|
||||
.saturating_sub(separator.len());
|
||||
|
||||
let style = if config.colored_output {
|
||||
Green.normal()
|
||||
@@ -255,13 +262,17 @@ pub fn list_themes(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_terminal_title_to(new_terminal_title: String) {
|
||||
fn set_terminal_title_to(new_terminal_title: String) -> Result<()> {
|
||||
let osc_command_for_setting_terminal_title = "\x1b]0;";
|
||||
let osc_end_command = "\x07";
|
||||
// Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC.
|
||||
let safe_title = bat::sanitize_for_terminal(&new_terminal_title);
|
||||
print!("{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}");
|
||||
io::stdout().flush().unwrap();
|
||||
write!(
|
||||
io::stdout(),
|
||||
"{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}"
|
||||
)?;
|
||||
io::stdout().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_new_terminal_title(inputs: &Vec<Input>) -> String {
|
||||
@@ -279,13 +290,13 @@ fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Resu
|
||||
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
|
||||
let controller = Controller::new(config, &assets);
|
||||
if config.paging_mode != PagingMode::Never && config.set_terminal_title {
|
||||
set_terminal_title_to(get_new_terminal_title(&inputs));
|
||||
set_terminal_title_to(get_new_terminal_title(&inputs))?;
|
||||
}
|
||||
controller.run(inputs, None)
|
||||
}
|
||||
|
||||
#[cfg(feature = "bugreport")]
|
||||
fn invoke_bugreport(app: &App, cache_dir: &Path) {
|
||||
fn invoke_bugreport(app: &App, cache_dir: &Path) -> Result<()> {
|
||||
use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry};
|
||||
|
||||
struct ColorSchemeCollector;
|
||||
@@ -322,6 +333,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
|
||||
.info(CommandLine::default())
|
||||
.info(EnvironmentVariables::list(&[
|
||||
"BAT_CACHE_PATH",
|
||||
"BAT_CONFIG_DIR",
|
||||
"BAT_CONFIG_PATH",
|
||||
"BAT_OPTS",
|
||||
"BAT_PAGER",
|
||||
@@ -363,7 +375,8 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
|
||||
))
|
||||
};
|
||||
|
||||
report.print::<Markdown>();
|
||||
writeln!(io::stdout(), "{}", report.format::<Markdown>())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `Err(..)` upon fatal errors. Otherwise, returns `Ok(true)` on full success and
|
||||
@@ -375,21 +388,22 @@ fn run() -> Result<bool> {
|
||||
|
||||
if app.matches.get_flag("diagnostic") {
|
||||
#[cfg(feature = "bugreport")]
|
||||
invoke_bugreport(&app, cache_dir);
|
||||
invoke_bugreport(&app, cache_dir)?;
|
||||
#[cfg(not(feature = "bugreport"))]
|
||||
println!("bat has been built without the 'bugreport' feature. The '--diagnostic' option is not available.");
|
||||
writeln!(io::stdout(), "bat has been built without the 'bugreport' feature. The '--diagnostic' option is not available.")?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
#[cfg(feature = "application")]
|
||||
if let Some(shell) = app.matches.get_one::<String>("completion") {
|
||||
match shell.as_str() {
|
||||
"bash" => println!("{}", completions::BASH_COMPLETION),
|
||||
"fish" => println!("{}", completions::FISH_COMPLETION),
|
||||
"ps1" => println!("{}", completions::PS1_COMPLETION),
|
||||
"zsh" => println!("{}", completions::ZSH_COMPLETION),
|
||||
let completion = match shell.as_str() {
|
||||
"bash" => completions::BASH_COMPLETION,
|
||||
"fish" => completions::FISH_COMPLETION,
|
||||
"ps1" => completions::PS1_COMPLETION,
|
||||
"zsh" => completions::ZSH_COMPLETION,
|
||||
_ => unreachable!("No completion for shell '{shell}' available."),
|
||||
}
|
||||
};
|
||||
writeln!(io::stdout(), "{completion}")?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -420,17 +434,15 @@ fn run() -> Result<bool> {
|
||||
if app.matches.get_flag("list-languages") {
|
||||
let languages: String = get_languages(&config, cache_dir)?;
|
||||
let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))];
|
||||
let plain_config = Config {
|
||||
style_components: StyleComponents::new(StyleComponent::Plain.components(false)),
|
||||
paging_mode: PagingMode::QuitIfOneScreen,
|
||||
..Default::default()
|
||||
};
|
||||
let mut plain_config = config.clone();
|
||||
plain_config.style_components =
|
||||
StyleComponents::new(StyleComponent::Plain.components(false));
|
||||
run_controller(inputs, &plain_config, cache_dir)
|
||||
} else if app.matches.get_flag("list-themes") {
|
||||
list_themes(&config, config_dir, cache_dir, app.theme_options())?;
|
||||
Ok(true)
|
||||
} else if app.matches.get_flag("config-file") {
|
||||
println!("{}", config_file().to_string_lossy());
|
||||
writeln!(io::stdout(), "{}", config_file().to_string_lossy())?;
|
||||
Ok(true)
|
||||
} else if app.matches.get_flag("generate-config-file") {
|
||||
generate_config_file()?;
|
||||
|
||||
+10
-3
@@ -98,24 +98,31 @@ pub struct Config<'a> {
|
||||
/// cached assets) are not available, assets from the binary will be used instead.
|
||||
pub use_custom_assets: bool,
|
||||
|
||||
// Whether or not to use $LESSOPEN if set
|
||||
/// Whether or not to use $LESSOPEN if set
|
||||
#[cfg(feature = "lessopen")]
|
||||
pub use_lessopen: bool,
|
||||
|
||||
// Whether or not to set terminal title when using a pager
|
||||
/// Whether or not to set terminal title when using a pager
|
||||
pub set_terminal_title: bool,
|
||||
|
||||
/// The maximum number of consecutive empty lines to display
|
||||
pub squeeze_lines: Option<usize>,
|
||||
|
||||
// Whether or not to strip ANSI escape codes from the input
|
||||
/// Whether or not to strip ANSI escape codes from the input
|
||||
pub strip_ansi: StripAnsiMode,
|
||||
|
||||
/// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi.
|
||||
pub sanitize: StripAnsiMode,
|
||||
|
||||
/// Whether or not to produce no output when input is empty
|
||||
pub quiet_empty: bool,
|
||||
|
||||
/// Whether or not to use unbuffered input reading for streaming use cases
|
||||
pub unbuffered: bool,
|
||||
|
||||
/// Only number non-blank lines (like `cat -b`). Has no effect if `style_components` doesn't
|
||||
/// include `LineNumbers`.
|
||||
pub number_nonblank: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "minimal-application", feature = "paging"))]
|
||||
|
||||
+15
-4
@@ -276,10 +276,21 @@ impl Controller<'_> {
|
||||
}
|
||||
if !reached_eof {
|
||||
if reader.read_line(&mut current_line_buffer)? {
|
||||
// Fill the buffer
|
||||
buffered_lines
|
||||
.push_back((mem::take(&mut current_line_buffer), current_line_number));
|
||||
current_line_number += 1;
|
||||
// Fill the buffer. In number-nonblank mode, don't advance the
|
||||
// line counter for empty lines (content-free lines that contain
|
||||
// only the line terminator).
|
||||
if self.config.number_nonblank
|
||||
&& current_line_buffer
|
||||
.iter()
|
||||
.all(|&b| b == b'\r' || b == b'\n')
|
||||
{
|
||||
buffered_lines
|
||||
.push_back((mem::take(&mut current_line_buffer), current_line_number));
|
||||
} else {
|
||||
buffered_lines
|
||||
.push_back((mem::take(&mut current_line_buffer), current_line_number));
|
||||
current_line_number += 1;
|
||||
}
|
||||
} else {
|
||||
// No more data to read
|
||||
reached_eof = true;
|
||||
|
||||
@@ -45,6 +45,14 @@ impl Decoration for LineNumberDecoration {
|
||||
continuation: bool,
|
||||
_printer: &InteractivePrinter,
|
||||
) -> DecorationText {
|
||||
if line_number == 0 {
|
||||
// Blank line in number-nonblank mode: show empty space instead of a number.
|
||||
return DecorationText {
|
||||
text: self.color.paint(" ".repeat(self.width())).to_string(),
|
||||
width: self.width(),
|
||||
};
|
||||
}
|
||||
|
||||
if continuation {
|
||||
if line_number >= self.cached_wrap_invalid_at {
|
||||
let new_width = self.cached_wrap.width + 1;
|
||||
|
||||
+176
-62
@@ -1,12 +1,13 @@
|
||||
#![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::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use git2::{DiffOptions, IntoCString, Repository};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LineChange {
|
||||
Added,
|
||||
RemovedAbove,
|
||||
@@ -16,68 +17,181 @@ pub enum LineChange {
|
||||
|
||||
pub type LineChanges = HashMap<u32, LineChange>;
|
||||
|
||||
pub fn get_git_diff(filename: &Path) -> Option<LineChanges> {
|
||||
let repo = Repository::discover(filename).ok()?;
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let old_lines = hunk.old_lines();
|
||||
let new_start = hunk.new_start();
|
||||
let new_lines = hunk.new_lines();
|
||||
let new_end = (new_start + new_lines) as i32 - 1;
|
||||
|
||||
if old_lines == 0 && new_lines > 0 {
|
||||
mark_section(&mut line_changes, new_start, new_end, LineChange::Added);
|
||||
} else if new_lines == 0 && old_lines > 0 {
|
||||
if new_start == 0 {
|
||||
mark_section(&mut line_changes, 1, 1, LineChange::RemovedAbove);
|
||||
} else {
|
||||
mark_section(
|
||||
&mut line_changes,
|
||||
new_start,
|
||||
new_start as i32,
|
||||
LineChange::RemovedBelow,
|
||||
);
|
||||
}
|
||||
} else if hunk.after.is_empty() && !hunk.before.is_empty() {
|
||||
if hunk.after.start == 0 {
|
||||
changes.insert(1, LineChange::RemovedAbove);
|
||||
} else {
|
||||
mark_section(&mut line_changes, new_start, new_end, LineChange::Modified);
|
||||
changes.insert(hunk.after.start, LineChange::RemovedBelow);
|
||||
}
|
||||
} else {
|
||||
for line in hunk.after {
|
||||
changes.insert(line + 1, LineChange::Modified);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}),
|
||||
None,
|
||||
Some(changes)
|
||||
}
|
||||
|
||||
pub fn get_git_diff(filename: &Path) -> Option<LineChanges> {
|
||||
let filepath_absolute = filename.canonicalize().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(),
|
||||
);
|
||||
|
||||
Some(line_changes)
|
||||
collect_changes_from_hunks(diff.hunks())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{get_git_diff, LineChange};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn git(repo: &Path, args: &[&str]) {
|
||||
let output = Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.expect("git command can run");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git {args:?} failed\nstdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn setup_repo() -> tempfile::TempDir {
|
||||
let dir = tempfile::tempdir().expect("can create temporary directory");
|
||||
let repo = dir.path();
|
||||
git(repo, &["init"]);
|
||||
git(repo, &["config", "user.email", "test@test.test"]);
|
||||
git(repo, &["config", "user.name", "Test"]);
|
||||
dir
|
||||
}
|
||||
|
||||
fn create_and_track_file(repo: &tempfile::TempDir, filename: &str) -> PathBuf {
|
||||
let filepath = repo.path().join(filename);
|
||||
std::fs::write(&filepath, "file\n").expect("can write file");
|
||||
git(repo.path(), &["add", filename]);
|
||||
git(repo.path(), &["commit", "-m", "initial"]);
|
||||
|
||||
filepath.to_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_a_git_repository() {
|
||||
let dir = tempfile::tempdir().expect("can create temporary directory");
|
||||
let file = dir.path().join("file.txt");
|
||||
std::fs::write(&file, "line 1\n").expect("can write file");
|
||||
|
||||
assert_eq!(get_git_diff(&file), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_is_calculated_against_index() {
|
||||
let repo = setup_repo();
|
||||
let file = create_and_track_file(&repo, "file.txt");
|
||||
|
||||
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
|
||||
// Add modified file to index -> should find 0 changes but not none
|
||||
git(repo.path(), &["add", "file.txt"]);
|
||||
|
||||
assert_eq!(get_git_diff(&file).expect("empty map").len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_is_calculated_against_index_2() {
|
||||
let repo = setup_repo();
|
||||
let file = create_and_track_file(&repo, "file.txt");
|
||||
|
||||
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
|
||||
git(repo.path(), &["add", "file.txt"]);
|
||||
// modify the second line again which should show a single change
|
||||
std::fs::write(&file, "line 1\nline 2 modified again\n").expect("can write file");
|
||||
|
||||
assert_eq!(get_git_diff(&file).expect("one change").len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_is_calculated_correctly() {
|
||||
let repo = setup_repo();
|
||||
create_and_track_file(&repo, "committed.txt");
|
||||
let filename = "committed2.txt";
|
||||
let file = create_and_track_file(&repo, filename);
|
||||
|
||||
std::fs::write(&file, "file\nline 2 added\n").expect("can write file");
|
||||
let mut changes = get_git_diff(&file).expect("multiple changes");
|
||||
assert_eq!(changes.get(&2), Some(&LineChange::Added));
|
||||
git(repo.path(), &["add", filename]);
|
||||
|
||||
std::fs::write(&file, "file\nline 2 modified\n").expect("can write file");
|
||||
changes = get_git_diff(&file).expect("multiple changes");
|
||||
assert_eq!(changes.get(&2), Some(&LineChange::Modified));
|
||||
git(repo.path(), &["add", filename]);
|
||||
|
||||
std::fs::write(&file, "line 2 modified\nline 3 added").expect("can write file");
|
||||
changes = get_git_diff(&file).expect("multiple changes");
|
||||
assert_eq!(changes.get(&1), Some(&LineChange::RemovedAbove));
|
||||
assert_eq!(changes.get(&2), Some(&LineChange::Added));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faulty_git_version_does_not_panic() {
|
||||
let repo = setup_repo();
|
||||
let file = create_and_track_file(&repo, "file.txt");
|
||||
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
|
||||
// changes are detected
|
||||
assert_eq!(get_git_diff(&file).expect("one change").len(), 2);
|
||||
// write invalid repositoryformatversion
|
||||
git(
|
||||
repo.path(),
|
||||
&["config", "core.repositoryformatversion", "one"],
|
||||
);
|
||||
// changes are no longer detected
|
||||
assert_eq!(get_git_diff(&file), None);
|
||||
}
|
||||
}
|
||||
+77
-4
@@ -9,6 +9,8 @@ use content_inspector::{self, ContentType};
|
||||
|
||||
use crate::error::*;
|
||||
|
||||
const CONTENT_INSPECTION_LIMIT: usize = 1024;
|
||||
|
||||
/// A description of an Input source.
|
||||
/// This tells bat how to refer to the input.
|
||||
#[derive(Clone)]
|
||||
@@ -263,14 +265,38 @@ impl<'a> InputReader<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn try_new<R: BufRead + 'a>(mut reader: R) -> io::Result<InputReader<'a>> {
|
||||
let mut first_line = vec![];
|
||||
reader.read_until(b'\n', &mut first_line)?;
|
||||
// content_inspector scans at most 1024 bytes. Capture the already-buffered
|
||||
// prefix before splitting out the first line so an early newline in binary
|
||||
// data does not shorten the inspected content. This does not consume input
|
||||
// or perform an additional read beyond the one read_until needs anyway.
|
||||
let mut inspection_prefix = {
|
||||
let buffered = reader.fill_buf()?;
|
||||
buffered[..buffered.len().min(CONTENT_INSPECTION_LIMIT)].to_vec()
|
||||
};
|
||||
|
||||
let content_type = inspect_content_type(&first_line);
|
||||
let mut first_line = vec![];
|
||||
if !inspection_prefix.is_empty() {
|
||||
reader.read_until(b'\n', &mut first_line)?;
|
||||
}
|
||||
|
||||
// A custom BufRead implementation may expose less than 1024 bytes at a
|
||||
// time. Keep the old behavior for long first lines in that case.
|
||||
let first_line_prefix_len = first_line.len().min(CONTENT_INSPECTION_LIMIT);
|
||||
if first_line_prefix_len > inspection_prefix.len() {
|
||||
inspection_prefix.clear();
|
||||
inspection_prefix.extend_from_slice(&first_line[..first_line_prefix_len]);
|
||||
}
|
||||
|
||||
let content_type = inspect_content_type(&inspection_prefix);
|
||||
|
||||
if content_type == Some(ContentType::UTF_16LE) {
|
||||
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?;
|
||||
} else if content_type == Some(ContentType::UTF_16BE) {
|
||||
} else if content_type == Some(ContentType::UTF_16BE)
|
||||
// In UTF-16BE a newline is encoded as 0x00 0x0A, so `read_until(b'\n')` above
|
||||
// may already have read a complete first line. Only keep reading if it did not
|
||||
// (e.g. the 0x0A byte was part of another character such as U+4E0A).
|
||||
&& !(first_line.len() % 2 == 0 && first_line.ends_with(&[0x00, 0x0A]))
|
||||
{
|
||||
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?;
|
||||
}
|
||||
|
||||
@@ -410,6 +436,53 @@ fn non_zip_pk_prefix_is_not_treated_as_binary() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_detection_scans_beyond_first_line_and_preserves_input() {
|
||||
let mut content = vec![b'a'; CONTENT_INSPECTION_LIMIT + 1];
|
||||
content[1] = b'\n';
|
||||
content[CONTENT_INSPECTION_LIMIT - 1] = 0;
|
||||
|
||||
let mut reader = InputReader::new(&content[..]);
|
||||
assert_eq!(Some(ContentType::BINARY), reader.content_type);
|
||||
|
||||
let mut replayed = Vec::new();
|
||||
while reader.read_line(&mut replayed).unwrap() {}
|
||||
assert_eq!(content, replayed);
|
||||
|
||||
drop(reader);
|
||||
|
||||
content[CONTENT_INSPECTION_LIMIT - 1] = b'a';
|
||||
content[CONTENT_INSPECTION_LIMIT] = 0;
|
||||
|
||||
let reader = InputReader::new(&content[..]);
|
||||
assert_eq!(Some(ContentType::UTF_8), reader.content_type);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_detection_does_not_read_twice() {
|
||||
struct OneRead(Option<&'static [u8]>);
|
||||
|
||||
impl Read for OneRead {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self.0.take() {
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"no more data is available yet",
|
||||
)),
|
||||
Some(content) => {
|
||||
buf[..content.len()].copy_from_slice(content);
|
||||
Ok(content.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (content, expected) in [(&b"text\n"[..], Some(ContentType::UTF_8)), (&b""[..], None)] {
|
||||
let input = InputReader::try_new(BufReader::new(OneRead(Some(content)))).unwrap();
|
||||
assert_eq!(expected, input.content_type);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_open_returns_initial_read_errors() {
|
||||
struct FailingRead;
|
||||
|
||||
+112
-4
@@ -14,6 +14,53 @@ use crate::{
|
||||
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
|
||||
pub(crate) struct LessOpenPreprocessor {
|
||||
lessopen: String,
|
||||
@@ -87,7 +134,7 @@ impl LessOpenPreprocessor {
|
||||
None => return input.open(stdin, stdout_identifier),
|
||||
};
|
||||
|
||||
let mut lessopen_command = shell(self.lessopen.replacen("%s", path_str, 1));
|
||||
let mut lessopen_command = shell(shell_substitute(&self.lessopen, path_str));
|
||||
lessopen_command.stdout(Stdio::piped());
|
||||
|
||||
let lessopen_output = match lessopen_command.execute_output() {
|
||||
@@ -122,7 +169,7 @@ impl LessOpenPreprocessor {
|
||||
let mut stdin_buffer = Vec::new();
|
||||
stdin.read_to_end(&mut stdin_buffer)?;
|
||||
|
||||
let mut lessopen_command = shell(self.lessopen.replacen("%s", "-", 1));
|
||||
let mut lessopen_command = shell(shell_substitute(&self.lessopen, "-"));
|
||||
lessopen_command.stdout(Stdio::piped());
|
||||
|
||||
let lessopen_output = match lessopen_command.execute_input_output(&stdin_buffer)
|
||||
@@ -181,7 +228,7 @@ impl LessOpenPreprocessor {
|
||||
lessclose: self
|
||||
.lessclose
|
||||
.as_ref()
|
||||
.map(|s| s.replacen("%s", &path_str, 1).replacen("%s", &stdout, 1)),
|
||||
.map(|s| shell_substitute_two(s, &path_str, &stdout)),
|
||||
}
|
||||
} else {
|
||||
Preprocessed {
|
||||
@@ -189,7 +236,7 @@ impl LessOpenPreprocessor {
|
||||
lessclose: self
|
||||
.lessclose
|
||||
.as_ref()
|
||||
.map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)),
|
||||
.map(|s| shell_substitute_two(s, &path_str, "-")),
|
||||
}
|
||||
},
|
||||
))?,
|
||||
@@ -384,4 +431,65 @@ mod tests {
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -355,13 +355,13 @@ fn test_parse_context_fail() {
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RangeCheckResult {
|
||||
// Within one of the given ranges
|
||||
/// Within one of the given ranges
|
||||
InRange,
|
||||
|
||||
// Before the first range or within two ranges
|
||||
/// Before the first range or within two ranges
|
||||
BeforeOrBetweenRanges,
|
||||
|
||||
// Line number is outside of all ranges and larger than the last range.
|
||||
/// Line number is outside of all ranges and larger than the last range.
|
||||
AfterLastRange,
|
||||
}
|
||||
|
||||
|
||||
+214
-2
@@ -139,16 +139,111 @@ pub fn replace_nonprintable(
|
||||
/// Strips ANSI escape sequences from the input.
|
||||
pub fn strip_ansi(line: &str) -> String {
|
||||
let mut buffer = String::with_capacity(line.len());
|
||||
|
||||
for seq in EscapeSequenceOffsetsIterator::new(line) {
|
||||
if let EscapeSequenceOffsets::Text { .. } = seq {
|
||||
buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -207,11 +302,21 @@ pub fn strip_overstrike(line: &str, first_backspace: usize) -> String {
|
||||
output
|
||||
}
|
||||
|
||||
/// Specify when to strip ANSI escape sequences from the input.
|
||||
///
|
||||
/// Used for both the `--strip-ansi` and the `--sanitize` option, the latter
|
||||
/// implying the former with the same value.
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Default)]
|
||||
pub enum StripAnsiMode {
|
||||
/// Keep escape sequences in the output
|
||||
#[default]
|
||||
Never,
|
||||
|
||||
/// Strip escape sequences from the output
|
||||
Always,
|
||||
|
||||
/// Strip escape sequences unless the syntax highlighting language is plain
|
||||
/// text, in which case they may be part of what the input wants to display
|
||||
Auto,
|
||||
}
|
||||
|
||||
@@ -270,6 +375,113 @@ fn test_strip_ansi() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_ansi_8bit_c1_introducers() {
|
||||
assert_eq!(strip_ansi("a\u{9B}31mRED\u{9B}0mb"), "aREDb");
|
||||
assert_eq!(strip_ansi("a\x1bP1;0|payload\x1b\\b"), "ab");
|
||||
assert_eq!(strip_ansi("a\u{90}body\u{9C}b"), "ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_ansi_single_char_esc() {
|
||||
// strip_ansi must consume both bytes of single-byte ESC sequences (RIS, DECSC, keypad, VT52).
|
||||
assert_eq!(strip_ansi("a\x1bcb"), "ab");
|
||||
assert_eq!(strip_ansi("a\x1b7b\x1b8c"), "abc");
|
||||
assert_eq!(strip_ansi("a\x1b=b\x1b>c"), "abc");
|
||||
assert_eq!(strip_ansi("a\x1bZb"), "ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_ansi_preserves_control_bytes() {
|
||||
// strip_ansi removes only ANSI escape sequences; control bytes pass through.
|
||||
assert_eq!(strip_ansi("safe\rEVIL"), "safe\rEVIL");
|
||||
assert_eq!(strip_ansi("a\x08b\x07c\x0E\x0Fd"), "a\x08b\x07c\x0E\x0Fd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_substitutes_dangerous_bytes() {
|
||||
let r = '\u{FFFD}';
|
||||
assert_eq!(sanitize("safe\rEVIL"), format!("safe{r}EVIL"));
|
||||
assert_eq!(sanitize("a\x08b"), format!("a{r}b"));
|
||||
assert_eq!(sanitize("a\x07b"), format!("a{r}b"));
|
||||
assert_eq!(sanitize("a\x0Bb"), format!("a{r}b"));
|
||||
assert_eq!(sanitize("a\x0Eb\x0Fc"), format!("a{r}b{r}c"));
|
||||
assert_eq!(sanitize("a\u{8D}b"), format!("a{r}b"));
|
||||
assert_eq!(sanitize("a\u{85}b"), format!("a{r}b"));
|
||||
assert_eq!(sanitize("trailing\r"), format!("trailing{r}"));
|
||||
assert_eq!(sanitize("a\x7Fb"), format!("a{r}b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_substitutes_bidi_and_zero_width() {
|
||||
let r = '\u{FFFD}';
|
||||
// Trojan-Source bidi formatting: U+202A..U+202E
|
||||
assert_eq!(sanitize("a\u{202A}b"), format!("a{r}b"));
|
||||
assert_eq!(sanitize("a\u{202E}b"), format!("a{r}b"));
|
||||
// Bidi isolates: U+2066..U+2069
|
||||
assert_eq!(sanitize("a\u{2066}b"), format!("a{r}b"));
|
||||
assert_eq!(sanitize("a\u{2069}b"), format!("a{r}b"));
|
||||
// Zero-width: U+200B..U+200D
|
||||
assert_eq!(sanitize("a\u{200B}b"), format!("a{r}b"));
|
||||
assert_eq!(sanitize("a\u{200D}b"), format!("a{r}b"));
|
||||
// BOM in middle of file: U+FEFF
|
||||
assert_eq!(sanitize("a\u{FEFF}b"), format!("a{r}b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_substitutes_every_bidi_control() {
|
||||
// Unicode Bidi_Control is exactly these 12 codepoints. Covering only some
|
||||
// of them leaves a reordering attack available through the rest.
|
||||
let r = '\u{FFFD}';
|
||||
for c in [
|
||||
'\u{061C}', '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}',
|
||||
'\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}',
|
||||
] {
|
||||
assert_eq!(
|
||||
sanitize(&format!("a{c}b")),
|
||||
format!("a{r}b"),
|
||||
"U+{:04X} was not substituted",
|
||||
c as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_preserves_arabic_sharing_the_alm_lead_byte() {
|
||||
// U+061C is reached via lead byte 0xD8, which also leads ordinary Arabic.
|
||||
assert_eq!(
|
||||
sanitize("\u{0600}\u{061F}\u{06FF}"),
|
||||
"\u{0600}\u{061F}\u{06FF}"
|
||||
);
|
||||
assert_eq!(sanitize("مرحبا بالعالم"), "مرحبا بالعالم");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_preserves_legitimate_bytes() {
|
||||
assert_eq!(sanitize("crlf\r\nline\r\n"), "crlf\r\nline\r\n");
|
||||
assert_eq!(sanitize("a\tb\nc"), "a\tb\nc");
|
||||
assert_eq!(sanitize("plain ascii"), "plain ascii");
|
||||
assert_eq!(sanitize("üñíçödé"), "üñíçödé");
|
||||
// FF (U+000C) passes through; section separator in C source / Emacs Lisp.
|
||||
assert_eq!(sanitize("section1\x0Csection2"), "section1\x0Csection2");
|
||||
// Common Unicode that shares a UTF-8 lead byte with dangerous codepoints
|
||||
// must pass through unchanged.
|
||||
assert_eq!(sanitize("snowman ☃ moon ☾"), "snowman ☃ moon ☾");
|
||||
assert_eq!(sanitize("emoji 🎉 ☃"), "emoji 🎉 ☃");
|
||||
assert_eq!(sanitize("0xC2 lead: ÿ ñ ç"), "0xC2 lead: ÿ ñ ç");
|
||||
assert_eq!(sanitize("CJK 漢字 emoji 🦀"), "CJK 漢字 emoji 🦀");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_strips_ansi() {
|
||||
let r = '\u{FFFD}';
|
||||
// sanitize is a strict superset of strip_ansi.
|
||||
assert_eq!(sanitize("a\x1B[31mb\x1B[0mc"), "abc");
|
||||
assert_eq!(sanitize("a\u{9B}31mb\u{9B}0mc"), "abc");
|
||||
// ANSI then dangerous byte: ANSI gone, byte substituted.
|
||||
assert_eq!(sanitize("\x1B[31mhello\rEVIL"), format!("hello{r}EVIL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_overstrike() {
|
||||
// Bold: X\x08X (same char repeated)
|
||||
|
||||
@@ -192,6 +192,15 @@ impl<'a> PrettyPrinter<'a> {
|
||||
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)
|
||||
pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self {
|
||||
self.config.wrapping_mode = mode;
|
||||
|
||||
+42
-8
@@ -30,7 +30,8 @@ use crate::input::OpenedInput;
|
||||
use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult};
|
||||
use crate::output::OutputHandle;
|
||||
use crate::preprocessor::{
|
||||
expand_tabs, replace_nonprintable, sanitize_for_terminal, strip_ansi, strip_overstrike,
|
||||
expand_tabs, replace_nonprintable, sanitize, sanitize_for_terminal, strip_ansi,
|
||||
strip_overstrike,
|
||||
};
|
||||
use crate::style::StyleComponent;
|
||||
use crate::terminal::{as_terminal_escaped, to_ansi_color};
|
||||
@@ -210,6 +211,7 @@ pub(crate) struct InteractivePrinter<'a> {
|
||||
background_color_highlight: Option<Color>,
|
||||
consecutive_empty_lines: usize,
|
||||
strip_ansi: bool,
|
||||
sanitize: bool,
|
||||
strip_overstrike: bool,
|
||||
}
|
||||
|
||||
@@ -273,7 +275,9 @@ impl<'a> InteractivePrinter<'a> {
|
||||
|
||||
let needs_to_match_syntax = (!is_printing_binary
|
||||
|| matches!(config.binary, BinaryBehavior::AsText))
|
||||
&& (config.colored_output || config.strip_ansi == StripAnsiMode::Auto);
|
||||
&& (config.colored_output
|
||||
|| config.strip_ansi == StripAnsiMode::Auto
|
||||
|| config.sanitize == StripAnsiMode::Auto);
|
||||
|
||||
let (is_plain_text, strip_overstrike, highlighter_from_set) = if needs_to_match_syntax {
|
||||
// Determine the type of syntax for highlighting
|
||||
@@ -319,6 +323,14 @@ impl<'a> InteractivePrinter<'a> {
|
||||
_ => 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 {
|
||||
panel_width,
|
||||
colors,
|
||||
@@ -332,6 +344,7 @@ impl<'a> InteractivePrinter<'a> {
|
||||
background_color_highlight,
|
||||
consecutive_empty_lines: 0,
|
||||
strip_ansi,
|
||||
sanitize,
|
||||
strip_overstrike,
|
||||
})
|
||||
}
|
||||
@@ -603,11 +616,23 @@ impl Printer for InteractivePrinter<'_> {
|
||||
let title = "8<";
|
||||
let title_count = title.chars().count();
|
||||
|
||||
let snip_left = "─ ".repeat((self.config.term_width - panel_count - (title_count / 2)) / 4);
|
||||
let snip_left = "─ ".repeat(
|
||||
self.config
|
||||
.term_width
|
||||
.saturating_sub(panel_count)
|
||||
.saturating_sub(title_count / 2)
|
||||
/ 4,
|
||||
);
|
||||
let snip_left_count = snip_left.chars().count(); // Can't use .len() with Unicode.
|
||||
|
||||
let snip_right =
|
||||
" ─".repeat((self.config.term_width - panel_count - snip_left_count - title_count) / 2);
|
||||
let snip_right = " ─".repeat(
|
||||
self.config
|
||||
.term_width
|
||||
.saturating_sub(panel_count)
|
||||
.saturating_sub(snip_left_count)
|
||||
.saturating_sub(title_count)
|
||||
/ 2,
|
||||
);
|
||||
|
||||
writeln!(
|
||||
handle,
|
||||
@@ -663,8 +688,10 @@ impl Printer for InteractivePrinter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
// If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting.
|
||||
if self.strip_ansi {
|
||||
// Sanitize is the strict superset; otherwise strip-ansi alone.
|
||||
if self.sanitize {
|
||||
line = sanitize(&line).into()
|
||||
} else if self.strip_ansi {
|
||||
line = strip_ansi(&line).into()
|
||||
}
|
||||
|
||||
@@ -711,10 +738,17 @@ impl Printer for InteractivePrinter<'_> {
|
||||
|
||||
// Line decorations.
|
||||
if self.panel_width > 0 {
|
||||
let display_line_number =
|
||||
if self.config.number_nonblank && line.trim_end_matches(['\r', '\n']).is_empty() {
|
||||
0
|
||||
} else {
|
||||
line_number
|
||||
};
|
||||
|
||||
let decorations = self
|
||||
.decorations
|
||||
.iter()
|
||||
.map(|d| d.generate(line_number, false, self));
|
||||
.map(|d| d.generate(display_line_number, false, self));
|
||||
|
||||
for deco in decorations {
|
||||
write!(handle, "{} ", deco.text)?;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# JSON Lines is a simple variation of JSON #2535
|
||||
[mappings]
|
||||
"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson"]
|
||||
"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson", "*.code-workspace"]
|
||||
@@ -0,0 +1,2 @@
|
||||
[mappings]
|
||||
"Makefile" = ["justfile", "Justfile", ".justfile", "*.justfile"]
|
||||
@@ -1,2 +1,2 @@
|
||||
[mappings]
|
||||
"SSH Config" = ["**/.ssh/config"]
|
||||
"SSH Config" = ["**/.ssh/**/config"]
|
||||
@@ -0,0 +1,2 @@
|
||||
[mappings]
|
||||
"INI" = ["/etc/dnf/dnf.conf", "/etc/yum.repos.d/*.repo"]
|
||||
@@ -1,5 +1,6 @@
|
||||
# see `man quadlet`
|
||||
[mappings]
|
||||
"INI" = [
|
||||
"*.quadlets",
|
||||
"**/containers/systemd/**/*.{container,volume,network,kube,image,build,pod,artifact}",
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
[mappings]
|
||||
"INI" = ["**/ghostty/**/*.ghostty", "**/ghostty/themes/*"]
|
||||
+232
-16
@@ -173,8 +173,10 @@ impl Attributes {
|
||||
2 => self.dim = "\x1B[2m".to_owned(),
|
||||
3 => self.italic = "\x1B[3m".to_owned(),
|
||||
4 => self.underline = "\x1B[4m".to_owned(),
|
||||
9 => self.strike = "\x1B[9m".to_owned(),
|
||||
23 => self.italic.clear(),
|
||||
24 => self.underline.clear(),
|
||||
29 => self.strike.clear(),
|
||||
22 => {
|
||||
self.bold.clear();
|
||||
self.dim.clear();
|
||||
@@ -386,12 +388,23 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
|
||||
}
|
||||
|
||||
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
|
||||
self.chars_take_while(|c| c != '\x1B')
|
||||
self.chars_take_while(|c| !is_sequence_introducer(c))
|
||||
.map(|(start, end)| EscapeSequenceOffsets::Text { start, end })
|
||||
}
|
||||
|
||||
fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> {
|
||||
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() {
|
||||
None => Some(EscapeSequenceOffsets::Unknown {
|
||||
start: start_sequence,
|
||||
@@ -400,12 +413,24 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
|
||||
|
||||
Some((_, ']')) => self.next_osc(start_sequence),
|
||||
Some((_, '[')) => self.next_csi(start_sequence),
|
||||
Some((i, c)) => match c {
|
||||
// 7-bit DCS/SOS/PM/APC: `ESC P/X/^/_` introduces a string body.
|
||||
Some((_, 'P' | 'X' | '^' | '_')) => {
|
||||
self.chars.next();
|
||||
self.next_string_terminated_body(start_sequence)
|
||||
}
|
||||
Some(&(i, c)) => match c {
|
||||
'\x20'..='\x2F' => self.next_nf(start_sequence),
|
||||
c => Some(EscapeSequenceOffsets::Unknown {
|
||||
start: start_sequence,
|
||||
end: i + c.len_utf8(),
|
||||
}),
|
||||
c => {
|
||||
// Single-byte ESC sequence (RIS, DECSC/DECRC, keypad, VT52 etc.).
|
||||
let end = match self.chars.next() {
|
||||
Some((j, fc)) => j + fc.len_utf8(),
|
||||
None => i + c.len_utf8(),
|
||||
};
|
||||
Some(EscapeSequenceOffsets::Unknown {
|
||||
start: start_sequence,
|
||||
end,
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -413,12 +438,27 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
|
||||
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");
|
||||
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 end_sequence: usize;
|
||||
|
||||
loop {
|
||||
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
|
||||
// ST is BEL, ESC `\\`, or U+009C.
|
||||
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B' | '\u{9C}')) {
|
||||
None => {
|
||||
start_terminator = self.text.len();
|
||||
end_sequence = start_terminator;
|
||||
@@ -437,6 +477,11 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
|
||||
break;
|
||||
}
|
||||
|
||||
Some((ti, '\u{9C}')) => {
|
||||
end_sequence = ti + '\u{9C}'.len_utf8();
|
||||
break;
|
||||
}
|
||||
|
||||
Some((ti, '\x1B')) => {
|
||||
match self.chars.next() {
|
||||
Some((i, '\\')) => {
|
||||
@@ -466,20 +511,82 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
Some(EscapeSequenceOffsets::OSC {
|
||||
EscapeSequenceOffsets::OSC {
|
||||
start_sequence,
|
||||
start_command: osc_open_index + osc_open_char.len_utf8(),
|
||||
start_command,
|
||||
start_terminator,
|
||||
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> {
|
||||
let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished");
|
||||
debug_assert_eq!(csi_open_char, '[');
|
||||
Some(self.read_csi_body(start_sequence, csi_open_index + csi_open_char.len_utf8()))
|
||||
}
|
||||
|
||||
let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
|
||||
/// CSI body parser entered after the 8-bit introducer U+009B was consumed.
|
||||
fn next_csi_body(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
|
||||
let start_parameters = start_sequence + '\u{9B}'.len_utf8();
|
||||
Some(self.read_csi_body(start_sequence, start_parameters))
|
||||
}
|
||||
|
||||
fn read_csi_body(
|
||||
&mut self,
|
||||
start_sequence: usize,
|
||||
start_parameters: usize,
|
||||
) -> EscapeSequenceOffsets {
|
||||
// Keep iterating while within the range of `0x30-0x3F`.
|
||||
let mut start_intermediates: usize = start_parameters;
|
||||
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) {
|
||||
@@ -498,13 +605,13 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
|
||||
Some((i, c)) => i + c.len_utf8(),
|
||||
};
|
||||
|
||||
Some(EscapeSequenceOffsets::CSI {
|
||||
EscapeSequenceOffsets::CSI {
|
||||
start_sequence,
|
||||
start_parameters,
|
||||
start_intermediates,
|
||||
start_final_byte,
|
||||
end: end_of_sequence,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
|
||||
@@ -543,13 +650,25 @@ impl Iterator for EscapeSequenceOffsetsIterator<'_> {
|
||||
type Item = EscapeSequenceOffsets;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.chars.peek() {
|
||||
Some((_, '\x1B')) => self.next_sequence(),
|
||||
Some((_, c)) if is_sequence_introducer(*c) => self.next_sequence(),
|
||||
Some((_, _)) => self.next_text(),
|
||||
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.
|
||||
///
|
||||
/// ## Example
|
||||
@@ -717,6 +836,79 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_sequence_offsets_iterator_parses_8bit_csi() {
|
||||
let mut iter = EscapeSequenceOffsetsIterator::new("\u{9B}31m");
|
||||
assert_eq!(
|
||||
iter.next(),
|
||||
Some(EscapeSequenceOffsets::CSI {
|
||||
start_sequence: 0,
|
||||
start_parameters: 2,
|
||||
start_intermediates: 4,
|
||||
start_final_byte: 4,
|
||||
end: 5,
|
||||
})
|
||||
);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_sequence_offsets_iterator_parses_8bit_osc_with_8bit_st() {
|
||||
let mut iter = EscapeSequenceOffsetsIterator::new("\u{9D}0;title\u{9C}");
|
||||
assert_eq!(
|
||||
iter.next(),
|
||||
Some(EscapeSequenceOffsets::OSC {
|
||||
start_sequence: 0,
|
||||
start_command: 2,
|
||||
start_terminator: 9,
|
||||
end: 11,
|
||||
})
|
||||
);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_sequence_offsets_iterator_parses_7bit_dcs_consumes_body() {
|
||||
let mut iter = EscapeSequenceOffsetsIterator::new("\x1BP1;0;|payload\x1B\\rest");
|
||||
assert_eq!(
|
||||
iter.next(),
|
||||
Some(EscapeSequenceOffsets::Unknown { start: 0, end: 16 })
|
||||
);
|
||||
assert_eq!(
|
||||
iter.next(),
|
||||
Some(EscapeSequenceOffsets::Text { start: 16, end: 20 })
|
||||
);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_sequence_offsets_iterator_8bit_dcs_consumes_body() {
|
||||
let mut iter = EscapeSequenceOffsetsIterator::new("\u{90}body\u{9C}rest");
|
||||
assert_eq!(
|
||||
iter.next(),
|
||||
Some(EscapeSequenceOffsets::Unknown { start: 0, end: 8 })
|
||||
);
|
||||
assert_eq!(
|
||||
iter.next(),
|
||||
Some(EscapeSequenceOffsets::Text { start: 8, end: 12 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_sequence_offsets_iterator_truncated_dcs_consumes_to_eof() {
|
||||
// Unterminated DCS must still be consumed, not emitted as `Text`.
|
||||
let input = "\x1BPno-terminator";
|
||||
let mut iter = EscapeSequenceOffsetsIterator::new(input);
|
||||
assert_eq!(
|
||||
iter.next(),
|
||||
Some(EscapeSequenceOffsets::Unknown {
|
||||
start: 0,
|
||||
end: input.len(),
|
||||
})
|
||||
);
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_sequence_offsets_iterator_parses_csi() {
|
||||
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m");
|
||||
@@ -922,10 +1114,10 @@ mod tests {
|
||||
fn test_sgr_attributes_do_not_leak_into_wrong_field() {
|
||||
let mut attrs = crate::vscreen::Attributes::new();
|
||||
|
||||
// Bold, Dim, Italic, Underline, Foreground, Background
|
||||
// Bold, Dim, Italic, Underline, Strike, Foreground, Background
|
||||
attrs.update(EscapeSequence::CSI {
|
||||
raw_sequence: "\x1B[1;2;3;4;31;41m",
|
||||
parameters: "1;2;3;4;31;41",
|
||||
raw_sequence: "\x1B[1;2;3;4;9;31;41m",
|
||||
parameters: "1;2;3;4;9;31;41",
|
||||
intermediates: "",
|
||||
final_byte: "m",
|
||||
});
|
||||
@@ -934,6 +1126,7 @@ mod tests {
|
||||
assert_eq!(attrs.dim, "\x1B[2m");
|
||||
assert_eq!(attrs.italic, "\x1B[3m");
|
||||
assert_eq!(attrs.underline, "\x1B[4m");
|
||||
assert_eq!(attrs.strike, "\x1B[9m");
|
||||
assert_eq!(attrs.foreground, "\x1B[31m");
|
||||
assert_eq!(attrs.background, "\x1B[41m");
|
||||
|
||||
@@ -950,4 +1143,27 @@ mod tests {
|
||||
assert_eq!(attrs.foreground, "\x1B[94m");
|
||||
assert_eq!(attrs.background, "\x1B[103m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sgr_strike_is_tracked_and_cleared() {
|
||||
let mut attrs = crate::vscreen::Attributes::new();
|
||||
|
||||
// Strikethrough on (SGR 9) must be tracked so it can be re-emitted.
|
||||
attrs.update(EscapeSequence::CSI {
|
||||
raw_sequence: "\x1B[9m",
|
||||
parameters: "9",
|
||||
intermediates: "",
|
||||
final_byte: "m",
|
||||
});
|
||||
assert_eq!(attrs.strike, "\x1B[9m");
|
||||
|
||||
// Strikethrough off (SGR 29) must clear it, just like italic (23) and underline (24).
|
||||
attrs.update(EscapeSequence::CSI {
|
||||
raw_sequence: "\x1B[29m",
|
||||
parameters: "29",
|
||||
intermediates: "",
|
||||
final_byte: "m",
|
||||
});
|
||||
assert_eq!(attrs.strike, "");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
pub enum WrappingMode {
|
||||
Character,
|
||||
Word,
|
||||
// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
|
||||
/// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
|
||||
NoWrapping(bool),
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
--show-all
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
NAME="${0##*/}"
|
||||
for i in {1..3}; do
|
||||
echo "hello $NAME $i"
|
||||
done
|
||||
+615
-8
@@ -25,10 +25,8 @@ mod unix {
|
||||
use unix::*;
|
||||
|
||||
mod utils;
|
||||
use utils::command::{bat, bat_with_config};
|
||||
use utils::command::{bat, bat_raw_command, bat_raw_command_with_config, bat_with_config};
|
||||
|
||||
#[cfg(unix)]
|
||||
use utils::command::bat_raw_command;
|
||||
use utils::mocked_pagers;
|
||||
|
||||
const EXAMPLES_DIR: &str = "tests/examples";
|
||||
@@ -60,6 +58,67 @@ fn stdin() {
|
||||
.stdout("foo\nbar\n");
|
||||
}
|
||||
|
||||
/// Runs `bat` with `args` while its stdout is a pipe whose read end has
|
||||
/// already been closed, so the very first write fails with `BrokenPipe`
|
||||
/// (EPIPE on Unix, `ERROR_NO_DATA` on Windows).
|
||||
fn assert_quiet_exit_when_stdout_is_closed(command: &mut std::process::Command, args: &[&str]) {
|
||||
use std::process::Stdio;
|
||||
|
||||
let (pipe_reader, pipe_writer) = std::io::pipe().expect("could not create a pipe");
|
||||
drop(pipe_reader);
|
||||
|
||||
let output = command
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(pipe_writer)
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|child| child.wait_with_output())
|
||||
.expect("could not run bat");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"bat {args:?} should exit quietly when stdout is closed, status: {}, stderr: {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
assert!(
|
||||
output.stderr.is_empty(),
|
||||
"bat {args:?} should not print to stderr when stdout is closed, stderr: {}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auxiliary_output_does_not_panic_on_closed_stdout() {
|
||||
// Auxiliary outputs used to be written via `println!`, which panics when
|
||||
// the reader of a piped stdout is already gone (e.g. `bat --config-file |
|
||||
// head -n 0`). The main output path has exited quietly on `BrokenPipe`
|
||||
// since #232; auxiliary outputs should behave the same way.
|
||||
assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--config-file"]);
|
||||
assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--diagnostic"]);
|
||||
|
||||
// The `cache` subcommand is only enabled when the working directory does
|
||||
// not contain a file named `cache` (see `cache_clear`), and it conflicts
|
||||
// with top-level flags like `--no-config`. Run it from a temporary
|
||||
// directory that doubles as `BAT_CACHE_PATH`, leaving the real cache alone.
|
||||
let temp_dir = tempdir().expect("could not create a temporary directory");
|
||||
let mut command = bat_raw_command_with_config();
|
||||
command
|
||||
.current_dir(temp_dir.path())
|
||||
.env("BAT_CONFIG_PATH", temp_dir.path().join("config"))
|
||||
.env("BAT_CACHE_PATH", temp_dir.path());
|
||||
assert_quiet_exit_when_stdout_is_closed(&mut command, &["cache", "--clear"]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "application")]
|
||||
#[test]
|
||||
fn completion_does_not_panic_on_closed_stdout() {
|
||||
for shell in ["bash", "fish", "ps1", "zsh"] {
|
||||
assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--completion", shell]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concatenate() {
|
||||
bat()
|
||||
@@ -213,6 +272,133 @@ fn numbers_honored_from_cli_when_preceeded_by_plain_in_loop_through_mode() {
|
||||
.stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_combined_flags_use_last_number_or_plain_flag() {
|
||||
bat()
|
||||
.arg("multiline.txt")
|
||||
.arg("-npn")
|
||||
.arg("--decorations=auto")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n");
|
||||
|
||||
bat()
|
||||
.arg("multiline.txt")
|
||||
.arg("-pnp")
|
||||
.arg("--decorations=auto")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(
|
||||
"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n",
|
||||
);
|
||||
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("-pbp")
|
||||
.arg("--decorations=auto")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("line 1\n\n\n\nline 5\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n");
|
||||
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("-bpb")
|
||||
.arg("--decorations=auto")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn option_terminator_keeps_filename_from_number_flags() {
|
||||
let tmp_dir = tempdir().expect("can create temporary directory");
|
||||
std::fs::write(tmp_dir.path().join("-b"), "hello\nworld\n").expect("can write temporary file");
|
||||
|
||||
bat()
|
||||
.current_dir(tmp_dir.path())
|
||||
.args(["--", "-b"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("hello\nworld\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attached_short_option_values_are_not_treated_as_flags() {
|
||||
let tmp_dir = tempdir().expect("can create temporary directory");
|
||||
std::fs::write(tmp_dir.path().join("input.txt"), "hello\nworld\n")
|
||||
.expect("can write temporary file");
|
||||
|
||||
bat()
|
||||
.current_dir(tmp_dir.path())
|
||||
.args(["-lbn", "input.txt"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("hello\nworld\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_nonblank_style() {
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("-b")
|
||||
.arg("--decorations=always")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_nonblank_from_cli_in_loop_through_mode() {
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("-b")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_nonblank_takes_precedence_over_number() {
|
||||
// -bn should behave like -b
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("-bn")
|
||||
.arg("--decorations=always")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
|
||||
|
||||
// -nb should also behave like -b
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("-nb")
|
||||
.arg("--decorations=always")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_nonblank_ignored_when_followed_by_plain() {
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("-bp")
|
||||
.arg("--decorations=auto")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("line 1\n\n\n\nline 5\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn piped_output_with_number_nonblank_flag() {
|
||||
bat()
|
||||
.arg("-b")
|
||||
.write_stdin("hello\n\nworld\n")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(" 1 hello\n \n 2 world\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_range_2_3() {
|
||||
bat()
|
||||
@@ -305,6 +491,19 @@ fn line_range_multiple() {
|
||||
.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]
|
||||
fn line_range_multiple_with_context() {
|
||||
bat()
|
||||
@@ -506,6 +705,30 @@ fn piped_output_with_default_style_flag() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_boolean_flag_is_accepted() {
|
||||
// A flag in the config file is prepended to the command line, so a user
|
||||
// who has `--show-all` in their config and also types `-A` ends up passing
|
||||
// it twice. That must not be an error.
|
||||
bat()
|
||||
.arg("empty_lines.txt")
|
||||
.arg("--show-all")
|
||||
.arg("--show-all")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boolean_flag_in_config_and_on_command_line() {
|
||||
bat_with_config()
|
||||
.env("BAT_CONFIG_PATH", "bat-show-all.conf")
|
||||
.arg("empty_lines.txt")
|
||||
.arg("--show-all")
|
||||
.arg("--paging=never")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn squeeze_blank() {
|
||||
bat()
|
||||
@@ -613,12 +836,18 @@ fn list_themes_to_piped_output() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn list_languages() {
|
||||
bat()
|
||||
.arg("--list-languages")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Rust").normalize());
|
||||
mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
|
||||
bat()
|
||||
.env("PAGER", mocked_pagers::from("echo pager-output"))
|
||||
.arg("--list-languages")
|
||||
.arg("--paging=never")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Rust").normalize())
|
||||
.stdout(predicate::str::contains("pager-output").not());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1526,6 +1755,7 @@ fn diagnostic_sanity_check() {
|
||||
.success()
|
||||
.stdout(predicate::str::contains("BAT_PAGER="))
|
||||
.stdout(predicate::str::contains("BAT_WIDTH="))
|
||||
.stdout(predicate::str::contains("BAT_CONFIG_DIR="))
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
@@ -1872,6 +2102,19 @@ fn utf16be() {
|
||||
.stdout(" 1 上一伊刀\n 2 foo bar\n 3 hello world\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16be_first_line_not_merged_with_second() {
|
||||
// The first line of a UTF-16BE file must not swallow the second line
|
||||
bat()
|
||||
.arg("--decorations=always")
|
||||
.arg("--style=numbers")
|
||||
.arg("--color=never")
|
||||
.arg("test_UTF-16BE.txt")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(" 1 hello world\n 2 this is a test\n");
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/sharkdp/bat/issues/1922
|
||||
#[test]
|
||||
fn bom_not_stripped_in_loop_through_mode() {
|
||||
@@ -2145,6 +2388,26 @@ fn header_binary() {
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/sharkdp/bat/issues/3554
|
||||
#[test]
|
||||
fn header_binary_with_null_after_first_line() {
|
||||
let tmp_dir = tempdir().expect("can create temporary directory");
|
||||
let tmp_path = tmp_dir.path().join("encrypted.gpg");
|
||||
std::fs::write(&tmp_path, b"packet-header\npayload\0bytes\n")
|
||||
.expect("can write temporary file");
|
||||
|
||||
bat()
|
||||
.arg(&tmp_path)
|
||||
.arg("--decorations=always")
|
||||
.arg("--style=header")
|
||||
.arg("--line-range=0:0")
|
||||
.arg("--file-name=encrypted.gpg")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("File: encrypted.gpg <BINARY>\n")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_zip_file_is_binary() {
|
||||
let tmp_dir = tempdir().expect("can create temporary directory");
|
||||
@@ -2540,6 +2803,145 @@ fn no_first_line_fallback_when_mapping_to_invalid_syntax() {
|
||||
.stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_detects_bash_from_first_line() {
|
||||
let content = "#!/bin/bash\necho hi\n";
|
||||
|
||||
let detected_output = bat()
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.write_stdin(content)
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let explicit_output = bat()
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.arg("--language=bash")
|
||||
.write_stdin(content)
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
assert_eq!(
|
||||
from_utf8(&detected_output).expect("output is valid utf-8"),
|
||||
from_utf8(&explicit_output).expect("output is valid utf-8")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_detects_diff_from_first_line() {
|
||||
let content = "diff --git a/x b/y\n--- a/x\n+++ b/y\n@@ -1 +1 @@\n-old\n+new\n";
|
||||
|
||||
let detected_output = bat()
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.write_stdin(content)
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let explicit_output = bat()
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.arg("--language=diff")
|
||||
.write_stdin(content)
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
assert_eq!(
|
||||
from_utf8(&detected_output).expect("output is valid utf-8"),
|
||||
from_utf8(&explicit_output).expect("output is valid utf-8")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_stdin_does_not_detect_syntax() {
|
||||
bat()
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.write_stdin("")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("")
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_stdin_does_not_detect_syntax_from_invalid_utf8_first_line() {
|
||||
let content = b"#!/bin/bash\xff\necho hi\n";
|
||||
|
||||
let detected_output = bat()
|
||||
.arg("--binary=as-text")
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.write_stdin(content.as_slice())
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let plain_text_output = bat()
|
||||
.arg("--binary=as-text")
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.arg("--language=txt")
|
||||
.write_stdin(content.as_slice())
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
assert_eq!(
|
||||
from_utf8(&detected_output).expect("output is valid utf-8"),
|
||||
from_utf8(&plain_text_output).expect("output is valid utf-8")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_language_overrides_stdin_first_line_detection() {
|
||||
let content = "diff --git a/x b/y\n--- a/x\n+++ b/y\n@@ -1 +1 @@\n-old\n+new\n";
|
||||
|
||||
let detected_output = bat()
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.write_stdin(content)
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let explicit_output = bat()
|
||||
.arg("--color=always")
|
||||
.arg("--style=plain")
|
||||
.arg("--language=json")
|
||||
.arg("-")
|
||||
.write_stdin(content)
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
assert_ne!(
|
||||
from_utf8(&detected_output).expect("output is valid utf-8"),
|
||||
from_utf8(&explicit_output).expect("output is valid utf-8")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_syntax_is_used_when_no_syntax_is_detected() {
|
||||
let content = "# comment\nfoo=bar\n";
|
||||
@@ -3026,6 +3428,25 @@ fn ansi_sgr_joins_attributes_when_wrapped() {
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
// Ensure that strikethrough (SGR 9) is tracked like the other attributes, so it is
|
||||
// re-emitted (and therefore preserved) across a wrap boundary instead of being dropped.
|
||||
#[test]
|
||||
fn ansi_sgr_strike_joins_attributes_when_wrapped() {
|
||||
bat()
|
||||
.arg("--paging=never")
|
||||
.arg("--color=never")
|
||||
.arg("--terminal-width=20")
|
||||
.arg("--wrap=character")
|
||||
.arg("--decorations=always")
|
||||
.arg("--style=plain")
|
||||
.write_stdin("\x1B[33mColor. \x1B[9mStrike.......Struck+color.\n")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("\x1B[33m\x1B[33mColor. \x1B[9m\x1B[33m\x1B[9mStrike.......\n\x1B[33m\x1B[9mStruck+color.\n")
|
||||
// FIXME: ~~~~~~~~ ~~~~~~~~~~~~~~~ should not be emitted twice.
|
||||
.stderr("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_suffix_arg() {
|
||||
bat()
|
||||
@@ -3831,6 +4252,158 @@ fn strip_ansi_auto_does_not_strip_ansi_when_plain_text_by_option() {
|
||||
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`.
|
||||
#[test]
|
||||
fn style_components_can_be_removed() {
|
||||
@@ -4235,3 +4808,37 @@ fn tcl_shebang_detection_expect() {
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_suffix_enables_first_line_detection() {
|
||||
// A shebang shell script saved with a `.txt` extension is Plain Text by
|
||||
// default (the extension wins). With `--ignored-suffix .txt` the suffix is
|
||||
// stripped before detection, so it falls back to the first line and is
|
||||
// highlighted exactly as if its language were forced to bash. See #2745.
|
||||
let fixture = "regression_tests/issue_2745.txt";
|
||||
let common = ["--color=always", "--decorations=never", "--style=plain"];
|
||||
|
||||
let stdout = |args: &[&str]| -> Vec<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);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[38;2;248;248;242m([0m[38;2;255;255;255mlogging[0m[38;2;248;248;242m)[0m[38;2;248;248;242m {[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mlog[0m[38;2;248;248;242m {[0m
|
||||
[38;2;248;248;242m [0m[38;2;249;38;114moutput[0m[38;2;248;248;242m file /var/log/caddy.log[0m
|
||||
[38;2;248;248;242m }[0m
|
||||
[38;2;248;248;242m}[0m
|
||||
|
||||
[38;2;117;113;94m# a comment[0m
|
||||
[38;2;255;255;255mlocalhost:8080[0m[38;2;249;38;114m,[0m[38;2;248;248;242m [0m[38;2;255;255;255mexample.com[0m[38;2;248;248;242m {[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mroot[0m[38;2;248;248;242m * /var/www/site[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mfile_server[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mreverse_proxy[0m[38;2;248;248;242m /.well-known/matrix/* localhost:8008[0m[38;2;248;248;242m {[0m
|
||||
[38;2;248;248;242m [0m[38;2;249;38;114mheader_up[0m[38;2;248;248;242m Host [0m[38;2;190;132;255m{upstream_hostport}[0m
|
||||
[38;2;248;248;242m }[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mimport[0m[38;2;248;248;242m logging[0m
|
||||
[38;2;248;248;242m}[0m
|
||||
|
||||
[38;2;255;255;255mwww.example.com[0m[38;2;248;248;242m {[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mredir[0m[38;2;248;248;242m https://example.com[0m[38;2;190;132;255m{uri}[0m[38;2;248;248;242m permanent[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mimport[0m[38;2;248;248;242m logging[0m
|
||||
[38;2;248;248;242m}[0m
|
||||
|
||||
[38;2;255;255;255mstatus.example.com[0m[38;2;248;248;242m {[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mreverse_proxy[0m[38;2;248;248;242m localhost:3002[0m
|
||||
[38;2;248;248;242m [0m[38;2;102;217;239mimport[0m[38;2;248;248;242m logging[0m
|
||||
[38;2;248;248;242m}[0m
|
||||
@@ -0,0 +1,5 @@
|
||||
[38;2;248;248;242m{[0m[38;2;255;255;255ma[0m
|
||||
[38;2;248;248;242m [0m[38;2;117;113;94m/*[0m[38;2;117;113;94m comment [0m[38;2;117;113;94m*/[0m
|
||||
[38;2;248;248;242m}[0m
|
||||
|
||||
[38;2;248;248;242m{[0m[38;2;255;255;255ma[0m[38;2;248;248;242m [0m[38;2;117;113;94m/*[0m[38;2;117;113;94m comment [0m[38;2;117;113;94m*/[0m[38;2;248;248;242m}[0m
|
||||
Vendored
File renamed without changes.
+25
@@ -0,0 +1,25 @@
|
||||
(logging) {
|
||||
log {
|
||||
output file /var/log/caddy.log
|
||||
}
|
||||
}
|
||||
|
||||
# a comment
|
||||
localhost:8080, example.com {
|
||||
root * /var/www/site
|
||||
file_server
|
||||
reverse_proxy /.well-known/matrix/* localhost:8008 {
|
||||
header_up Host {upstream_hostport}
|
||||
}
|
||||
import logging
|
||||
}
|
||||
|
||||
www.example.com {
|
||||
redir https://example.com{uri} permanent
|
||||
import logging
|
||||
}
|
||||
|
||||
status.example.com {
|
||||
reverse_proxy localhost:3002
|
||||
import logging
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{a
|
||||
/* comment */
|
||||
}
|
||||
|
||||
{a /* comment */}
|
||||
File renamed without changes.
+37
-28
@@ -1,15 +1,17 @@
|
||||
use gix::actor::SignatureRef;
|
||||
use gix::bstr::BString;
|
||||
use gix::bstr::ByteSlice;
|
||||
use gix::date::time::Format;
|
||||
use gix::date::Time;
|
||||
use gix::objs::tree;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use git2::build::CheckoutBuilder;
|
||||
use git2::Repository;
|
||||
use git2::Signature;
|
||||
|
||||
pub struct BatTester {
|
||||
/// Temporary working directory
|
||||
temp_dir: TempDir,
|
||||
@@ -59,35 +61,42 @@ impl Default for BatTester {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sample_directory() -> Result<TempDir, git2::Error> {
|
||||
fn create_sample_directory() -> Result<TempDir, Box<dyn std::error::Error>> {
|
||||
// Create temp directory and initialize repository
|
||||
let temp_dir = TempDir::new().expect("Temp directory");
|
||||
let repo = Repository::init(&temp_dir)?;
|
||||
let repo = gix::init(&temp_dir)?;
|
||||
let mut tree = gix::objs::Tree::empty();
|
||||
|
||||
// Copy over `sample.rs`
|
||||
let sample_path = temp_dir.path().join("sample.rs");
|
||||
println!("{sample_path:?}");
|
||||
fs::copy("tests/snapshots/sample.rs", &sample_path).expect("successful copy");
|
||||
// Create sample.rs from snapshot file
|
||||
let blob_id = repo.write_blob_stream(File::open("tests/snapshots/sample.rs")?)?;
|
||||
let entry = tree::Entry {
|
||||
mode: tree::EntryMode::from(tree::EntryKind::Blob),
|
||||
oid: blob_id.object()?.id,
|
||||
filename: BString::from("sample.rs"),
|
||||
};
|
||||
tree.entries.push(entry);
|
||||
let tree_id = repo.write_object(tree)?;
|
||||
|
||||
// Commit
|
||||
let mut index = repo.index()?;
|
||||
index.add_path(Path::new("sample.rs"))?;
|
||||
|
||||
let oid = index.write_tree()?;
|
||||
let signature = Signature::now("bat test runner", "bat@test.runner")?;
|
||||
let tree = repo.find_tree(oid)?;
|
||||
let _ = repo.commit(
|
||||
Some("HEAD"), // point HEAD to our new commit
|
||||
&signature, // author
|
||||
&signature, // committer
|
||||
let author = SignatureRef {
|
||||
name: "test".as_bytes().as_bstr(),
|
||||
email: "test@test.test".as_bytes().as_bstr(),
|
||||
time: &Time::now_local_or_utc().format_or_unix(Format::Raw),
|
||||
};
|
||||
let commit_id = repo.commit_as(
|
||||
author,
|
||||
author,
|
||||
"HEAD",
|
||||
"initial commit",
|
||||
&tree,
|
||||
&[],
|
||||
);
|
||||
let mut opts = CheckoutBuilder::new();
|
||||
repo.checkout_head(Some(opts.force()))?;
|
||||
tree_id,
|
||||
gix::commit::NO_PARENT_IDS,
|
||||
)?;
|
||||
assert_eq!(commit_id, repo.head_id()?);
|
||||
|
||||
fs::copy("tests/snapshots/sample.modified.rs", &sample_path).expect("successful copy");
|
||||
fs::copy(
|
||||
"tests/snapshots/sample.modified.rs",
|
||||
temp_dir.path().join("sample.rs"),
|
||||
)
|
||||
.expect("successful copy");
|
||||
|
||||
Ok(temp_dir)
|
||||
}
|
||||
Reference in new issue
Block a user