Compare commits

..

No commits in common. "master" and "v0.24.0" have entirely different histories.

115 changed files with 1120 additions and 6533 deletions

View File

@ -26,4 +26,4 @@ guidelines for adding new syntaxes:
[Name or description of the syntax/language here]
**Guideline Criteria:**
[packagecontrol.io link here]
[packagecontro.io link here]

View File

@ -1,23 +0,0 @@
# This workflow triggers auto-merge of any PR that dependabot creates so that
# PRs will be merged automatically without maintainer intervention if CI passes
name: Auto-merge dependabot PRs
on:
pull_request_target:
types: [opened]
jobs:
auto-merge:
if: github.repository == 'sharkdp/bat' && startsWith(github.head_ref, 'dependabot/')
runs-on: ubuntu-latest
environment:
name: auto-merge
url: https://github.com/sharkdp/bat/blob/main/.github/workflows/Auto-merge-dependabot-PRs.yml
env:
GITHUB_TOKEN: ${{ secrets.AUTO_MERGE_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
- run: |
gh pr review ${{ github.event.pull_request.number }} --comment --body "If CI passes, this dependabot PR will be [auto-merged](https://github.com/sharkdp/bat/blob/main/.github/workflows/Auto-merge-dependabot-PRs.yml) 🚀"
- run: |
gh pr merge --auto --squash ${{ github.event.pull_request.number }}

View File

@ -163,19 +163,17 @@ jobs:
fail-fast: false
matrix:
job:
- { target: aarch64-unknown-linux-musl , os: ubuntu-20.04, dpkg_arch: arm64, use-cross: true }
- { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, dpkg_arch: arm64, use-cross: true }
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, dpkg_arch: armhf, use-cross: true }
- { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, dpkg_arch: musl-linux-armhf, use-cross: true }
- { target: i686-pc-windows-msvc , os: windows-2019, }
- { target: i686-unknown-linux-gnu , os: ubuntu-20.04, dpkg_arch: i686, use-cross: true }
- { target: i686-unknown-linux-musl , os: ubuntu-20.04, dpkg_arch: musl-linux-i686, use-cross: true }
- { target: x86_64-apple-darwin , os: macos-12, }
- { target: aarch64-apple-darwin , os: macos-14, }
- { target: x86_64-pc-windows-gnu , os: windows-2019, }
- { target: x86_64-pc-windows-msvc , os: windows-2019, }
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, dpkg_arch: amd64, use-cross: true }
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, dpkg_arch: musl-linux-amd64, use-cross: true }
- { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
- { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
- { target: i686-pc-windows-msvc , os: windows-2019 }
- { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
- { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
- { target: x86_64-apple-darwin , os: macos-12 }
- { target: x86_64-pc-windows-gnu , os: windows-2019 }
- { target: x86_64-pc-windows-msvc , os: windows-2019 }
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
env:
BUILD_CMD: cargo
steps:
@ -339,7 +337,16 @@ jobs:
DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl
case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
DPKG_VERSION=${{ needs.crate_metadata.outputs.version }}
DPKG_ARCH="${{ matrix.job.dpkg_arch }}"
unset DPKG_ARCH
case ${{ matrix.job.target }} in
aarch64-*-linux-*) DPKG_ARCH=arm64 ;;
arm-*-linux-*hf) DPKG_ARCH=armhf ;;
i686-*-linux-*) DPKG_ARCH=i686 ;;
x86_64-*-linux-*) DPKG_ARCH=amd64 ;;
*) DPKG_ARCH=notset ;;
esac;
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT
@ -446,7 +453,7 @@ jobs:
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
- name: Publish archives and packages
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
if: steps.is-release.outputs.IS_RELEASE
with:
files: |

View File

@ -1,33 +0,0 @@
name: Changelog
on:
pull_request:
jobs:
check-changelog:
name: Check for changelog entry
runs-on: ubuntu-latest
# dependabot PRs are automerged if CI passes; we shouldn't block these
if: github.actor != 'dependabot[bot]'
env:
PR_NUMBER: ${{ github.event.number }}
PR_BASE: ${{ github.base_ref }}
steps:
- uses: actions/checkout@v4
- name: Fetch PR base
run: git fetch --no-tags --prune --depth=1 origin
# cannot use `github.actor`: the triggering commit may be authored by a maintainer
- name: Get PR submitter
id: get-submitter
run: curl -sSfL https://api.github.com/repos/sharkdp/bat/pulls/${PR_NUMBER} | jq -r '"submitter=" + .user.login' | tee -a $GITHUB_OUTPUT
- name: Search for added line in changelog
env:
PR_SUBMITTER: ${{ steps.get-submitter.outputs.submitter }}
run: |
ADDED=$(git diff -U0 "origin/${PR_BASE}" HEAD -- CHANGELOG.md | grep -P '^\+[^\+].+$')
echo "Added lines in CHANGELOG.md:"
echo "$ADDED"
echo "Grepping for PR info (see CONTRIBUTING.md):"
grep "#${PR_NUMBER}\\b.*@${PR_SUBMITTER}\\b" <<< "$ADDED"

3
.gitmodules vendored
View File

@ -260,6 +260,3 @@
[submodule "assets/syntaxes/02_Extra/vscode-wgsl"]
path = assets/syntaxes/02_Extra/vscode-wgsl
url = https://github.com/PolyMeilex/vscode-wgsl.git
[submodule "assets/syntaxes/02_Extra/CFML"]
path = assets/syntaxes/02_Extra/CFML
url = https://github.com/jcberquist/sublimetext-cfml.git

View File

@ -1,77 +1,3 @@
# unreleased
## Features
- Set terminal title to file names when Paging is not Paging::Never #2807 (@Oliver-Looney)
- `bat --squeeze-blank`/`bat -s` will now squeeze consecutive empty lines, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar)
- `bat --strip-ansi={never,always,auto}` to remove ANSI escape sequences from bat's input, see #2999 (@eth-p)
- Add or remove individual style components without replacing all styles #2929 (@eth-p)
## Bugfixes
- Fix long file name wrapping in header, see #2835 (@FilipRazek)
- Fix `NO_COLOR` support, see #2767 (@acuteenvy)
- Fix handling of inputs with OSC ANSI escape sequences, see #2541 and #2544 (@eth-p)
- Fix handling of inputs with combined ANSI color and attribute sequences, see #2185 and #2856 (@eth-p)
- Fix panel width when line 10000 wraps, see #2854 (@eth-p)
- Fix compile issue of `time` dependency caused by standard library regression #3045 (@cyqsimon)
## Other
- Upgrade to Rust 2021 edition #2748 (@cyqsimon)
- Refactor and cleanup build script #2756 (@cyqsimon)
- Checks changelog has been written to for PRs in CI #2766 (@cyqsimon)
- Use GitHub API to get correct PR submitter #2791 (@cyqsimon)
- Minor benchmark script improvements #2768 (@cyqsimon)
- Update Arch Linux package URL in README files #2779 (@brunobell)
- Update and improve `zsh` completion, see #2772 (@okapia)
- More extensible syntax mapping mechanism #2755 (@cyqsimon)
- Use proper Architecture for Debian packages built for musl, see #2811 (@Enselic)
- Pull in fix for unsafe-libyaml security advisory, see #2812 (@dtolnay)
- Update git-version dependency to use Syn v2, see #2816 (@dtolnay)
- Update git2 dependency to v0.18.2, see #2852 (@eth-p)
- Improve performance when color output disabled, see #2397 and #2857 (@eth-p)
- Relax syntax mapping rule restrictions to allow brace expansion #2865 (@cyqsimon)
- Apply clippy fixes #2864 (@cyqsimon)
- Faster startup by offloading glob matcher building to a worker thread #2868 (@cyqsimon)
- Display which theme is the default one in basic output (no colors), see #2937 (@sblondon)
- Display which theme is the default one in colored output, see #2838 (@sblondon)
- Add aarch64-apple-darwin ("Apple Silicon") binary tarballs to releases, see #2967 (@someposer)
- Update the Lisp syntax, see #2970 (@ccqpein)
- Use bat's ANSI iterator during tab expansion, see #2998 (@eth-p)
- Support 'statically linked binary' for aarch64 in 'Release' page, see #2992 (@tzq0301)
- Update options in shell completions and the man page of `bat`, see #2995 (@akinomyoga)
## Syntaxes
- `cmd-help`: scope subcommands followed by other terms, and other misc improvements, see #2819 (@victor-gp)
- Upgrade JQ syntax, see #2820 (@dependabot[bot])
- Add syntax mapping for quadman quadlets #2866 (@cyqsimon)
- Map containers .conf files to TOML syntax #2867 (@cyqsimon)
- Associate `.xsh` files with `xonsh` syntax that is Python, see #2840 (@anki-code)
- Associate JSON with Comments `.jsonc` with `json` syntax, see #2795 (@mxaddict)
- Associate JSON-LD `.jsonld` files with `json` syntax, see #3037 (@vorburger)
- Associate `.textproto` files with `ProtoBuf` syntax, see #3038 (@vorburger)
- Associate GeoJSON `.geojson` files with `json` syntax, see #3084 (@mvaaltola)
- Associate `.aws/{config,credentials}`, see #2795 (@mxaddict)
- Associate Wireguard config `/etc/wireguard/*.conf`, see #2874 (@cyqsimon)
- Add support for [CFML](https://www.adobe.com/products/coldfusion-family.html), see #3031 (@brenton-at-pieces)
- Map `*.mkd` files to `Markdown` syntax, see issue #3060 and PR #3061 (@einfachIrgendwer0815)
## Themes
## `bat` as a library
- Changes to `syntax_mapping::SyntaxMapping` #2755 (@cyqsimon)
- `SyntaxMapping::get_syntax_for` is now correctly public
- [BREAKING] `SyntaxMapping::{empty,builtin}` are removed; use `SyntaxMapping::new` instead
- [BREAKING] `SyntaxMapping::mappings` is replaced by `SyntaxMapping::{builtin,custom,all}_mappings`
- Make `Controller::run_with_error_handler`'s error handler `FnMut`, see #2831 (@rhysd)
- Improve compile time by 20%, see #2815 (@dtolnay)
# v0.24.0
## Features

View File

@ -6,42 +6,21 @@ Thank you for considering to contribute to `bat`!
## Add an entry to the changelog
Keeping the [`CHANGELOG.md`](CHANGELOG.md) file up-to-date makes the release
process much easier and therefore helps to get your changes into a new `bat`
release faster. However, not every change to the repository requires a
changelog entry. Below are a few examples of that.
Please update the changelog if your contribution contains changes regarding
any of the following:
- the behavior of `bat`
- syntax mappings
- syntax definitions
- themes
- the build system, linting, or CI workflows
A changelog entry is not necessary when:
- updating documentation
- fixing typos
>[!NOTE]
> For PRs, a CI workflow verifies that a suitable changelog entry is
> added. If such an entry is missing, the workflow will fail. If your
> changes do not need an entry to the changelog (see above), that
> workflow failure can be disregarded.
### Changelog entry format
If your contribution changes the behavior of `bat` (as opposed to a typo-fix
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md) file
and describe your changes. This makes the release process much easier and
therefore helps to get your changes into a new `bat` release faster.
The top of the `CHANGELOG` contains a *"unreleased"* section with a few
subsections (Features, Bugfixes, …). Please add your entry to the subsection
that best describes your change.
Entries must follow this format:
Entries follow this format:
```
- Short description of what has been changed, see #123 (@user)
```
Please replace `#123` with the number of your pull request (not issue) and
`@user` by your GitHub username.
Here, `#123` is the number of the original issue and/or your pull request.
Please replace `@user` by your GitHub username.
## Development

639
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ name = "bat"
repository = "https://github.com/sharkdp/bat"
version = "0.24.0"
exclude = ["assets/syntaxes/*", "assets/themes/*"]
build = "build/main.rs"
edition = '2021'
build = "build.rs"
edition = '2018'
rust-version = "1.70"
[features]
@ -41,33 +41,32 @@ regex-onig = ["syntect/regex-onig"] # Use the "oniguruma" regex engine
regex-fancy = ["syntect/regex-fancy"] # Use the rust-only "fancy-regex" engine
[dependencies]
nu-ansi-term = "0.50.0"
nu-ansi-term = "0.49.0"
ansi_colours = "^1.2"
bincode = "1.0"
console = "0.15.8"
console = "0.15.5"
flate2 = "1.0"
once_cell = "1.19"
once_cell = "1.18"
thiserror = "1.0"
wild = { version = "2.2", optional = true }
wild = { version = "2.1", optional = true }
content_inspector = "0.2.4"
shell-words = { version = "1.1.0", optional = true }
unicode-width = "0.1.13"
unicode-width = "0.1.10"
globset = "0.4"
serde = "1.0"
serde_derive = "1.0"
serde_yaml = "0.9.28"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
semver = "1.0"
path_abs = { version = "0.5", default-features = false }
clircle = "0.5"
clircle = "0.4"
bugreport = { version = "0.5.0", optional = true }
etcetera = { version = "0.8.0", optional = true }
grep-cli = { version = "0.1.10", optional = true }
regex = { version = "1.10.2", optional = true }
walkdir = { version = "2.5", optional = true }
grep-cli = { version = "0.1.9", optional = true }
regex = { version = "1.8.3", optional = true }
walkdir = { version = "2.3", optional = true }
bytesize = { version = "1.3.0" }
encoding_rs = "0.8.34"
os_str_bytes = { version = "~7.0", optional = true }
run_script = { version = "^0.10.1", optional = true}
encoding_rs = "0.8.33"
os_str_bytes = { version = "~6.4", optional = true }
run_script = { version = "^0.10.0", optional = true}
[dependencies.git2]
version = "0.18"
@ -75,45 +74,32 @@ optional = true
default-features = false
[dependencies.syntect]
version = "5.2.0"
version = "5.0.0"
default-features = false
features = ["parsing"]
[dependencies.clap]
version = "4.4.12"
version = "4.4.6"
optional = true
features = ["wrap_help", "cargo"]
[target.'cfg(target_os = "macos")'.dependencies]
home = "0.5.9"
plist = "1.7.0"
home = "0.5.4"
plist = "1.4.3"
[dev-dependencies]
assert_cmd = "2.0.12"
expect-test = "1.5.0"
assert_cmd = "2.0.10"
expect-test = "1.4.1"
serial_test = { version = "2.0.0", default-features = false }
predicates = "3.1.0"
predicates = "3.0.3"
wait-timeout = "0.2.0"
tempfile = "3.8.1"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.8.0"
[target.'cfg(unix)'.dev-dependencies]
nix = { version = "0.26.4", default-features = false, features = ["term"] }
[build-dependencies]
anyhow = "1.0.86"
indexmap = { version = "2.3.0", features = ["serde"] }
itertools = "0.13.0"
once_cell = "1.18"
regex = "1.10.2"
serde = "1.0"
serde_derive = "1.0"
serde_with = { version = "3.8.1", default-features = false, features = ["macros"] }
toml = { version = "0.8.9", features = ["preserve_order"] }
walkdir = "2.5"
nix = { version = "0.26.2", default-features = false, features = ["term"] }
[build-dependencies.clap]
version = "4.4.12"
version = "4.4.6"
optional = true
features = ["wrap_help", "cargo"]

View File

@ -254,7 +254,7 @@ Please report any issues with the help syntax in [this repository](https://githu
## Installation
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
### On Ubuntu (using `apt`)
*... and other Debian-based Linux distributions.*
@ -296,7 +296,7 @@ apk add bat
### On Arch Linux
You can install [the `bat` package](https://www.archlinux.org/packages/extra/x86_64/bat/)
You can install [the `bat` package](https://www.archlinux.org/packages/community/x86_64/bat/)
from the official sources:
```bash
@ -373,14 +373,6 @@ You can install `bat` using the [nix package manager](https://nixos.org/nix):
nix-env -i bat
```
### Via flox
You can install `bat` using [Flox](https://flox.dev)
```bash
flox install bat
```
### On openSUSE
You can install `bat` with zypper:
@ -515,16 +507,6 @@ and line numbers but no grid and no file header. Set the `BAT_STYLE` environment
variable to make these changes permanent or use `bat`s
[configuration file](https://github.com/sharkdp/bat#configuration-file).
>[!tip]
> If you specify a default style in `bat`'s config file, you can change which components
> are displayed during a single run of `bat` using the `--style` command-line argument.
> By prefixing a component with `+` or `-`, it can be added or removed from the current style.
>
> For example, if your config contains `--style=full,-snip`, you can run bat with
> `--style=-grid,+snip` to remove the grid and add back the `snip` component.
> Or, if you want to override the styles completely, you use `--style=numbers` to
> only show the line numbers.
### Adding new syntaxes / language definitions
Should you find that a particular syntax is not available within `bat`, you can follow these
@ -620,8 +602,7 @@ set, `less` is used by default. If you want to use a different pager, you can ei
`PAGER` variable or set the `BAT_PAGER` environment variable to override what is specified in
`PAGER`.
>[!NOTE]
> If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors.
**Note**: If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors.
If you want to pass command-line arguments to the pager, you can also set them via the
`PAGER`/`BAT_PAGER` variables:
@ -632,37 +613,20 @@ export BAT_PAGER="less -RF"
Instead of using environment variables, you can also use `bat`s [configuration file](https://github.com/sharkdp/bat#configuration-file) to configure the pager (`--pager` option).
**Note**: By default, if the pager is set to `less` (and no command-line options are specified),
`bat` will pass the following command line options to the pager: `-R`/`--RAW-CONTROL-CHARS`,
`-F`/`--quit-if-one-screen` and `-X`/`--no-init`. The last option (`-X`) is only used for `less`
versions older than 530.
### Using `less` as a pager
The `-R` option is needed to interpret ANSI colors correctly. The second option (`-F`) instructs
less to exit immediately if the output size is smaller than the vertical size of the terminal.
This is convenient for small files because you do not have to press `q` to quit the pager. The
third option (`-X`) is needed to fix a bug with the `--quit-if-one-screen` feature in old versions
of `less`. Unfortunately, it also breaks mouse-wheel support in `less`.
When using `less` as a pager, `bat` will automatically pass extra options along to `less`
to improve the experience. Specifically, `-R`/`--RAW-CONTROL-CHARS`, `-F`/`--quit-if-one-screen`,
and under certain conditions, `-X`/`--no-init` and/or `-S`/`--chop-long-lines`.
>[!IMPORTANT]
> These options will not be added if:
> - The pager is not named `less`.
> - The `--pager` argument contains any command-line arguments (e.g. `--pager="less -R"`).
> - The `BAT_PAGER` environment variable contains any command-line arguments (e.g. `export BAT_PAGER="less -R"`)
>
> The `--quit-if-one-screen` option will not be added when:
> - The `--paging=always` argument is used.
> - The `BAT_PAGING` environment is set to `always`.
The `-R` option is needed to interpret ANSI colors correctly.
The `-F` option instructs `less` to exit immediately if the output size is smaller than
the vertical size of the terminal. This is convenient for small files because you do not
have to press `q` to quit the pager.
The `-X` option is needed to fix a bug with the `--quit-if-one-screen` feature in versions
of `less` older than version 530. Unfortunately, it also breaks mouse-wheel support in `less`.
If you want to enable mouse-wheel scrolling on older versions of `less` and do not mind losing
the quit-if-one-screen feature, you can set the pager (via `--pager` or `BAT_PAGER`) to `less -R`.
For `less` 530 or newer, it should work out of the box.
The `-S` option is added when `bat`'s `-S`/`--chop-long-lines` option is used. This tells `less`
to truncate any lines larger than the terminal width.
If you want to enable mouse-wheel scrolling on older versions of `less`, you can pass just `-R` (as
in the example above, this will disable the quit-if-one-screen feature). For less 530 or newer,
it should work out of the box.
### Indentation
@ -747,7 +711,7 @@ your `PATH` or [define an environment variable](#using-a-different-pager). The [
Windows 10 natively supports colors in both `conhost.exe` (Command Prompt) and PowerShell since
[v1511](https://en.wikipedia.org/wiki/Windows_10_version_history#Version_1511_(November_Update)), as
well as in newer versions of bash. On earlier versions of Windows, you can use
[Cmder](http://cmder.app/), which includes [ConEmu](https://conemu.github.io/).
[Cmder](http://cmder.net/), which includes [ConEmu](https://conemu.github.io/).
**Note:** Old versions of `less` do not correctly interpret colors on Windows. To fix this, you can add the optional Unix tools to your PATH when installing Git. If you dont have any other pagers installed, you can disable paging entirely by passing `--paging=never` or by setting `BAT_PAGER` to an empty string.
@ -777,14 +741,9 @@ bat() {
If an input file contains color codes or other ANSI escape sequences or control characters, `bat` will have problems
performing syntax highlighting and text wrapping, and thus the output can become garbled.
If your version of `bat` supports the `--strip-ansi=auto` option, it can be used to remove such sequences
before syntax highlighting. Alternatively, you may disable both syntax highlighting and wrapping by
When displaying such files it is recommended to disable both syntax highlighting and wrapping by
passing the `--color=never --wrap=never` options to `bat`.
> [!NOTE]
> The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text.
### Terminals & colors
`bat` handles terminals *with* and *without* truecolor support. However, the colors in most syntax

View File

@ -76,7 +76,6 @@ _bat() {
-m | --map-syntax | \
--ignored-suffix | \
--list-themes | \
--squeeze-limit | \
--line-range | \
-L | --list-languages | \
--lessopen | \
@ -158,7 +157,6 @@ _bat() {
--diff-context
--tabs
--wrap
--chop-long-lines
--terminal-width
--number
--color
@ -171,24 +169,18 @@ _bat() {
--ignored-suffix
--theme
--list-themes
--squeeze-blank
--squeeze-limit
--style
--line-range
--list-languages
--lessopen
--diagnostic
--acknowledgements
--set-terminal-title
--help
--version
--cache-dir
--config-dir
--config-file
--generate-config-file
--no-config
--no-custom-assets
--no-lessopen
" -- "$cur"))
return 0
fi

View File

@ -133,8 +133,6 @@ set -l tabs_opts '
complete -c $bat -l acknowledgements -d "Print acknowledgements" -n __fish_is_first_arg
complete -c $bat -l cache-dir -f -d "Show bat's cache directory" -n __fish_is_first_arg
complete -c $bat -l color -x -a "$color_opts" -d "When to use colored output" -n __bat_no_excl_args
complete -c $bat -l config-dir -f -d "Display location of configuration directory" -n __fish_is_first_arg
@ -149,8 +147,6 @@ complete -c $bat -s d -l diff -d "Only show lines with Git changes" -n __bat_no_
complete -c $bat -l diff-context -x -d "Show N context lines around Git changes" -n "__fish_seen_argument -s d -l diff"
complete -c $bat -l generate-config-file -f -d "Generates a default configuration file" -n __fish_is_first_arg
complete -c $bat -l file-name -x -d "Specify the display name" -n __bat_no_excl_args
complete -c $bat -s f -l force-colorization -d "Force color and decorations" -n __bat_no_excl_args
@ -177,12 +173,6 @@ complete -c $bat -l list-themes -f -d "List syntax highlighting themes" -n __fis
complete -c $bat -s m -l map-syntax -x -a "(__bat_complete_map_syntax)" -d "Map <glob pattern>:<language syntax>" -n __bat_no_excl_args
complete -c $bat -l no-config -d "Do not use the configuration file"
complete -c $bat -l no-custom-assets -d "Do not load custom assets"
complete -c $bat -l no-lessopen -d "Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)"
complete -c $bat -s n -l number -d "Only show line numbers, no other decorations" -n __bat_no_excl_args
complete -c $bat -l pager -x -a "$pager_opts" -d "Which pager to use" -n __bat_no_excl_args

View File

@ -1,20 +1,19 @@
#compdef {{PROJECT_EXECUTABLE}}
local curcontext="$curcontext" ret=1
local -a state state_descr line
local context state state_descr line
typeset -A opt_args
(( $+functions[_{{PROJECT_EXECUTABLE}}_cache_subcommand] )) ||
_{{PROJECT_EXECUTABLE}}_cache_subcommand() {
local -a args
args=(
'(-b --build -c --clear)'{-b,--build}'[initialize or update the syntax/theme cache]'
'(-b --build -c --clear)'{-c,--clear}'[remove the cached syntax definitions and themes]'
--source='[specify directory to load syntaxes and themes from]:directory:_files -/'
--target='[specify directory to store the cached syntax and theme set in]:directory:_files -/'
--blank'[create completely new syntax and theme sets]'
--acknowledgements'[build acknowledgements.bin]'
'(: -)'{-h,--help}'[show help information]'
'(-b --build -c --clear)'{-b,--build}'[Initialize or update the syntax/theme cache]'
'(-b --build -c --clear)'{-c,--clear}'[Remove the cached syntax definitions and themes]'
'(--source)'--source='[Use a different directory to load syntaxes and themes from]:directory:_files -/'
'(--target)'--target='[Use a different directory to store the cached syntax and theme set]:directory:_files -/'
'(--blank)'--blank'[Create completely new syntax and theme sets]'
'(: -)'{-h,--help}'[Prints help information]'
'*: :'
)
_arguments -S -s $args
@ -24,80 +23,69 @@ _{{PROJECT_EXECUTABLE}}_cache_subcommand() {
_{{PROJECT_EXECUTABLE}}_main() {
local -a args
args=(
'(-A --show-all)'{-A,--show-all}'[show non-printable characters (space, tab, newline, ..)]'
--nonprintable-notation='[specify how to display non-printable characters when using --show-all]:notation:(caret unicode)'
\*{-p,--plain}'[show plain style (alias for `--style=plain`), repeat twice to disable disable automatic paging (alias for `--paging=never`)]'
'(-l --language)'{-l+,--language=}'[set the language for syntax highlighting]:language:->languages'
\*{-H+,--highlight-line=}'[highlight specified block of lines]:start\:end'
\*--file-name='[specify the name to display for a file]:name:_files'
'(-d --diff)'--diff'[only show lines that have been added/removed/modified]'
--diff-context='[specify lines of context around added/removed/modified lines when using `--diff`]:lines'
--tabs='[set the tab width]:tab width [4]'
--wrap='[specify the text-wrapping mode]:mode [auto]:(auto never character)'
'!(--wrap)'{-S,--chop-long-lines}
--terminal-width='[explicitly set the width of the terminal instead of determining it automatically]:width'
'(-n --number --diff --diff-context)'{-n,--number}'[show line numbers]'
--color='[specify when to use colors]:when:(auto never always)'
--italic-text='[use italics in output]:when:(always never)'
--decorations='[specify when to show the decorations]:when:(auto never always)'
--paging='[specify when to use the pager]:when:(auto never always)'
'(-m --map-syntax)'{-m+,--map-syntax=}'[map a glob pattern to an existing syntax name]: :->syntax-maps'
'(--theme)'--theme='[set the color theme for syntax highlighting]:theme:->themes'
'(: --list-themes --list-languages -L)'--list-themes'[show all supported highlighting themes]'
--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'
'(* -)'{-L,--list-languages}'[display all supported languages]'
-P'[disable paging]'
"--no-config[don't use the configuration file]"
"--no-custom-assets[don't load custom assets]"
'(--no-lessopen)'--lessopen'[enable the $LESSOPEN preprocessor]'
'(--lessopen)'--no-lessopen'[disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)]'
'(* -)'--config-dir"[show bat's configuration directory]"
'(* -)'--config-file'[show path to the configuration file]'
'(* -)'--generate-config-file'[generate a default configuration file]'
'(* -)'--cache-dir"[show bat's cache directory]"
'(* -)'{-h,--help}'[show help information]'
'(* -)'{-V,--version}'[show version information]'
'*: :{ _files || compadd cache }'
'(-A --show-all)'{-A,--show-all}'[Show non-printable characters (space, tab, newline, ..)]'
'*'{-p,--plain}'[Show plain style (alias for `--style=plain`), repeat twice to disable disable automatic paging (alias for `--paging=never`)]'
'(-l --language)'{-l+,--language=}'[Set the language for syntax highlighting]:<language>:->language'
'(-H --highlight-line)'{-H,--highlight-line}'[Highlight lines N through M]:<N\:M>...'
'(--file-name)'--file-name'[Specify the name to display for a file]:<name>...:_files'
'(-d --diff)'--diff'[Only show lines that have been added/removed/modified]'
'(--diff-context)'--diff-context'[Include N lines of context around added/removed/modified lines when using `--diff`]:<N> (lines):()'
'(--tabs)'--tabs'[Set the tab width to T spaces]:<T> (tab width):()'
'(--wrap)'--wrap='[Specify the text-wrapping mode]:<when>:(auto never character)'
'(--terminal-width)'--terminal-width'[Explicitly set the width of the terminal instead of determining it automatically]:<width>'
'(-n --number)'{-n,--number}'[Show line numbers]'
'(--color)'--color='[When to use colors]:<when>:(auto never always)'
'(--italic-text)'--italic-text='[Use italics in output]:<when>:(always never)'
'(--decorations)'--decorations='[When to show the decorations]:<when>:(auto never always)'
'(--paging)'--paging='[Specify when to use the pager]:<when>:(auto never always)'
'(-m --map-syntax)'{-m+,--map-syntax=}'[Use the specified syntax for files matching the glob pattern]:<glob\:syntax>...'
'(--theme)'--theme='[Set the color theme for syntax highlighting]:<theme>:->theme'
'(: --list-themes --list-languages -L)'--list-themes'[Display all supported highlighting themes]'
'(--style)'--style='[Comma-separated list of style elements to display]:<components>:->style'
'(-r --line-range)'{-r+,--line-range=}'[Only print the lines from N to M]:<N\:M>...'
'(: --list-themes --list-languages -L)'{-L,--list-languages}'[Display all supported languages]'
'(: --no-config)'--no-config'[Do not use the configuration file]'
'(: --no-custom-assets)'--no-custom-assets'[Do not load custom assets]'
'(: --lessopen)'--lessopen'[Enable the $LESSOPEN preprocessor]'
'(: --no-lessopen)'--no-lessopen'[Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)]'
'(: --config-dir)'--config-dir'[Show bat'"'"'s configuration directory]'
'(: --config-file)'--config-file'[Show path to the configuration file]'
'(: --generate-config-file)'--generate-config-file'[Generates a default configuration file]'
'(: --cache-dir)'--cache-dir'[Show bat'"'"'s cache directory]'
'(: -)'{-h,--help}'[Print this help message]'
'(: -)'{-V,--version}'[Show version information]'
'*: :_files'
)
_arguments -S -s $args && ret=0
_arguments -S -s $args
case "$state" in
syntax-maps)
if ! compset -P '*:'; then
_message -e patterns 'glob pattern:language'
return
fi
;& # fall-through
languages)
language)
local IFS=$'\n'
local -a languages
languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') )
_describe 'language' languages && ret=0
_describe 'language' languages
;;
themes)
local -a themes expl
themes=( ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} )
theme)
local IFS=$'\n'
local -a themes
themes=( $({{PROJECT_EXECUTABLE}} --list-themes | sort) )
_wanted themes expl 'theme' compadd -a themes && ret=0
_values 'theme' $themes
;;
style)
_values -s , 'style' default auto full plain changes header header-filename header-filesize grid rule numbers snip
;;
esac
return ret
}
case $words[2] in
cache)
## Completion of the 'cache' command itself is removed for better UX
## See https://github.com/sharkdp/bat/issues/2085#issuecomment-1271646802
shift words
(( CURRENT-- ))
curcontext="${curcontext%:*}-${words[1]}:"
_{{PROJECT_EXECUTABLE}}_cache_subcommand
;;

View File

@ -87,10 +87,6 @@ Set the tab width to T spaces. Use a width of 0 to pass tabs through directly
Specify the text\-wrapping mode (*auto*, never, character). The '\-\-terminal\-width' option
can be used in addition to control the output width.
.HP
\fB\-S\fR, \fB\-\-chop\-long\-lines\fR
.IP
Truncate all lines longer than screen width. Alias for '\-\-wrap=never'.
.HP
\fB\-\-terminal\-width\fR <width>
.IP
Explicitly set the width of the terminal instead of determining it automatically. If
@ -145,11 +141,6 @@ use -m '*.build:Python'. To highlight files named '.myignore' with the Git Ignor
syntax, use -m '.myignore:Git Ignore'.
Note that the right-hand side is the *name* of the syntax, not a file extension.
.HP
\fB\-\-ignored\-suffix\fR <ignored-suffix>
.IP
Ignore extension. For example: 'bat \-\-ignored-suffix ".dev" my_file.json.dev'
will use JSON syntax, and ignore '.dev'
.HP
\fB\-\-theme\fR <theme>
.IP
Set the theme for syntax highlighting. Use '\-\-list\-themes' to see all available themes.
@ -160,14 +151,6 @@ export the BAT_THEME environment variable (e.g.: export BAT_THEME="...").
.IP
Display a list of supported themes for syntax highlighting.
.HP
\fB\-s\fR, \fB\-\-squeeze\-blank\fR
.IP
Squeeze consecutive empty lines into a single empty line.
.HP
\fB\-\-squeeze\-limit\fR <squeeze-limit>
.IP
Set the maximum number of consecutive empty lines to be printed.
.HP
\fB\-\-style\fR <style\-components>
.IP
Configure which elements (line numbers, file headers, grid borders, Git modifications,
@ -201,30 +184,6 @@ Display a list of supported languages for syntax highlighting.
This option exists for POSIX\-compliance reasons ('u' is for 'unbuffered'). The output is
always unbuffered \- this option is simply ignored.
.HP
\fB\-\-no\-custom\-assets\fR
.IP
Do not load custom assets.
.HP
\fB\-\-config\-dir\fR
.IP
Show bat's configuration directory.
.HP
\fB\-\-cache\-dir\fR
.IP
Show bat's cache directory.
.HP
\fB\-\-diagnostic\fR
.IP
Show diagnostic information for bug reports.
.HP
\fB\-\-acknowledgements\fR
.IP
Show acknowledgements.
.HP
\fB\-\-set\-terminal\-title\fR
.IP
Sets terminal title to filenames when using a pager.
.HP
\fB\-h\fR, \fB\-\-help\fR
.IP
Print this help message.
@ -253,20 +212,6 @@ location of the configuration file.
To generate a default configuration file, call:
\fB{{PROJECT_EXECUTABLE}} --generate-config-file\fR
These are related options:
.HP
\fB\-\-config\-file\fR
.IP
Show path to the configuration file.
.HP
\fB\-\-generate-config\-file\fR
.IP
Generates a default configuration file.
.HP
\fB\-\-no\-config\fR
.IP
Do not use the configuration file.
.SH "ADDING CUSTOM LANGUAGES"
{{PROJECT_EXECUTABLE}} supports Sublime Text \fB.sublime-syntax\fR language files, and can be
customized to add additional languages to your local installation. To do this, add the \fB.sublime-syntax\fR language
@ -313,16 +258,6 @@ To temporarily disable the preprocessor if it is enabled by default, call:
\fB{{PROJECT_EXECUTABLE}} --no-lessopen\fR
These are related options:
.HP
\fB\-\-lessopen\fR
.IP
Enable the $LESSOPEN preprocessor.
.HP
\fB\-\-no\-lessopen\fR
.IP
Disable the $LESSOPEN preprocessor if enabled (overrides --lessopen)
.PP
For more information, see the "INPUT PREPROCESSOR" section of less(1).
.SH "MORE INFORMATION"

View File

@ -1,14 +0,0 @@
Submodule assets/syntaxes/01_Packages contains modified content
diff --git syntaxes/01_Packages/JavaScript/JavaScript.sublime-syntax syntaxes/01_Packages/JavaScript/JavaScript.sublime-syntax
index 05a4fed6..78a7bf55 100644
--- syntaxes/01_Packages/JavaScript/JavaScript.sublime-syntax
+++ syntaxes/01_Packages/JavaScript/JavaScript.sublime-syntax
@@ -5,7 +5,7 @@ name: JavaScript
file_extensions:
- js
- htc
-first_line_match: ^#!\s*/.*\b(node|js)\b
+first_line_match: ^#!\s*/.*\b(node|bun|js)\b
scope: source.js
variables:
bin_digit: '[01_]'

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
Subproject commit b91c44a32e251c20c6359a8d9232287e1b408e6c

@ -1 +1 @@
Subproject commit 3366b10be91aaab7a61ae0bc0a5af5cc375e58d1
Subproject commit 98233f96d4827a1a576c0b8bf87a68b9c97e4306

@ -1 +1 @@
Subproject commit 1365331580b0e4bb86f74d0c599dccc87e7bdacb
Subproject commit 726e21d74dac23cbb036f2fbbd626decdc954060

@ -1 +1 @@
Subproject commit b7e53e5d86814f04a48d2e441bcf5f9fdf07e9c1
Subproject commit 687058289c1a888e0895378432d66b41609a84d8

@ -1 +1 @@
Subproject commit 209559b72f7e8848c988828088231b3a4d8b6838
Subproject commit f41e5fc8381fe200a4e18c410bb41e911110a6e9

@ -1 +1 @@
Subproject commit 86d4ee7a1f884851a1d21d66249687f527fced32
Subproject commit 43dc527731731666d6d2b1311e86951a8ce07fec

108
build.rs Normal file
View File

@ -0,0 +1,108 @@
// TODO: Re-enable generation of shell completion files (below) when clap 3 is out.
// For more details, see https://github.com/sharkdp/bat/issues/372
// For bat-as-a-library, no build script is required. The build script is for
// the manpage and completions, which are only relevant to the bat application.
#[cfg(not(feature = "application"))]
fn main() {}
#[cfg(feature = "application")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::path::Path;
// Read environment variables.
let project_name = option_env!("PROJECT_NAME").unwrap_or("bat");
let executable_name = option_env!("PROJECT_EXECUTABLE").unwrap_or(project_name);
let executable_name_uppercase = executable_name.to_uppercase();
static PROJECT_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Generates a file from a template.
fn template(
variables: &HashMap<&str, &str>,
in_file: &str,
out_file: impl AsRef<Path>,
) -> Result<(), Box<dyn Error>> {
let mut content = fs::read_to_string(in_file)?;
for (variable_name, value) in variables {
// Replace {{variable_name}} by the value
let pattern = format!("{{{{{variable_name}}}}}", variable_name = variable_name);
content = content.replace(&pattern, value);
}
fs::write(out_file, content)?;
Ok(())
}
let mut variables = HashMap::new();
variables.insert("PROJECT_NAME", project_name);
variables.insert("PROJECT_EXECUTABLE", executable_name);
variables.insert("PROJECT_EXECUTABLE_UPPERCASE", &executable_name_uppercase);
variables.insert("PROJECT_VERSION", PROJECT_VERSION);
let out_dir_env = std::env::var_os("BAT_ASSETS_GEN_DIR")
.or_else(|| std::env::var_os("OUT_DIR"))
.expect("BAT_ASSETS_GEN_DIR or OUT_DIR to be set in build.rs");
let out_dir = Path::new(&out_dir_env);
fs::create_dir_all(out_dir.join("assets/manual")).unwrap();
fs::create_dir_all(out_dir.join("assets/completions")).unwrap();
template(
&variables,
"assets/manual/bat.1.in",
out_dir.join("assets/manual/bat.1"),
)?;
template(
&variables,
"assets/completions/bat.bash.in",
out_dir.join("assets/completions/bat.bash"),
)?;
template(
&variables,
"assets/completions/bat.fish.in",
out_dir.join("assets/completions/bat.fish"),
)?;
template(
&variables,
"assets/completions/_bat.ps1.in",
out_dir.join("assets/completions/_bat.ps1"),
)?;
template(
&variables,
"assets/completions/bat.zsh.in",
out_dir.join("assets/completions/bat.zsh"),
)?;
Ok(())
}
// #[macro_use]
// extern crate clap;
// use clap::Shell;
// use std::fs;
// include!("src/clap_app.rs");
// const BIN_NAME: &str = "bat";
// fn main() {
// let outdir = std::env::var_os("SHELL_COMPLETIONS_DIR").or(std::env::var_os("OUT_DIR"));
// let outdir = match outdir {
// None => return,
// Some(outdir) => outdir,
// };
// fs::create_dir_all(&outdir).unwrap();
// let mut app = build_app(true);
// app.gen_completions(BIN_NAME, Shell::Bash, &outdir);
// app.gen_completions(BIN_NAME, Shell::Fish, &outdir);
// app.gen_completions(BIN_NAME, Shell::Zsh, &outdir);
// app.gen_completions(BIN_NAME, Shell::PowerShell, &outdir);
// }

View File

@ -1,67 +0,0 @@
use std::{env, fs, path::PathBuf};
use crate::util::render_template;
/// Generate manpage and shell completions for the bat application.
pub fn gen_man_and_comp() -> anyhow::Result<()> {
println!("cargo:rerun-if-changed=assets/manual/");
println!("cargo:rerun-if-changed=assets/completions/");
println!("cargo:rerun-if-env-changed=PROJECT_NAME");
println!("cargo:rerun-if-env-changed=PROJECT_EXECUTABLE");
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
println!("cargo:rerun-if-env-changed=BAT_ASSETS_GEN_DIR");
// Read environment variables.
let project_name = env::var("PROJECT_NAME").unwrap_or("bat".into());
let executable_name = env::var("PROJECT_EXECUTABLE").unwrap_or(project_name.clone());
let executable_name_uppercase = executable_name.to_uppercase();
let project_version = env::var("CARGO_PKG_VERSION")?;
let variables = [
("PROJECT_NAME", project_name),
("PROJECT_EXECUTABLE", executable_name),
("PROJECT_EXECUTABLE_UPPERCASE", executable_name_uppercase),
("PROJECT_VERSION", project_version),
]
.into_iter()
.collect();
let Some(out_dir) = env::var_os("BAT_ASSETS_GEN_DIR")
.or_else(|| env::var_os("OUT_DIR"))
.map(PathBuf::from)
else {
anyhow::bail!("BAT_ASSETS_GEN_DIR or OUT_DIR should be set for build.rs");
};
fs::create_dir_all(out_dir.join("assets/manual")).unwrap();
fs::create_dir_all(out_dir.join("assets/completions")).unwrap();
render_template(
&variables,
"assets/manual/bat.1.in",
out_dir.join("assets/manual/bat.1"),
)?;
render_template(
&variables,
"assets/completions/bat.bash.in",
out_dir.join("assets/completions/bat.bash"),
)?;
render_template(
&variables,
"assets/completions/bat.fish.in",
out_dir.join("assets/completions/bat.fish"),
)?;
render_template(
&variables,
"assets/completions/_bat.ps1.in",
out_dir.join("assets/completions/_bat.ps1"),
)?;
render_template(
&variables,
"assets/completions/bat.zsh.in",
out_dir.join("assets/completions/bat.zsh"),
)?;
Ok(())
}

View File

@ -1,17 +0,0 @@
#[cfg(feature = "application")]
mod application;
mod syntax_mapping;
mod util;
fn main() -> anyhow::Result<()> {
// only watch manually-designated files
// see: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed
println!("cargo:rerun-if-changed=build/");
syntax_mapping::build_static_mappings()?;
#[cfg(feature = "application")]
application::gen_man_and_comp()?;
Ok(())
}

View File

@ -1,294 +0,0 @@
use std::{
convert::Infallible,
env, fs,
path::{Path, PathBuf},
str::FromStr,
};
use anyhow::{anyhow, bail};
use indexmap::IndexMap;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
use serde_derive::Deserialize;
use serde_with::DeserializeFromStr;
use walkdir::WalkDir;
/// Known mapping targets.
///
/// Corresponds to `syntax_mapping::MappingTarget`.
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Debug, Eq, PartialEq, Hash, DeserializeFromStr)]
pub enum MappingTarget {
MapTo(String),
MapToUnknown,
MapExtensionToUnknown,
}
impl FromStr for MappingTarget {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"MappingTarget::MapToUnknown" => Ok(Self::MapToUnknown),
"MappingTarget::MapExtensionToUnknown" => Ok(Self::MapExtensionToUnknown),
syntax => Ok(Self::MapTo(syntax.into())),
}
}
}
impl MappingTarget {
fn codegen(&self) -> String {
match self {
Self::MapTo(syntax) => format!(r###"MappingTarget::MapTo(r#"{syntax}"#)"###),
Self::MapToUnknown => "MappingTarget::MapToUnknown".into(),
Self::MapExtensionToUnknown => "MappingTarget::MapExtensionToUnknown".into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)]
/// A single matcher.
///
/// Codegen converts this into a `Lazy<Option<GlobMatcher>>`.
struct Matcher(Vec<MatcherSegment>);
/// Parse a matcher.
///
/// Note that this implementation is rather strict: it will greedily interpret
/// every valid environment variable replacement as such, then immediately
/// hard-error if it finds a '$' anywhere in the remaining text segments.
///
/// The reason for this strictness is I currently cannot think of a valid reason
/// why you would ever need '$' as plaintext in a glob pattern. Therefore any
/// such occurrences are likely human errors.
///
/// If we later discover some edge cases, it's okay to make it more permissive.
///
/// Revision history:
/// - 2024-02-20: allow `{` and `}` (glob brace expansion)
impl FromStr for Matcher {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use MatcherSegment as Seg;
static VAR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap());
let mut segments = vec![];
let mut text_start = 0;
for capture in VAR_REGEX.captures_iter(s) {
let match_0 = capture.get(0).unwrap();
// text before this var
let text_end = match_0.start();
segments.push(Seg::Text(s[text_start..text_end].into()));
text_start = match_0.end();
// this var
segments.push(Seg::Env(capture.get(1).unwrap().as_str().into()));
}
// possible trailing text
segments.push(Seg::Text(s[text_start..].into()));
// cleanup empty text segments
let non_empty_segments = segments
.into_iter()
.filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true))
.collect_vec();
// sanity check
if non_empty_segments
.windows(2)
.any(|segs| segs[0].is_text() && segs[1].is_text())
{
unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}");
}
// guard empty case
if non_empty_segments.is_empty() {
bail!(r#"Parsed an empty matcher: "{s}""#);
}
// guard variable syntax leftover fragments
if non_empty_segments
.iter()
.filter_map(Seg::text)
.any(|t| t.contains('$'))
{
bail!(r#"Invalid matcher: "{s}""#);
}
Ok(Self(non_empty_segments))
}
}
impl Matcher {
fn codegen(&self) -> String {
match self.0.len() {
0 => unreachable!("0-length matcher should never be created"),
// if-let guard would be ideal here
// see: https://github.com/rust-lang/rust/issues/51114
1 if self.0[0].is_text() => {
let s = self.0[0].text().unwrap();
format!(r###"Lazy::new(|| Some(build_matcher_fixed(r#"{s}"#)))"###)
}
// parser logic ensures that this case can only happen when there are dynamic segments
_ => {
let segs = self.0.iter().map(MatcherSegment::codegen).join(", ");
format!(r###"Lazy::new(|| build_matcher_dynamic(&[{segs}]))"###)
}
}
}
}
/// A segment in a matcher.
///
/// Corresponds to `syntax_mapping::MatcherSegment`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum MatcherSegment {
Text(String),
Env(String),
}
#[allow(dead_code)]
impl MatcherSegment {
fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
fn is_env(&self) -> bool {
matches!(self, Self::Env(_))
}
fn text(&self) -> Option<&str> {
match self {
Self::Text(t) => Some(t),
Self::Env(_) => None,
}
}
fn env(&self) -> Option<&str> {
match self {
Self::Text(_) => None,
Self::Env(t) => Some(t),
}
}
fn codegen(&self) -> String {
match self {
Self::Text(s) => format!(r###"MatcherSegment::Text(r#"{s}"#)"###),
Self::Env(s) => format!(r###"MatcherSegment::Env(r#"{s}"#)"###),
}
}
}
/// A struct that models a single .toml file in /src/syntax_mapping/builtins/.
#[derive(Clone, Debug, Deserialize)]
struct MappingDefModel {
mappings: IndexMap<MappingTarget, Vec<Matcher>>,
}
impl MappingDefModel {
fn into_mapping_list(self) -> MappingList {
let list = self
.mappings
.into_iter()
.flat_map(|(target, matchers)| {
matchers
.into_iter()
.map(|matcher| (matcher, target.clone()))
.collect::<Vec<_>>()
})
.collect();
MappingList(list)
}
}
#[derive(Clone, Debug)]
struct MappingList(Vec<(Matcher, MappingTarget)>);
impl MappingList {
fn codegen(&self) -> String {
let array_items: Vec<_> = self
.0
.iter()
.map(|(matcher, target)| {
format!("({m}, {t})", m = matcher.codegen(), t = target.codegen())
})
.collect();
let len = array_items.len();
format!(
"/// Generated by build script from /src/syntax_mapping/builtins/.\n\
pub(crate) static BUILTIN_MAPPINGS: [(Lazy<Option<GlobMatcher>>, MappingTarget); {len}] = [\n{items}\n];",
items = array_items.join(",\n")
)
}
}
/// Get the list of paths to all mapping definition files that should be
/// included for the current target platform.
fn get_def_paths() -> anyhow::Result<Vec<PathBuf>> {
let source_subdirs = [
"common",
#[cfg(target_family = "unix")]
"unix-family",
#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "macos"
))]
"bsd-family",
#[cfg(target_os = "linux")]
"linux",
#[cfg(target_os = "macos")]
"macos",
#[cfg(target_os = "windows")]
"windows",
];
let mut toml_paths = vec![];
for subdir in source_subdirs {
let wd = WalkDir::new(Path::new("src/syntax_mapping/builtins").join(subdir));
let paths = wd
.into_iter()
.filter_map_ok(|entry| {
let path = entry.path();
(path.is_file() && path.extension().map(|ext| ext == "toml").unwrap_or(false))
.then(|| path.to_owned())
})
.collect::<Result<Vec<_>, _>>()?;
toml_paths.extend(paths);
}
toml_paths.sort_by_key(|path| {
path.file_name()
.expect("file name should not terminate in ..")
.to_owned()
});
Ok(toml_paths)
}
fn read_all_mappings() -> anyhow::Result<MappingList> {
let mut all_mappings = vec![];
for path in get_def_paths()? {
let toml_string = fs::read_to_string(path)?;
let mappings = toml::from_str::<MappingDefModel>(&toml_string)?.into_mapping_list();
all_mappings.extend(mappings.0);
}
let duplicates = all_mappings
.iter()
.duplicates_by(|(matcher, _)| matcher)
.collect_vec();
if !duplicates.is_empty() {
bail!("Rules with duplicate matchers found: {duplicates:?}");
}
Ok(MappingList(all_mappings))
}
/// Build the static syntax mappings defined in /src/syntax_mapping/builtins/
/// into a .rs source file, which is to be inserted with `include!`.
pub fn build_static_mappings() -> anyhow::Result<()> {
println!("cargo:rerun-if-changed=src/syntax_mapping/builtins/");
let mappings = read_all_mappings()?;
let codegen_path = Path::new(&env::var_os("OUT_DIR").ok_or(anyhow!("OUT_DIR is unset"))?)
.join("codegen_static_syntax_mappings.rs");
fs::write(codegen_path, mappings.codegen())?;
Ok(())
}

View File

@ -1,21 +0,0 @@
#![allow(dead_code)]
use std::{collections::HashMap, fs, path::Path};
/// Generates a file from a template.
pub fn render_template(
variables: &HashMap<&str, String>,
in_file: &str,
out_file: impl AsRef<Path>,
) -> anyhow::Result<()> {
let mut content = fs::read_to_string(in_file)?;
for (variable_name, value) in variables {
// Replace {{variable_name}} by the value
let pattern = format!("{{{{{variable_name}}}}}");
content = content.replace(&pattern, value);
}
fs::write(out_file, content)?;
Ok(())
}

View File

@ -181,7 +181,7 @@ man 2 select
## インストール
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
### On Ubuntu (`apt` を使用)
*... や他のDebianベースのLinuxディストリビューション*
@ -219,7 +219,7 @@ apk add bat
### On Arch Linux
[Arch Linuxの公式リソース](https://www.archlinux.org/packages/extra/x86_64/bat/)
[Arch Linuxの公式リソース](https://www.archlinux.org/packages/community/x86_64/bat/)
からインストールできます。
```bash

View File

@ -214,7 +214,7 @@ man 2 select
## 설치
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
### Ubuntu에서 (`apt` 사용)
*... 그리고 다른 Debian 기반의 Linux 배포판들에서.*
@ -264,7 +264,7 @@ apk add bat
### Arch Linux에서
공식 소스를 통해
[`bat` 패키지](https://www.archlinux.org/packages/extra/x86_64/bat/)를
[`bat` 패키지](https://www.archlinux.org/packages/community/x86_64/bat/)를
설치할 수 있습니다:
```bash

View File

@ -12,7 +12,7 @@
<a href="#установка">Установка</a>
<a href="#кастомизация">Кастомизация</a>
<a href="#цели-и-альтернативы">Цели и альтернативы </a><br>
[<a href="../README.md">English</a>]
[<a href="../README.md">English]
[<a href="README-zh.md">中文</a>]
[<a href="README-ja.md">日本語</a>]
[<a href="README-ko.md">한국어</a>]
@ -160,7 +160,7 @@ man 2 select
## Установка
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
### Ubuntu (с помощью `apt`)
*... и другие дистрибутивы основанные на Debian.*
@ -201,7 +201,7 @@ apk add bat
### Arch Linux
Вы можете установить [`bat`](https://www.archlinux.org/packages/extra/x86_64/bat/) из официального источника:
Вы можете установить [`bat`](https://www.archlinux.org/packages/community/x86_64/bat/) из официального источника:
```bash
pacman -S bat

View File

@ -191,7 +191,7 @@ man 2 select
## 安装
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg)](https://repology.org/project/bat-cat/versions)
### Ubuntu (使用 `apt`)
@ -232,7 +232,7 @@ apk add bat
### Arch Linux
你可以用下面下列命令从官方源中安装[`bat`包](https://www.archlinux.org/packages/extra/x86_64/bat/)
你可以用下面下列命令从官方源中安装[`bat`包](https://www.archlinux.org/packages/community/x86_64/bat/)
```bash
pacman -S bat
@ -412,7 +412,7 @@ bat --list-themes | fzf --preview="bat --theme={} --color=always /path/to/file"
### 输出样式
你可以用`--style`参数来控制`bat`输出的样式。使用`--style=numbers,changes`可以只开启 Git 修改和行号显示而不添加其他内容。`BAT_STYLE`环境变量具有相同功能。
你可以用`--style`参数来控制`bat`输出的样式。使用`--style=numbers,chanegs`可以只开启 Git 修改和行号显示而不添加其他内容。`BAT_STYLE`环境变量具有相同功能。
### 添加新的语言和语法
@ -616,59 +616,63 @@ iconv -f ISO-8859-1 -t UTF-8 my-file.php | bat
注意: 当`bat`无法识别语言时你可能会需要`-l`/`--language`参数。
## 开发
## Development
```bash
# 递归 clone 以获取所有子模块
# Recursive clone to retrieve all submodules
git clone --recursive https://github.com/sharkdp/bat
# 构建(调试版本)
# Build (debug version)
cd bat
cargo build --bins
# 运行单元测试和集成测试
# Run unit tests and integration tests
cargo test
# 安装(发布版本)
# Install (release version)
cargo install --path . --locked
# 使用修改后的语法和主题构建一个 bat 二进制文件
# Build a bat binary with modified syntaxes and themes
bash assets/create.sh
cargo install --path . --locked --force
```
如果你想构建一个使用 `bat` 美化打印功能的应用程序,请查看 [API 文档](https://docs.rs/bat/)。请注意,当你依赖 `bat` 作为库时,必须使用 `regex-onig``regex-fancy` 作为特性。
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/).
Note that you have to use either `regex-onig` or `regex-fancy` as a feature
when you depend on `bat` as a library.
## 贡献指南
## Contributing
请查看 [`CONTRIBUTING.md`](CONTRIBUTING.md) 指南。
Take a look at the [`CONTRIBUTING.md`](CONTRIBUTING.md) guide.
## 维护者
## Maintainers
- [sharkdp](https://github.com/sharkdp)
- [eth-p](https://github.com/eth-p)
- [keith-hall](https://github.com/keith-hall)
- [Enselic](https://github.com/Enselic)
## 安全漏洞
## Security vulnerabilities
如果你想报告 `bat` 中的漏洞,请通过邮件联系 [David Peter](https://david-peter.de/)。
Please contact [David Peter](https://david-peter.de/) via email if you want to report a vulnerability in `bat`.
## 项目目标和替代方案
## Project goals and alternatives
`bat` 试图实现以下目标:
`bat` tries to achieve the following goals:
- 提供美观的高级语法高亮
- 与 Git 集成以显示文件修改
- 成为 (POSIX) `cat` 的替代品
- 提供用户友好的命令行界面
- Provide beautiful, advanced syntax highlighting
- Integrate with Git to show file modifications
- Be a drop-in replacement for (POSIX) `cat`
- Offer a user-friendly command-line interface
如果你在寻找类似的程序,有很多替代方案。请参阅[本文档](doc/alternatives.md)进行比较。
There are a lot of alternatives, if you are looking for similar programs. See
[this document](doc/alternatives.md) for a comparison.
## 许可证
## License
版权所有 (c) 2018-2021 [bat-developers](https://github.com/sharkdp/bat)。
Copyright (c) 2018-2021 [bat-developers](https://github.com/sharkdp/bat).
`bat` 可根据 MIT 许可证或 Apache 许可证 2.0 的条款使用,任选其一。
`bat` is made available under the terms of either the MIT License or the Apache License 2.0, at your option.
有关许可证的详细信息,请参阅 [LICENSE-APACHE](LICENSE-APACHE) 和 [LICENSE-MIT](LICENSE-MIT) 文件。
See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details.

View File

@ -116,17 +116,6 @@ Options:
--list-themes
Display a list of supported themes for syntax highlighting.
-s, --squeeze-blank
Squeeze consecutive empty lines into a single empty line.
--squeeze-limit <squeeze-limit>
Set the maximum number of consecutive empty lines to be printed.
--strip-ansi <when>
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. 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
@ -134,15 +123,6 @@ Options:
set a default style, add the '--style=".."' option to the configuration file or export the
BAT_STYLE environment variable (e.g.: export BAT_STYLE="..").
When styles are specified in multiple places, the "nearest" set of styles take precedence.
The command-line arguments are the highest priority, followed by the BAT_STYLE environment
variable, and then the configuration file. If any set of styles consists entirely of
components prefixed with "+" or "-", it will modify the previous set of styles instead of
replacing them.
By default, the following components are enabled:
changes, grid, header-filename, numbers, snip
Possible values:
* default: enables recommended style components (default).
@ -180,9 +160,6 @@ Options:
--acknowledgements
Show acknowledgements.
--set-terminal-title
Sets terminal title to filenames when using a pager.
-h, --help
Print help (see a summary with '-h')

View File

@ -9,16 +9,7 @@
- [ ] Update the version and the min. supported Rust version in `README.md` and
`doc/README-*.md`. Check with
`git grep -i -e 'rust.*1\.' -e '1\..*rust' | grep README | grep -v tests/`.
## CHANGELOG.md updates
- [ ] Go to https://github.com/sharkdp/bat/releases/new, click "Choose a tag",
type the name of the tag that will be created later, click "Generate release
notes". DO NOT ACTUALLY CREATE ANY RELEASE IN THIS STEP.
- [ ] Compare current `CHANGELOG.md` with auto-generated release notes and add
missing entries. Expect in particular dependabot PRs to not be in
`CHANGELOG.md` since they are [auto-merged] if CI passes.
- [ ] Introduce a section for the new release and perform final touch-ups.
- [ ] Update `CHANGELOG.md`. Introduce a section for the new release.
## Update syntaxes and themes (build assets)
@ -80,5 +71,3 @@
```
[auto-merged]: https://github.com/sharkdp/bat/blob/master/.github/workflows/Auto-merge-dependabot-PRs.yml

View File

@ -43,8 +43,6 @@ Options:
Set the color theme for syntax highlighting.
--list-themes
Display all supported highlighting themes.
-s, --squeeze-blank
Squeeze consecutive empty lines.
--style <components>
Comma-separated list of style elements to display (*default*, auto, full, plain, changes,
header, header-filename, header-filesize, grid, rule, numbers, snip).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 817 KiB

View File

@ -13,6 +13,6 @@ fn main() {
println!("Themes:");
for theme in printer.themes() {
println!("- {theme}");
println!("- {}", theme);
}
}

View File

@ -380,7 +380,7 @@ fn asset_from_contents<T: serde::de::DeserializeOwned>(
} else {
bincode::deserialize_from(contents)
}
.map_err(|_| format!("Could not parse {description}").into())
.map_err(|_| format!("Could not parse {}", description).into())
}
fn asset_from_cache<T: serde::de::DeserializeOwned>(
@ -396,7 +396,7 @@ fn asset_from_cache<T: serde::de::DeserializeOwned>(
)
})?;
asset_from_contents(&contents[..], description, compressed)
.map_err(|_| format!("Could not parse cached {description}").into())
.map_err(|_| format!("Could not parse cached {}", description).into())
}
#[cfg(target_os = "macos")]
@ -441,7 +441,7 @@ mod tests {
fn new() -> Self {
SyntaxDetectionTest {
assets: HighlightingAssets::from_binary(),
syntax_mapping: SyntaxMapping::new(),
syntax_mapping: SyntaxMapping::builtin(),
temp_dir: TempDir::new().expect("creation of temporary directory"),
}
}
@ -466,7 +466,7 @@ mod tests {
let file_path = self.temp_dir.path().join(file_name);
{
let mut temp_file = File::create(&file_path).unwrap();
writeln!(temp_file, "{first_line}").unwrap();
writeln!(temp_file, "{}", first_line).unwrap();
}
let input = Input::ordinary_file(&file_path);
@ -514,7 +514,8 @@ mod tests {
if !consistent {
eprintln!(
"Inconsistent syntax detection:\nFor File: {as_file}\nFor Reader: {as_reader}"
"Inconsistent syntax detection:\nFor File: {}\nFor Reader: {}",
as_file, as_reader
)
}

View File

@ -3,7 +3,7 @@ use std::path::Path;
use std::time::SystemTime;
use semver::Version;
use serde_derive::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use crate::error::*;

View File

@ -93,7 +93,7 @@ fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
if !missing_contexts.is_empty() {
println!("Some referenced contexts could not be found!");
for context in missing_contexts {
println!("- {context}");
println!("- {}", context);
}
}
}
@ -152,7 +152,7 @@ pub(crate) fn asset_to_contents<T: serde::Serialize>(
} else {
bincode::serialize_into(&mut contents, asset)
}
.map_err(|_| format!("Could not serialize {description}"))?;
.map_err(|_| format!("Could not serialize {}", description))?;
Ok(contents)
}

View File

@ -80,7 +80,7 @@ fn handle_license(path: &Path) -> Result<Option<String>> {
} else if license_not_needed_in_acknowledgements(&license_text) {
Ok(None)
} else {
Err(format!("ERROR: License is of unknown type: {path:?}").into())
Err(format!("ERROR: License is of unknown type: {:?}", path).into())
}
}
@ -125,7 +125,7 @@ fn append_to_acknowledgements(
relative_path: &str,
license_text: &str,
) {
write!(acknowledgements, "## {relative_path}\n\n{license_text}").ok();
write!(acknowledgements, "## {}\n\n{}", relative_path, license_text).ok();
// Make sure the last char is a newline to not mess up formatting later
if acknowledgements

View File

@ -3,7 +3,8 @@ use super::*;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use serde_derive::{Deserialize, Serialize};
use serde::Deserialize;
use serde::Serialize;
use once_cell::unsync::OnceCell;
@ -88,7 +89,7 @@ impl TryFrom<ThemeSet> for LazyThemeSet {
let lazy_theme = LazyTheme {
serialized: crate::assets::build_assets::asset_to_contents(
&theme,
&format!("theme {name}"),
&format!("theme {}", name),
COMPRESS_LAZY_THEMES,
)?,
deserialized: OnceCell::new(),

View File

@ -2,14 +2,11 @@ use std::collections::HashSet;
use std::env;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::{
clap_app,
config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars},
};
use bat::style::StyleComponentList;
use bat::StripAnsiMode;
use clap::ArgMatches;
use console::Term;
@ -32,10 +29,6 @@ fn is_truecolor_terminal() -> bool {
.unwrap_or(false)
}
pub fn env_no_color() -> bool {
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
}
pub struct App {
pub matches: ArgMatches,
interactive_output: bool,
@ -88,6 +81,7 @@ impl App {
// .. and the rest at the end
cli_args.for_each(|a| args.push(a));
args
};
@ -123,11 +117,7 @@ impl App {
_ => unreachable!("other values for --paging are not allowed"),
};
let mut syntax_mapping = SyntaxMapping::new();
// start building glob matchers for builtin mappings immediately
// this is an appropriate approach because it's statistically likely that
// all the custom mappings need to be checked
syntax_mapping.start_offload_build_all();
let mut syntax_mapping = SyntaxMapping::builtin();
if let Some(values) = self.matches.get_many::<String>("ignored-suffix") {
for suffix in values {
@ -136,9 +126,7 @@ impl App {
}
if let Some(values) = self.matches.get_many::<String>("map-syntax") {
// later args take precedence over earlier ones, hence `.rev()`
// see: https://github.com/sharkdp/bat/pull/2755#discussion_r1456416875
for from_to in values.rev() {
for from_to in values {
let parts: Vec<_> = from_to.split(':').collect();
if parts.len() != 2 {
@ -219,7 +207,7 @@ impl App {
|| match self.matches.get_one::<String>("color").map(|s| s.as_str()) {
Some("always") => true,
Some("never") => false,
Some("auto") => !env_no_color() && self.interactive_output,
Some("auto") => env::var_os("NO_COLOR").is_none() && self.interactive_output,
_ => unreachable!("other values for --color are not allowed"),
},
paging_mode,
@ -244,16 +232,6 @@ 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"),
},
theme: self
.matches
.get_one::<String>("theme")
@ -305,17 +283,6 @@ impl App {
use_custom_assets: !self.matches.get_flag("no-custom-assets"),
#[cfg(feature = "lessopen")]
use_lessopen: self.matches.get_flag("lessopen"),
set_terminal_title: self.matches.get_flag("set-terminal-title"),
squeeze_lines: if self.matches.get_flag("squeeze-blank") {
Some(
self.matches
.get_one::<usize>("squeeze-limit")
.map(|limit| limit.to_owned())
.unwrap_or(1),
)
} else {
None
},
})
}
@ -365,57 +332,34 @@ impl App {
Ok(file_input)
}
fn forced_style_components(&self) -> Option<StyleComponents> {
// No components if `--decorations=never``.
if self
.matches
.get_one::<String>("decorations")
.map(|s| s.as_str())
== Some("never")
{
return Some(StyleComponents(HashSet::new()));
}
// Only line numbers if `--number`.
if self.matches.get_flag("number") {
return Some(StyleComponents(HashSet::from([
StyleComponent::LineNumbers,
])));
}
// Plain if `--plain` is specified at least once.
if self.matches.get_count("plain") > 0 {
return Some(StyleComponents(HashSet::from([StyleComponent::Plain])));
}
// Default behavior.
None
}
fn style_components(&self) -> Result<StyleComponents> {
let matches = &self.matches;
let mut styled_components = match self.forced_style_components() {
Some(forced_components) => forced_components,
// Parse the `--style` arguments and merge them.
None if matches.contains_id("style") => {
let lists = matches
.get_many::<String>("style")
.expect("styles present")
.map(|v| StyleComponentList::from_str(v))
.collect::<Result<Vec<StyleComponentList>>>()?;
StyleComponentList::to_components(lists, self.interactive_output, true)
}
// Use the default.
None => StyleComponents(HashSet::from_iter(
StyleComponent::Default
.components(self.interactive_output)
let mut styled_components = StyleComponents(
if matches.get_one::<String>("decorations").map(|s| s.as_str()) == Some("never") {
HashSet::new()
} else if matches.get_flag("number") {
[StyleComponent::LineNumbers].iter().cloned().collect()
} else if 0 < matches.get_count("plain") {
[StyleComponent::Plain].iter().cloned().collect()
} else {
matches
.get_one::<String>("style")
.map(|styles| {
styles
.split(',')
.map(|style| style.parse::<StyleComponent>())
.filter_map(|style| style.ok())
.collect::<Vec<_>>()
})
.unwrap_or_else(|| vec![StyleComponent::Default])
.into_iter()
.cloned(),
)),
};
.map(|style| style.components(self.interactive_output))
.fold(HashSet::new(), |mut acc, components| {
acc.extend(components.iter().cloned());
acc
})
},
);
// If `grid` is set, remove `rule` as it is a subset of `grid`, and print a warning.
if styled_components.grid() && styled_components.0.remove(&StyleComponent::Rule) {

View File

@ -44,7 +44,7 @@ pub fn assets_from_cache_or_binary(
}
fn clear_asset(path: PathBuf, description: &str) {
print!("Clearing {description} ... ");
print!("Clearing {} ... ", description);
match fs::remove_file(&path) {
Err(err) if err.kind() == io::ErrorKind::NotFound => {
println!("skipped (not present)");

View File

@ -1,11 +1,9 @@
use bat::style::StyleComponentList;
use clap::{
crate_name, crate_version, value_parser, Arg, ArgAction, ArgGroup, ColorChoice, Command,
};
use once_cell::sync::Lazy;
use std::env;
use std::path::{Path, PathBuf};
use std::str::FromStr;
static VERSION: Lazy<String> = Lazy::new(|| {
#[cfg(feature = "bugreport")]
@ -21,7 +19,7 @@ static VERSION: Lazy<String> = Lazy::new(|| {
});
pub fn build_app(interactive_output: bool) -> Command {
let color_when = if interactive_output && !crate::app::env_no_color() {
let color_when = if interactive_output && env::var_os("NO_COLOR").is_none() {
ColorChoice::Auto
} else {
ColorChoice::Never
@ -389,45 +387,37 @@ pub fn build_app(interactive_output: bool) -> Command {
.help("Display all supported highlighting themes.")
.long_help("Display a list of supported themes for syntax highlighting."),
)
.arg(
Arg::new("squeeze-blank")
.long("squeeze-blank")
.short('s')
.action(ArgAction::SetTrue)
.help("Squeeze consecutive empty lines.")
.long_help("Squeeze consecutive empty lines into a single empty line.")
)
.arg(
Arg::new("squeeze-limit")
.long("squeeze-limit")
.value_parser(|s: &str| s.parse::<usize>().map_err(|_| "Requires a non-negative number".to_owned()))
.long_help("Set the maximum number of consecutive empty lines to be printed.")
.hide_short_help(true)
)
.arg(
Arg::new("strip-ansi")
.long("strip-ansi")
.overrides_with("strip-ansi")
.value_name("when")
.value_parser(["auto", "always", "never"])
.default_value("never")
.hide_default_value(true)
.help("Strip colors from the input (auto, always, *never*)")
.long_help("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. Possible values: auto, always, *never*.")
.hide_short_help(true)
)
.arg(
Arg::new("style")
.long("style")
.action(ArgAction::Append)
.value_name("components")
.overrides_with("style")
.overrides_with("plain")
.overrides_with("number")
// Cannot use claps built in validation because we have to turn off clap's delimiters
.value_parser(|val: &str| {
match StyleComponentList::from_str(val) {
Err(err) => Err(err),
Ok(_) => Ok(val.to_owned()),
let mut invalid_vals = val.split(',').filter(|style| {
!&[
"auto",
"full",
"default",
"plain",
"header",
"header-filename",
"header-filesize",
"grid",
"rule",
"numbers",
"snip",
#[cfg(feature = "git")]
"changes",
].contains(style)
});
if let Some(invalid) = invalid_vals.next() {
Err(format!("Unknown style, '{}'", invalid))
} else {
Ok(val.to_owned())
}
})
.help(
@ -442,14 +432,6 @@ pub fn build_app(interactive_output: bool) -> Command {
pre-defined style ('full'). To set a default style, add the \
'--style=\"..\"' option to the configuration file or export the \
BAT_STYLE environment variable (e.g.: export BAT_STYLE=\"..\").\n\n\
When styles are specified in multiple places, the \"nearest\" set \
of styles take precedence. The command-line arguments are the highest \
priority, followed by the BAT_STYLE environment variable, and then \
the configuration file. If any set of styles consists entirely of \
components prefixed with \"+\" or \"-\", it will modify the \
previous set of styles instead of replacing them.\n\n\
By default, the following components are enabled:\n \
changes, grid, header-filename, numbers, snip\n\n\
Possible values:\n\n \
* default: enables recommended style components (default).\n \
* full: enables all available components.\n \
@ -585,13 +567,6 @@ pub fn build_app(interactive_output: bool) -> Command {
.action(ArgAction::SetTrue)
.hide_short_help(true)
.help("Show acknowledgements."),
)
.arg(
Arg::new("set-terminal-title")
.long("set-terminal-title")
.action(ArgAction::SetTrue)
.hide_short_help(true)
.help("Sets terminal title to filenames when using a pager."),
);
// Check if the current directory contains a file name cache. Otherwise,

View File

@ -146,11 +146,8 @@ pub fn get_args_from_env_vars() -> Vec<OsString> {
("--style", "BAT_STYLE"),
]
.iter()
.filter_map(|(flag, key)| {
env::var(key)
.ok()
.map(|var| [flag.to_string(), var].join("="))
})
.filter_map(|(flag, key)| env::var(key).ok().map(|var| [flag.to_string(), var]))
.flatten()
.map(|a| a.into())
.collect()
}

View File

@ -30,7 +30,6 @@ use directories::PROJECT_DIRS;
use globset::GlobMatcher;
use bat::{
assets::HighlightingAssets,
config::Config,
controller::Controller,
error::*,
@ -79,11 +78,9 @@ fn run_cache_subcommand(
Ok(())
}
fn get_syntax_mapping_to_paths<'r, 't, I>(mappings: I) -> HashMap<&'t str, Vec<String>>
where
I: IntoIterator<Item = (&'r GlobMatcher, &'r MappingTarget<'t>)>,
't: 'r, // target text outlives rule
{
fn get_syntax_mapping_to_paths<'a>(
mappings: &[(GlobMatcher, MappingTarget<'a>)],
) -> HashMap<&'a str, Vec<String>> {
let mut map = HashMap::new();
for mapping in mappings {
if let (matcher, MappingTarget::MapTo(s)) = mapping {
@ -126,7 +123,7 @@ pub fn get_languages(config: &Config, cache_dir: &Path) -> Result<String> {
languages.sort_by_key(|lang| lang.name.to_uppercase());
let configured_languages = get_syntax_mapping_to_paths(config.syntax_mapping.all_mappings());
let configured_languages = get_syntax_mapping_to_paths(config.syntax_mapping.mappings());
for lang in &mut languages {
if let Some(additional_paths) = configured_languages.get(lang.name.as_str()) {
@ -200,31 +197,19 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<
let stdout = io::stdout();
let mut stdout = stdout.lock();
let default_theme = HighlightingAssets::default_theme();
for theme in assets.themes() {
let default_theme_info = if default_theme == theme {
" (default)"
} else {
""
};
if config.colored_output {
if config.colored_output {
for theme in assets.themes() {
writeln!(
stdout,
"Theme: {}{}\n",
Style::new().bold().paint(theme.to_string()),
default_theme_info
"Theme: {}\n",
Style::new().bold().paint(theme.to_string())
)?;
config.theme = theme.to_string();
Controller::new(&config, &assets)
.run(vec![theme_preview_file()], None)
.ok();
writeln!(stdout)?;
} else {
writeln!(stdout, "{theme}{default_theme_info}")?;
}
}
if config.colored_output {
writeln!(
stdout,
"Further themes can be installed to '{}', \
@ -233,35 +218,18 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<
https://github.com/sharkdp/bat#adding-new-themes",
config_dir.join("themes").to_string_lossy()
)?;
} else {
for theme in assets.themes() {
writeln!(stdout, "{}", theme)?;
}
}
Ok(())
}
fn set_terminal_title_to(new_terminal_title: String) {
let osc_command_for_setting_terminal_title = "\x1b]0;";
let osc_end_command = "\x07";
print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}");
io::stdout().flush().unwrap();
}
fn get_new_terminal_title(inputs: &Vec<Input>) -> String {
let mut new_terminal_title = "bat: ".to_string();
for (index, input) in inputs.iter().enumerate() {
new_terminal_title += input.description().title();
if index < inputs.len() - 1 {
new_terminal_title += ", ";
}
}
new_terminal_title
}
fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Result<bool> {
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
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));
}
controller.run(inputs, None)
}
@ -281,25 +249,24 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
.info(OperatingSystem::default())
.info(CommandLine::default())
.info(EnvironmentVariables::list(&[
"SHELL",
"PAGER",
"LESS",
"LANG",
"LC_ALL",
"BAT_PAGER",
"BAT_PAGING",
"BAT_CACHE_PATH",
"BAT_CONFIG_PATH",
"BAT_OPTS",
"BAT_PAGER",
"BAT_PAGING",
"BAT_STYLE",
"BAT_TABS",
"BAT_THEME",
"COLORTERM",
"LANG",
"LC_ALL",
"LESS",
"MANPAGER",
"NO_COLOR",
"PAGER",
"SHELL",
"TERM",
"XDG_CACHE_HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"COLORTERM",
"NO_COLOR",
"MANPAGER",
]))
.info(FileContent::new("System Config file", system_config_file()))
.info(FileContent::new("Config file", config_file()))

View File

@ -5,7 +5,6 @@ use crate::paging::PagingMode;
use crate::style::StyleComponents;
use crate::syntax_mapping::SyntaxMapping;
use crate::wrapping::WrappingMode;
use crate::StripAnsiMode;
#[derive(Debug, Clone)]
pub enum VisibleLines {
@ -95,15 +94,6 @@ pub struct Config<'a> {
// Whether or not to use $LESSOPEN if set
#[cfg(feature = "lessopen")]
pub use_lessopen: bool,
// Weather 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>,
// Weather or not to set terminal title when using a pager
pub strip_ansi: StripAnsiMode,
}
#[cfg(all(feature = "minimal-application", feature = "paging"))]

View File

@ -47,7 +47,7 @@ impl<'b> Controller<'b> {
&self,
inputs: Vec<Input>,
output_buffer: Option<&mut dyn std::fmt::Write>,
mut handle_error: impl FnMut(&Error, &mut dyn Write),
handle_error: impl Fn(&Error, &mut dyn Write),
) -> Result<bool> {
let mut output_type;

View File

@ -46,7 +46,7 @@ impl Decoration for LineNumberDecoration {
_printer: &InteractivePrinter,
) -> DecorationText {
if continuation {
if line_number >= self.cached_wrap_invalid_at {
if line_number > self.cached_wrap_invalid_at {
let new_width = self.cached_wrap.width + 1;
return DecorationText {
text: self.color.paint(" ".repeat(new_width)).to_string(),
@ -56,7 +56,7 @@ impl Decoration for LineNumberDecoration {
self.cached_wrap.clone()
} else {
let plain: String = format!("{line_number:4}");
let plain: String = format!("{:4}", line_number);
DecorationText {
width: plain.len(),
text: self.color.paint(plain).to_string(),

View File

@ -197,7 +197,7 @@ impl<'a> Input<'a> {
InputKind::StdIn => {
if let Some(stdout) = stdout_identifier {
let input_identifier = Identifier::try_from(clircle::Stdio::Stdin)
.map_err(|e| format!("Stdin: Error identifying file: {e}"))?;
.map_err(|e| format!("Stdin: Error identifying file: {}", e))?;
if stdout.surely_conflicts_with(&input_identifier) {
return Err("IO circle detected. The input from stdin is also an output. Aborting to avoid infinite loop.".into());
}

View File

@ -53,7 +53,6 @@ mod vscreen;
pub(crate) mod wrapping;
pub use nonprintable_notation::NonprintableNotation;
pub use preprocessor::StripAnsiMode;
pub use pretty_printer::{Input, PrettyPrinter, Syntax};
pub use syntax_mapping::{MappingTarget, SyntaxMapping};
pub use wrapping::WrappingMode;

View File

@ -1,18 +1,17 @@
use std::fmt::Write;
use crate::{
nonprintable_notation::NonprintableNotation,
vscreen::{EscapeSequenceOffsets, EscapeSequenceOffsetsIterator},
};
use console::AnsiCodeIterator;
use crate::nonprintable_notation::NonprintableNotation;
/// Expand tabs like an ANSI-enabled expand(1).
pub fn expand_tabs(line: &str, width: usize, cursor: &mut usize) -> String {
let mut buffer = String::with_capacity(line.len() * 2);
for seq in EscapeSequenceOffsetsIterator::new(line) {
match seq {
EscapeSequenceOffsets::Text { .. } => {
let mut text = &line[seq.index_of_start()..seq.index_past_end()];
for chunk in AnsiCodeIterator::new(line) {
match chunk {
(text, true) => buffer.push_str(text),
(mut text, false) => {
while let Some(index) = text.find('\t') {
// Add previous text.
if index > 0 {
@ -32,10 +31,6 @@ pub fn expand_tabs(line: &str, width: usize, cursor: &mut usize) -> String {
*cursor += text.len();
buffer.push_str(text);
}
_ => {
// Copy the ANSI escape sequence.
buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()])
}
}
}
@ -96,27 +91,31 @@ pub fn replace_nonprintable(
});
line_idx = 0;
}
// ASCII control characters
'\x00'..='\x1F' => {
let c = u32::from(chr);
match nonprintable_notation {
NonprintableNotation::Caret => {
let caret_character = char::from_u32(0x40 + c).unwrap();
write!(output, "^{caret_character}").ok();
}
NonprintableNotation::Unicode => {
let replacement_symbol = char::from_u32(0x2400 + c).unwrap();
output.push(replacement_symbol)
}
}
}
// delete
'\x7F' => match nonprintable_notation {
NonprintableNotation::Caret => output.push_str("^?"),
NonprintableNotation::Unicode => output.push('\u{2421}'),
},
// carriage return
'\x0D' => output.push_str(match nonprintable_notation {
NonprintableNotation::Caret => "^M",
NonprintableNotation::Unicode => "",
}),
// null
'\x00' => output.push_str(match nonprintable_notation {
NonprintableNotation::Caret => "^@",
NonprintableNotation::Unicode => "",
}),
// bell
'\x07' => output.push_str(match nonprintable_notation {
NonprintableNotation::Caret => "^G",
NonprintableNotation::Unicode => "",
}),
// backspace
'\x08' => output.push_str(match nonprintable_notation {
NonprintableNotation::Caret => "^H",
NonprintableNotation::Unicode => "",
}),
// escape
'\x1B' => output.push_str(match nonprintable_notation {
NonprintableNotation::Caret => "^[",
NonprintableNotation::Unicode => "",
}),
// printable ASCII
c if c.is_ascii_alphanumeric()
|| c.is_ascii_punctuation()
@ -136,27 +135,6 @@ pub fn replace_nonprintable(
output
}
/// 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
}
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub enum StripAnsiMode {
#[default]
Never,
Always,
Auto,
}
#[test]
fn test_try_parse_utf8_char() {
assert_eq!(try_parse_utf8_char(&[0x20]), Some((' ', 1)));
@ -200,14 +178,3 @@ fn test_try_parse_utf8_char() {
assert_eq!(try_parse_utf8_char(&[0xef, 0x20]), None);
assert_eq!(try_parse_utf8_char(&[0xf0, 0xf0]), None);
}
#[test]
fn test_strip_ansi() {
// The sequence detection is covered by the tests in the vscreen module.
assert_eq!(strip_ansi("no ansi"), "no ansi");
assert_eq!(strip_ansi("\x1B[33mone"), "one");
assert_eq!(
strip_ansi("\x1B]1\x07multiple\x1B[J sequences"),
"multiple sequences"
);
}

View File

@ -11,7 +11,7 @@ use crate::{
input,
line_range::{HighlightedLineRanges, LineRange, LineRanges},
style::StyleComponent,
StripAnsiMode, SyntaxMapping, WrappingMode,
SyntaxMapping, WrappingMode,
};
#[cfg(feature = "paging")]
@ -182,15 +182,6 @@ impl<'a> PrettyPrinter<'a> {
self
}
/// Whether to remove ANSI escape sequences from the input (default: never)
///
/// If `Auto` is used, escape sequences will only be removed when the input
/// is not plain text.
pub fn strip_ansi(&mut self, mode: StripAnsiMode) -> &mut Self {
self.config.strip_ansi = mode;
self
}
/// Text wrapping mode (default: do not wrap)
pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self {
self.config.wrapping_mode = mode;
@ -239,12 +230,6 @@ impl<'a> PrettyPrinter<'a> {
self
}
/// Specify the maximum number of consecutive empty lines to print.
pub fn squeeze_empty_lines(&mut self, maximum: Option<usize>) -> &mut Self {
self.config.squeeze_lines = maximum;
self
}
/// Specify the highlighting theme
pub fn theme(&mut self, theme: impl AsRef<str>) -> &mut Self {
self.config.theme = theme.as_ref().to_owned();

View File

@ -7,9 +7,10 @@ use nu_ansi_term::Style;
use bytesize::ByteSize;
use console::AnsiCodeIterator;
use syntect::easy::HighlightLines;
use syntect::highlighting::Color;
use syntect::highlighting::FontStyle;
use syntect::highlighting::Theme;
use syntect::parsing::SyntaxSet;
@ -29,43 +30,11 @@ use crate::diff::LineChanges;
use crate::error::*;
use crate::input::OpenedInput;
use crate::line_range::RangeCheckResult;
use crate::preprocessor::strip_ansi;
use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color};
use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
use crate::vscreen::AnsiStyle;
use crate::wrapping::WrappingMode;
use crate::StripAnsiMode;
const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
raw_sequence: "\x1B[4m",
parameters: "4",
intermediates: "",
final_byte: "m",
};
const ANSI_UNDERLINE_DISABLE: EscapeSequence = EscapeSequence::CSI {
raw_sequence: "\x1B[24m",
parameters: "24",
intermediates: "",
final_byte: "m",
};
const EMPTY_SYNTECT_STYLE: syntect::highlighting::Style = syntect::highlighting::Style {
foreground: Color {
r: 127,
g: 127,
b: 127,
a: 255,
},
background: Color {
r: 127,
g: 127,
b: 127,
a: 255,
},
font_style: FontStyle::empty(),
};
pub enum OutputHandle<'a> {
IoWrite(&'a mut dyn io::Write),
@ -103,15 +72,11 @@ pub(crate) trait Printer {
pub struct SimplePrinter<'a> {
config: &'a Config<'a>,
consecutive_empty_lines: usize,
}
impl<'a> SimplePrinter<'a> {
pub fn new(config: &'a Config) -> Self {
SimplePrinter {
config,
consecutive_empty_lines: 0,
}
SimplePrinter { config }
}
}
@ -140,21 +105,6 @@ impl<'a> Printer for SimplePrinter<'a> {
_line_number: usize,
line_buffer: &[u8],
) -> Result<()> {
// Skip squeezed lines.
if let Some(squeeze_limit) = self.config.squeeze_lines {
if String::from_utf8_lossy(line_buffer)
.trim_end_matches(|c| c == '\r' || c == '\n')
.is_empty()
{
self.consecutive_empty_lines += 1;
if self.consecutive_empty_lines > squeeze_limit {
return Ok(());
}
} else {
self.consecutive_empty_lines = 0;
}
}
if !out_of_range {
if self.config.show_nonprintable {
let line = replace_nonprintable(
@ -162,7 +112,7 @@ impl<'a> Printer for SimplePrinter<'a> {
self.config.tab_width,
self.config.nonprintable_notation,
);
write!(handle, "{line}")?;
write!(handle, "{}", line)?;
} else {
match handle {
OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?,
@ -208,8 +158,6 @@ pub(crate) struct InteractivePrinter<'a> {
pub line_changes: &'a Option<LineChanges>,
highlighter_from_set: Option<HighlighterFromSet<'a>>,
background_color_highlight: Option<Color>,
consecutive_empty_lines: usize,
strip_ansi: bool,
}
impl<'a> InteractivePrinter<'a> {
@ -262,47 +210,24 @@ impl<'a> InteractivePrinter<'a> {
panel_width = 0;
}
// Get the highlighter for the output.
let is_printing_binary = input
let highlighter_from_set = if input
.reader
.content_type
.map_or(false, |c| c.is_binary() && !config.show_nonprintable);
let needs_to_match_syntax = !is_printing_binary
&& (config.colored_output || config.strip_ansi == StripAnsiMode::Auto);
let (is_plain_text, highlighter_from_set) = if needs_to_match_syntax {
// Determine the type of syntax for highlighting
const PLAIN_TEXT_SYNTAX: &str = "Plain Text";
match assets.get_syntax(config.language, input, &config.syntax_mapping) {
Ok(syntax_in_set) => (
syntax_in_set.syntax.name == PLAIN_TEXT_SYNTAX,
Some(HighlighterFromSet::new(syntax_in_set, theme)),
),
Err(Error::UndetectedSyntax(_)) => (
true,
Some(
assets
.find_syntax_by_name(PLAIN_TEXT_SYNTAX)?
.map(|s| HighlighterFromSet::new(s, theme))
.expect("A plain text syntax is available"),
),
),
Err(e) => return Err(e),
}
.map_or(false, |c| c.is_binary() && !config.show_nonprintable)
{
None
} else {
(false, None)
};
// Determine the type of syntax for highlighting
let syntax_in_set =
match assets.get_syntax(config.language, input, &config.syntax_mapping) {
Ok(syntax_in_set) => syntax_in_set,
Err(Error::UndetectedSyntax(_)) => assets
.find_syntax_by_name("Plain Text")?
.expect("A plain text syntax is available"),
Err(e) => return Err(e),
};
// Determine when to strip ANSI sequences
let strip_ansi = match config.strip_ansi {
_ if config.show_nonprintable => false,
StripAnsiMode::Always => true,
StripAnsiMode::Auto if is_plain_text => false, // Plain text may already contain escape sequences.
StripAnsiMode::Auto => true,
_ => false,
Some(HighlighterFromSet::new(syntax_in_set, theme))
};
Ok(InteractivePrinter {
@ -316,8 +241,6 @@ impl<'a> InteractivePrinter<'a> {
line_changes,
highlighter_from_set,
background_color_highlight,
consecutive_empty_lines: 0,
strip_ansi,
})
}
@ -358,20 +281,12 @@ impl<'a> InteractivePrinter<'a> {
" ".repeat(self.panel_width - 1 - text_truncated.len())
);
if self.config.style_components.grid() {
format!("{text_filled}")
format!("{}", text_filled)
} else {
text_filled
}
}
fn get_header_component_indent_length(&self) -> usize {
if self.config.style_components.grid() && self.panel_width > 0 {
self.panel_width + 2
} else {
self.panel_width
}
}
fn print_header_component_indent(&mut self, handle: &mut OutputHandle) -> Result<()> {
if self.config.style_components.grid() {
write!(
@ -387,55 +302,6 @@ impl<'a> InteractivePrinter<'a> {
}
}
fn print_header_component_with_indent(
&mut self,
handle: &mut OutputHandle,
content: &str,
) -> Result<()> {
self.print_header_component_indent(handle)?;
writeln!(handle, "{content}")
}
fn print_header_multiline_component(
&mut self,
handle: &mut OutputHandle,
content: &str,
) -> Result<()> {
let mut content = content;
let content_width = self.config.term_width - self.get_header_component_indent_length();
while content.len() > content_width {
let (content_line, remaining) = content.split_at(content_width);
self.print_header_component_with_indent(handle, content_line)?;
content = remaining;
}
self.print_header_component_with_indent(handle, content)
}
fn highlight_regions_for_line<'b>(
&mut self,
line: &'b str,
) -> Result<Vec<(syntect::highlighting::Style, &'b str)>> {
let highlighter_from_set = match self.highlighter_from_set {
Some(ref mut highlighter_from_set) => highlighter_from_set,
_ => return Ok(vec![(EMPTY_SYNTECT_STYLE, line)]),
};
// skip syntax highlighting on long lines
let too_long = line.len() > 1024 * 16;
let for_highlighting: &str = if too_long { "\n" } else { line };
let mut highlighted_line = highlighter_from_set
.highlighter
.highlight_line(for_highlighting, highlighter_from_set.syntax_set)?;
if too_long {
highlighted_line[0].1 = &line;
}
Ok(highlighted_line)
}
fn preprocess(&self, text: &str, cursor: &mut usize) -> String {
if self.config.tab_width > 0 {
return expand_tabs(text, self.config.tab_width, cursor);
@ -511,32 +377,31 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
}
header_components
.iter()
.try_for_each(|component| match component {
StyleComponent::HeaderFilename => {
let header_filename = format!(
"{}{}{}",
description
.kind()
.map(|kind| format!("{kind}: "))
.unwrap_or_else(|| "".into()),
self.colors.header_value.paint(description.title()),
mode
);
self.print_header_multiline_component(handle, &header_filename)
}
header_components.iter().try_for_each(|component| {
self.print_header_component_indent(handle)?;
match component {
StyleComponent::HeaderFilename => writeln!(
handle,
"{}{}{}",
description
.kind()
.map(|kind| format!("{}: ", kind))
.unwrap_or_else(|| "".into()),
self.colors.header_value.paint(description.title()),
mode
),
StyleComponent::HeaderFilesize => {
let bsize = metadata
.size
.map(|s| format!("{}", ByteSize(s)))
.unwrap_or_else(|| "-".into());
let header_filesize =
format!("Size: {}", self.colors.header_value.paint(bsize));
self.print_header_multiline_component(handle, &header_filesize)
writeln!(handle, "Size: {}", self.colors.header_value.paint(bsize))
}
_ => Ok(()),
})?;
}
})?;
if self.config.style_components.grid() {
if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {
@ -577,7 +442,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
"{}",
self.colors
.grid
.paint(format!("{panel}{snip_left}{title}{snip_right}"))
.paint(format!("{}{}{}{}", panel, snip_left, title, snip_right))
)?;
Ok(())
@ -598,7 +463,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
)
.into()
} else {
let mut line = match self.content_type {
match self.content_type {
Some(ContentType::BINARY) | None => {
return Ok(());
}
@ -615,31 +480,35 @@ impl<'a> Printer for InteractivePrinter<'a> {
line
}
}
};
// If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting.
if self.strip_ansi {
line = strip_ansi(&line).into()
}
line
};
let regions = self.highlight_regions_for_line(&line)?;
if out_of_range {
return Ok(());
}
// Skip squeezed lines.
if let Some(squeeze_limit) = self.config.squeeze_lines {
if line.trim_end_matches(|c| c == '\r' || c == '\n').is_empty() {
self.consecutive_empty_lines += 1;
if self.consecutive_empty_lines > squeeze_limit {
let regions = {
let highlighter_from_set = match self.highlighter_from_set {
Some(ref mut highlighter_from_set) => highlighter_from_set,
_ => {
return Ok(());
}
} else {
self.consecutive_empty_lines = 0;
};
// skip syntax highlighting on long lines
let too_long = line.len() > 1024 * 16;
let for_highlighting: &str = if too_long { "\n" } else { &line };
let mut highlighted_line = highlighter_from_set
.highlighter
.highlight_line(for_highlighting, highlighter_from_set.syntax_set)?;
if too_long {
highlighted_line[0].1 = &line;
}
highlighted_line
};
if out_of_range {
return Ok(());
}
let mut cursor: usize = 0;
@ -652,7 +521,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
if highlight_this_line && self.config.theme == "ansi" {
self.ansi_style.update(ANSI_UNDERLINE_ENABLE);
self.ansi_style.update("^[4m");
}
let background_color = self
@ -679,17 +548,23 @@ impl<'a> Printer for InteractivePrinter<'a> {
let italics = self.config.use_italic_text;
for &(style, region) in &regions {
let ansi_iterator = EscapeSequenceIterator::new(region);
let ansi_iterator = AnsiCodeIterator::new(region);
for chunk in ansi_iterator {
match chunk {
// ANSI escape passthrough.
(ansi, true) => {
self.ansi_style.update(ansi);
write!(handle, "{}", ansi)?;
}
// Regular text.
EscapeSequence::Text(text) => {
let text = self.preprocess(text, &mut cursor_total);
(text, false) => {
let text = &*self.preprocess(text, &mut cursor_total);
let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
write!(
handle,
"{}{}",
"{}",
as_terminal_escaped(
style,
&format!("{}{}", self.ansi_style, text_trimmed),
@ -697,11 +572,9 @@ impl<'a> Printer for InteractivePrinter<'a> {
colored_output,
italics,
background_color
),
self.ansi_style.to_reset_sequence(),
)
)?;
// Pad the rest of the line.
if text.len() != text_trimmed.len() {
if let Some(background_color) = background_color {
let ansi_style = Style {
@ -719,12 +592,6 @@ impl<'a> Printer for InteractivePrinter<'a> {
write!(handle, "{}", &text[text_trimmed.len()..])?;
}
}
// ANSI escape passthrough.
_ => {
write!(handle, "{}", chunk.raw())?;
self.ansi_style.update(chunk);
}
}
}
}
@ -734,11 +601,17 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
} else {
for &(style, region) in &regions {
let ansi_iterator = EscapeSequenceIterator::new(region);
let ansi_iterator = AnsiCodeIterator::new(region);
for chunk in ansi_iterator {
match chunk {
// ANSI escape passthrough.
(ansi, true) => {
self.ansi_style.update(ansi);
write!(handle, "{}", ansi)?;
}
// Regular text.
EscapeSequence::Text(text) => {
(text, false) => {
let text = self.preprocess(
text.trim_end_matches(|c| c == '\r' || c == '\n'),
&mut cursor_total,
@ -781,7 +654,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
// It wraps.
write!(
handle,
"{}{}\n{}",
"{}\n{}",
as_terminal_escaped(
style,
&format!("{}{}", self.ansi_style, line_buf),
@ -790,7 +663,6 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.use_italic_text,
background_color
),
self.ansi_style.to_reset_sequence(),
panel_wrap.clone().unwrap()
)?;
@ -819,12 +691,6 @@ impl<'a> Printer for InteractivePrinter<'a> {
)
)?;
}
// ANSI escape passthrough.
_ => {
write!(handle, "{}", chunk.raw())?;
self.ansi_style.update(chunk);
}
}
}
}
@ -845,8 +711,8 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
if highlight_this_line && self.config.theme == "ansi" {
write!(handle, "{}", ANSI_UNDERLINE_DISABLE.raw())?;
self.ansi_style.update(ANSI_UNDERLINE_DISABLE);
self.ansi_style.update("^[24m");
write!(handle, "\x1B[24m")?;
}
Ok(())

View File

@ -80,7 +80,7 @@ impl FromStr for StyleComponent {
"full" => Ok(StyleComponent::Full),
"default" => Ok(StyleComponent::Default),
"plain" => Ok(StyleComponent::Plain),
_ => Err(format!("Unknown style '{s}'").into()),
_ => Err(format!("Unknown style '{}'", s).into()),
}
}
}
@ -138,227 +138,3 @@ impl StyleComponents {
self.0.clear();
}
}
#[derive(Debug, PartialEq)]
enum ComponentAction {
Override,
Add,
Remove,
}
impl ComponentAction {
fn extract_from_str(string: &str) -> (ComponentAction, &str) {
match string.chars().next() {
Some('-') => (ComponentAction::Remove, string.strip_prefix('-').unwrap()),
Some('+') => (ComponentAction::Add, string.strip_prefix('+').unwrap()),
_ => (ComponentAction::Override, string),
}
}
}
/// A list of [StyleComponent] that can be parsed from a string.
pub struct StyleComponentList(Vec<(ComponentAction, StyleComponent)>);
impl StyleComponentList {
fn expand_into(&self, components: &mut HashSet<StyleComponent>, interactive_terminal: bool) {
for (action, component) in self.0.iter() {
let subcomponents = component.components(interactive_terminal);
use ComponentAction::*;
match action {
Override | Add => components.extend(subcomponents),
Remove => components.retain(|c| !subcomponents.contains(c)),
}
}
}
/// Returns `true` if any component in the list was not prefixed with `+` or `-`.
fn contains_override(&self) -> bool {
self.0.iter().any(|(a, _)| *a == ComponentAction::Override)
}
/// Combines multiple [StyleComponentList]s into a single [StyleComponents] set.
///
/// ## Precedence
/// The most recent list will take precedence and override all previous lists
/// unless it only contains components prefixed with `-` or `+`. When this
/// happens, the list's components will be merged into the previous list.
///
/// ## Example
/// ```text
/// [numbers,grid] + [header,changes] -> [header,changes]
/// [numbers,grid] + [+header,-grid] -> [numbers,header]
/// ```
///
/// ## Parameters
/// - `with_default`: If true, the styles lists will build upon the StyleComponent::Auto style.
pub fn to_components(
lists: impl IntoIterator<Item = StyleComponentList>,
interactive_terminal: bool,
with_default: bool,
) -> StyleComponents {
let mut components: HashSet<StyleComponent> = HashSet::new();
if with_default {
components.extend(StyleComponent::Auto.components(interactive_terminal))
}
StyleComponents(lists.into_iter().fold(components, |mut components, list| {
if list.contains_override() {
components.clear();
}
list.expand_into(&mut components, interactive_terminal);
components
}))
}
}
impl Default for StyleComponentList {
fn default() -> Self {
StyleComponentList(vec![(ComponentAction::Override, StyleComponent::Default)])
}
}
impl FromStr for StyleComponentList {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Ok(StyleComponentList(
s.split(",")
.map(|s| ComponentAction::extract_from_str(s)) // If the component starts with "-", it's meant to be removed
.map(|(a, s)| Ok((a, StyleComponent::from_str(s)?)))
.collect::<Result<Vec<(ComponentAction, StyleComponent)>>>()?,
))
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use std::str::FromStr;
use super::ComponentAction::*;
use super::StyleComponent;
use super::StyleComponent::*;
use super::StyleComponentList;
#[test]
pub fn style_component_list_parse() {
assert_eq!(
StyleComponentList::from_str("grid,+numbers,snip,-snip,header")
.expect("no error")
.0,
vec![
(Override, Grid),
(Add, LineNumbers),
(Override, Snip),
(Remove, Snip),
(Override, Header),
]
);
assert!(StyleComponentList::from_str("not-a-component").is_err());
assert!(StyleComponentList::from_str("grid,not-a-component").is_err());
assert!(StyleComponentList::from_str("numbers,-not-a-component").is_err());
}
#[test]
pub fn style_component_list_to_components() {
assert_eq!(
StyleComponentList::to_components(
vec![StyleComponentList::from_str("grid,numbers").expect("no error")],
false,
false
)
.0,
HashSet::from([Grid, LineNumbers])
);
}
#[test]
pub fn style_component_list_to_components_removes_negated() {
assert_eq!(
StyleComponentList::to_components(
vec![StyleComponentList::from_str("grid,numbers,-grid").expect("no error")],
false,
false
)
.0,
HashSet::from([LineNumbers])
);
}
#[test]
pub fn style_component_list_to_components_expands_subcomponents() {
assert_eq!(
StyleComponentList::to_components(
vec![StyleComponentList::from_str("full").expect("no error")],
false,
false
)
.0,
HashSet::from_iter(Full.components(true).to_owned())
);
}
#[test]
pub fn style_component_list_expand_negates_subcomponents() {
assert!(!StyleComponentList::to_components(
vec![StyleComponentList::from_str("full,-numbers").expect("no error")],
true,
false
)
.numbers());
}
#[test]
pub fn style_component_list_to_components_precedence_overrides_previous_lists() {
assert_eq!(
StyleComponentList::to_components(
vec![
StyleComponentList::from_str("grid").expect("no error"),
StyleComponentList::from_str("numbers").expect("no error"),
],
false,
false
)
.0,
HashSet::from([LineNumbers])
);
}
#[test]
pub fn style_component_list_to_components_precedence_merges_previous_lists() {
assert_eq!(
StyleComponentList::to_components(
vec![
StyleComponentList::from_str("grid,header").expect("no error"),
StyleComponentList::from_str("-grid").expect("no error"),
StyleComponentList::from_str("+numbers").expect("no error"),
],
false,
false
)
.0,
HashSet::from([HeaderFilename, LineNumbers])
);
}
#[test]
pub fn style_component_list_default_builds_on_auto() {
assert_eq!(
StyleComponentList::to_components(
vec![StyleComponentList::from_str("-numbers").expect("no error"),],
true,
true
)
.0,
{
let mut expected: HashSet<StyleComponent> = HashSet::new();
expected.extend(Auto.components(true));
expected.remove(&LineNumbers);
expected
}
);
}
}

View File

@ -1,30 +1,11 @@
use std::{
path::Path,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
};
use globset::{Candidate, GlobBuilder, GlobMatcher};
use once_cell::sync::Lazy;
use std::path::Path;
use crate::error::Result;
use builtin::BUILTIN_MAPPINGS;
use ignored_suffixes::IgnoredSuffixes;
mod builtin;
pub mod ignored_suffixes;
use globset::{Candidate, GlobBuilder, GlobMatcher};
fn make_glob_matcher(from: &str) -> Result<GlobMatcher> {
let matcher = GlobBuilder::new(from)
.case_insensitive(true)
.literal_separator(true)
.build()?
.compile_matcher();
Ok(matcher)
}
pub mod ignored_suffixes;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
@ -48,108 +29,204 @@ pub enum MappingTarget<'a> {
#[derive(Debug, Clone, Default)]
pub struct SyntaxMapping<'a> {
/// User-defined mappings at run time.
///
/// Rules in front have precedence.
custom_mappings: Vec<(GlobMatcher, MappingTarget<'a>)>,
mappings: Vec<(GlobMatcher, MappingTarget<'a>)>,
pub(crate) ignored_suffixes: IgnoredSuffixes<'a>,
/// A flag to halt glob matcher building, which is offloaded to another thread.
///
/// We have this so that we can signal the thread to halt early when appropriate.
halt_glob_build: Arc<AtomicBool>,
}
impl<'a> Drop for SyntaxMapping<'a> {
fn drop(&mut self) {
// signal the offload thread to halt early
self.halt_glob_build.store(true, Ordering::Relaxed);
}
}
impl<'a> SyntaxMapping<'a> {
pub fn new() -> SyntaxMapping<'a> {
pub fn empty() -> SyntaxMapping<'a> {
Default::default()
}
/// Start a thread to build the glob matchers for all builtin mappings.
///
/// The use of this function while not necessary, is useful to speed up startup
/// times by starting this work early in parallel.
///
/// The thread halts if/when `halt_glob_build` is set to true.
pub fn start_offload_build_all(&self) {
let halt = Arc::clone(&self.halt_glob_build);
thread::spawn(move || {
for (matcher, _) in BUILTIN_MAPPINGS.iter() {
if halt.load(Ordering::Relaxed) {
break;
}
Lazy::force(matcher);
pub fn builtin() -> SyntaxMapping<'a> {
let mut mapping = Self::empty();
mapping.insert("*.h", MappingTarget::MapTo("C++")).unwrap();
mapping
.insert(".clang-format", MappingTarget::MapTo("YAML"))
.unwrap();
mapping.insert("*.fs", MappingTarget::MapTo("F#")).unwrap();
mapping
.insert("build", MappingTarget::MapToUnknown)
.unwrap();
mapping
.insert("**/.ssh/config", MappingTarget::MapTo("SSH Config"))
.unwrap();
mapping
.insert(
"**/bat/config",
MappingTarget::MapTo("Bourne Again Shell (bash)"),
)
.unwrap();
mapping
.insert(
"/etc/profile",
MappingTarget::MapTo("Bourne Again Shell (bash)"),
)
.unwrap();
mapping
.insert(
"os-release",
MappingTarget::MapTo("Bourne Again Shell (bash)"),
)
.unwrap();
mapping
.insert("*.pac", MappingTarget::MapTo("JavaScript (Babel)"))
.unwrap();
mapping
.insert("fish_history", MappingTarget::MapTo("YAML"))
.unwrap();
for glob in ["*.jsonl", "*.sarif"] {
mapping.insert(glob, MappingTarget::MapTo("JSON")).unwrap();
}
// See #2151, https://nmap.org/book/nse-language.html
mapping
.insert("*.nse", MappingTarget::MapTo("Lua"))
.unwrap();
// See #1008
mapping
.insert("rails", MappingTarget::MapToUnknown)
.unwrap();
mapping
.insert("Containerfile", MappingTarget::MapTo("Dockerfile"))
.unwrap();
mapping
.insert("*.ksh", MappingTarget::MapTo("Bourne Again Shell (bash)"))
.unwrap();
// Nginx and Apache syntax files both want to style all ".conf" files
// see #1131 and #1137
mapping
.insert("*.conf", MappingTarget::MapExtensionToUnknown)
.unwrap();
for glob in &[
"/etc/nginx/**/*.conf",
"/etc/nginx/sites-*/**/*",
"nginx.conf",
"mime.types",
] {
mapping.insert(glob, MappingTarget::MapTo("nginx")).unwrap();
}
for glob in &[
"/etc/apache2/**/*.conf",
"/etc/apache2/sites-*/**/*",
"httpd.conf",
] {
mapping
.insert(glob, MappingTarget::MapTo("Apache Conf"))
.unwrap();
}
for glob in &[
"**/systemd/**/*.conf",
"**/systemd/**/*.example",
"*.automount",
"*.device",
"*.dnssd",
"*.link",
"*.mount",
"*.netdev",
"*.network",
"*.nspawn",
"*.path",
"*.service",
"*.scope",
"*.slice",
"*.socket",
"*.swap",
"*.target",
"*.timer",
] {
mapping.insert(glob, MappingTarget::MapTo("INI")).unwrap();
}
// unix mail spool
for glob in &["/var/spool/mail/*", "/var/mail/*"] {
mapping.insert(glob, MappingTarget::MapTo("Email")).unwrap()
}
// pacman hooks
mapping
.insert("*.hook", MappingTarget::MapTo("INI"))
.unwrap();
mapping
.insert("*.ron", MappingTarget::MapTo("Rust"))
.unwrap();
// Global git config files rooted in `$XDG_CONFIG_HOME/git/` or `$HOME/.config/git/`
// See e.g. https://git-scm.com/docs/git-config#FILES
match (
std::env::var_os("XDG_CONFIG_HOME").filter(|val| !val.is_empty()),
std::env::var_os("HOME")
.filter(|val| !val.is_empty())
.map(|home| Path::new(&home).join(".config")),
) {
(Some(xdg_config_home), Some(default_config_home))
if xdg_config_home == default_config_home => {
insert_git_config_global(&mut mapping, &xdg_config_home)
}
});
// Note that this thread is not joined upon completion because there's
// no shared resources that need synchronization to be safely dropped.
// If we later add code into this thread that requires interesting
// resources (e.g. IO), it would be a good idea to store the handle
// and join it on drop.
(Some(xdg_config_home), Some(default_config_home)) /* else guard */ => {
insert_git_config_global(&mut mapping, &xdg_config_home);
insert_git_config_global(&mut mapping, &default_config_home)
}
(Some(config_home), None) => insert_git_config_global(&mut mapping, &config_home),
(None, Some(config_home)) => insert_git_config_global(&mut mapping, &config_home),
(None, None) => (),
};
fn insert_git_config_global(mapping: &mut SyntaxMapping, config_home: impl AsRef<Path>) {
let git_config_path = config_home.as_ref().join("git");
mapping
.insert(
&git_config_path.join("config").to_string_lossy(),
MappingTarget::MapTo("Git Config"),
)
.ok();
mapping
.insert(
&git_config_path.join("ignore").to_string_lossy(),
MappingTarget::MapTo("Git Ignore"),
)
.ok();
mapping
.insert(
&git_config_path.join("attributes").to_string_lossy(),
MappingTarget::MapTo("Git Attributes"),
)
.ok();
}
mapping
}
pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
let matcher = make_glob_matcher(from)?;
self.custom_mappings.push((matcher, to));
let glob = GlobBuilder::new(from)
.case_insensitive(true)
.literal_separator(true)
.build()?;
self.mappings.push((glob.compile_matcher(), to));
Ok(())
}
/// Returns an iterator over all mappings. User-defined mappings are listed
/// before builtin mappings; mappings in front have higher precedence.
///
/// Builtin mappings' `GlobMatcher`s are lazily compiled.
///
/// Note that this function only returns mappings that are valid under the
/// current environment. For details see [`Self::builtin_mappings`].
pub fn all_mappings(&self) -> impl Iterator<Item = (&GlobMatcher, &MappingTarget<'a>)> {
self.custom_mappings()
.iter()
.map(|(matcher, target)| (matcher, target)) // as_ref
.chain(
// we need a map with a closure to "do" the lifetime variance
// see: https://discord.com/channels/273534239310479360/1120124565591425034/1170543402870382653
// also, clippy false positive:
// see: https://github.com/rust-lang/rust-clippy/issues/9280
#[allow(clippy::map_identity)]
self.builtin_mappings().map(|rule| rule),
)
pub fn mappings(&self) -> &[(GlobMatcher, MappingTarget<'a>)] {
&self.mappings
}
/// Returns an iterator over all valid builtin mappings. Mappings in front
/// have higher precedence.
///
/// The `GlabMatcher`s are lazily compiled.
///
/// Mappings that are invalid under the current environment (i.e. rule
/// requires environment variable(s) that is unset, or the joined string
/// after variable(s) replacement is not a valid glob expression) are
/// ignored.
pub fn builtin_mappings(
&self,
) -> impl Iterator<Item = (&'static GlobMatcher, &'static MappingTarget<'static>)> {
BUILTIN_MAPPINGS
.iter()
.filter_map(|(matcher, target)| matcher.as_ref().map(|glob| (glob, target)))
}
/// Returns all user-defined mappings.
pub fn custom_mappings(&self) -> &[(GlobMatcher, MappingTarget<'a>)] {
&self.custom_mappings
}
pub fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> {
pub(crate) fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> {
// Try matching on the file name as-is.
let candidate = Candidate::new(&path);
let candidate_filename = path.as_ref().file_name().map(Candidate::new);
for (glob, syntax) in self.all_mappings() {
for (ref glob, ref syntax) in self.mappings.iter().rev() {
if glob.is_match_candidate(&candidate)
|| candidate_filename
.as_ref()
@ -175,46 +252,9 @@ impl<'a> SyntaxMapping<'a> {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_mappings_work() {
let map = SyntaxMapping::new();
assert_eq!(
map.get_syntax_for("/path/to/build"),
Some(MappingTarget::MapToUnknown)
);
}
#[test]
fn all_fixed_builtin_mappings_can_compile() {
let map = SyntaxMapping::new();
// collect call evaluates all lazy closures
// fixed builtin mappings will panic if they fail to compile
let _mappings = map.builtin_mappings().collect::<Vec<_>>();
}
#[test]
fn builtin_mappings_matcher_only_compile_once() {
let map = SyntaxMapping::new();
let two_iterations: Vec<_> = (0..2)
.map(|_| {
// addresses of every matcher
map.builtin_mappings()
.map(|(matcher, _)| matcher as *const _ as usize)
.collect::<Vec<_>>()
})
.collect();
// if the matchers are only compiled once, their address should remain the same
assert_eq!(two_iterations[0], two_iterations[1]);
}
#[test]
fn custom_mappings_work() {
let mut map = SyntaxMapping::new();
fn basic() {
let mut map = SyntaxMapping::empty();
map.insert("/path/to/Cargo.lock", MappingTarget::MapTo("TOML"))
.ok();
map.insert("/path/to/.ignore", MappingTarget::MapTo("Git Ignore"))
@ -233,32 +273,52 @@ mod tests {
}
#[test]
fn custom_mappings_override_builtin() {
let mut map = SyntaxMapping::new();
fn user_can_override_builtin_mappings() {
let mut map = SyntaxMapping::builtin();
assert_eq!(
map.get_syntax_for("/path/to/httpd.conf"),
Some(MappingTarget::MapTo("Apache Conf"))
map.get_syntax_for("/etc/profile"),
Some(MappingTarget::MapTo("Bourne Again Shell (bash)"))
);
map.insert("httpd.conf", MappingTarget::MapTo("My Syntax"))
map.insert("/etc/profile", MappingTarget::MapTo("My Syntax"))
.ok();
assert_eq!(
map.get_syntax_for("/path/to/httpd.conf"),
map.get_syntax_for("/etc/profile"),
Some(MappingTarget::MapTo("My Syntax"))
);
}
#[test]
fn custom_mappings_precedence() {
let mut map = SyntaxMapping::new();
fn builtin_mappings() {
let map = SyntaxMapping::builtin();
map.insert("/path/to/foo", MappingTarget::MapTo("alpha"))
.ok();
map.insert("/path/to/foo", MappingTarget::MapTo("bravo"))
.ok();
assert_eq!(
map.get_syntax_for("/path/to/foo"),
Some(MappingTarget::MapTo("alpha"))
map.get_syntax_for("/path/to/build"),
Some(MappingTarget::MapToUnknown)
);
}
#[test]
/// verifies that SyntaxMapping::builtin() doesn't repeat `Glob`-based keys
fn no_duplicate_builtin_keys() {
let mappings = SyntaxMapping::builtin().mappings;
for i in 0..mappings.len() {
let tail = mappings[i + 1..].into_iter();
let (dupl, _): (Vec<_>, Vec<_>) =
tail.partition(|item| item.0.glob() == mappings[i].0.glob());
// emit repeats on failure
assert_eq!(
dupl.len(),
0,
"Glob pattern `{}` mapped to multiple: {:?}",
mappings[i].0.glob().glob(),
{
let (_, mut dupl_targets): (Vec<GlobMatcher>, Vec<MappingTarget>) =
dupl.into_iter().cloned().unzip();
dupl_targets.push(mappings[i].1)
},
)
}
}
}

View File

@ -1,91 +0,0 @@
use std::env;
use globset::GlobMatcher;
use once_cell::sync::Lazy;
use crate::syntax_mapping::{make_glob_matcher, MappingTarget};
// Static syntax mappings generated from /src/syntax_mapping/builtins/ by the
// build script (/build/syntax_mapping.rs).
include!(concat!(
env!("OUT_DIR"),
"/codegen_static_syntax_mappings.rs"
));
// The defined matcher strings are analysed at compile time and converted into
// lazily-compiled `GlobMatcher`s. This is so that the string searches are moved
// from run time to compile time, thus improving startup performance.
//
// To any future maintainer (including possibly myself) wondering why there is
// not a `BuiltinMatcher` enum that looks like this:
//
// ```
// enum BuiltinMatcher {
// Fixed(&'static str),
// Dynamic(Lazy<Option<String>>),
// }
// ```
//
// Because there was. I tried it and threw it out.
//
// Naively looking at the problem from a distance, this may seem like a good
// design (strongly typed etc. etc.). It would also save on compiled size by
// extracting out common behaviour into functions. But while actually
// implementing the lazy matcher compilation logic, I realised that it's most
// convenient for `BUILTIN_MAPPINGS` to have the following type:
//
// `[(Lazy<Option<GlobMatcher>>, MappingTarget); N]`
//
// The benefit for this is that operations like listing all builtin mappings
// would be effectively memoised. The caller would not have to compile another
// `GlobMatcher` for rules that they have previously visited.
//
// Unfortunately, this means we are going to have to store a distinct closure
// for each rule anyway, which makes a `BuiltinMatcher` enum a pointless layer
// of indirection.
//
// In the current implementation, the closure within each generated rule simply
// calls either `build_matcher_fixed` or `build_matcher_dynamic`, depending on
// whether the defined matcher contains dynamic segments or not.
/// Compile a fixed glob string into a glob matcher.
///
/// A failure to compile is a fatal error.
///
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
fn build_matcher_fixed(from: &str) -> GlobMatcher {
make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile")
}
/// Join a list of matcher segments to create a glob string, replacing all
/// environment variables, then compile to a glob matcher.
///
/// Returns `None` if any replacement fails, or if the joined glob string fails
/// to compile.
///
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option<GlobMatcher> {
// join segments
let mut buf = String::new();
for seg in segs {
match seg {
MatcherSegment::Text(s) => buf.push_str(s),
MatcherSegment::Env(var) => {
let replaced = env::var(var).ok()?;
buf.push_str(&replaced);
}
}
}
// compile glob matcher
let matcher = make_glob_matcher(&buf).ok()?;
Some(matcher)
}
/// A segment of a dynamic builtin matcher.
///
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
#[derive(Clone, Debug)]
enum MatcherSegment {
Text(&'static str),
Env(&'static str),
}

View File

@ -1,116 +0,0 @@
# `/src/syntax_mapping/builtins`
The files in this directory define path/name-based syntax mappings, which amend
and take precedence over the extension/content-based syntax mappings provided by
[syntect](https://github.com/trishume/syntect).
## File organisation
Each TOML file should describe the syntax mappings of a single application, or
otherwise a set of logically-related rules.
What defines "a single application" here is deliberately vague, since the
file-splitting is purely for maintainability reasons. (Technically, we could
just as well use a single TOML file.) So just use common sense.
TOML files should reside in the corresponding subdirectory of the platform(s)
that they intend to target. At compile time, the build script will go through
each subdirectory that is applicable to the compilation target, collect the
syntax mappings defined by all TOML files, and embed them into the binary.
## File syntax
Each TOML file should contain a single section named `mappings`, with each of
its keys being a language identifier (first column of `bat -L`; also referred to
as "target").
The value of each key should be an array of strings, with each item being a glob
matcher. We will call each of these items a "rule".
For example, if `foo-application` uses both TOML and YAML configuration files,
we could write something like this:
```toml
# 30-foo-application.toml
[mappings]
"TOML" = [
# rules for TOML syntax go here
"/usr/share/foo-application/toml-config/*.conf",
"/etc/foo-application/toml-config/*.conf",
]
"YAML" = [
# rules for YAML syntax go here
# ...
]
```
### Dynamic environment variable replacement
In additional to the standard glob matcher syntax, rules also support dynamic
replacement of environment variables at runtime. This allows us to concisely
handle things like [XDG](https://specifications.freedesktop.org/basedir-spec/latest/).
All environment variables intended to be replaced at runtime must be enclosed in
`${}`, for example `"/foo/*/${YOUR_ENV}-suffix/*.log"`. Note that this is the
**only** admissible syntax; other variable substitution syntaxes are not
supported and will either cause a compile time error, or be treated as plain
text.
For example, if `foo-application` also supports per-user configuration files, we
could write something like this:
```toml
# 30-foo-application.toml
[mappings]
"TOML" = [
# rules for TOML syntax go here
"/usr/share/foo-application/toml-config/*.conf",
"/etc/foo-application/toml-config/*.conf",
"${XDG_CONFIG_HOME}/foo-application/toml-config/*.conf",
"${HOME}/.config/foo-application/toml-config/*.conf",
]
"YAML" = [
# rules for YAML syntax go here
# ...
]
```
If any environment variable replacement in a rule fails (for example when a
variable is unset), or if the glob string after replacements is invalid, the
entire rule will be ignored.
### Explicitly mapping to unknown
Sometimes it may be necessary to "unset" a particular syntect mapping - perhaps
a syntax's matching rules are "too greedy", and is claiming files that it should
not. In this case, there are two special identifiers:
`MappingTarget::MapToUnknown` and `MappingTarget::MapExtensionToUnknown`
(corresponding to the two variants of the `syntax_mapping::MappingTarget` enum).
An example of this would be `*.conf` files in general. So we may write something
like this:
```toml
# 99-unset-ambiguous-extensions.toml
[mappings]
"MappingTarget::MapExtensionToUnknown" = [
"*.conf",
]
```
## Ordering
At compile time, all TOML files applicable to the target are processed in
lexicographical filename order. So `00-foo.toml` takes precedence over
`10-bar.toml`, which takes precedence over `20-baz.toml`, and so on. Note that
**only** the filenames of the TOML files are taken into account; the
subdirectories they are placed in have no influence on ordering.
This behaviour can be occasionally useful for creating high/low priority rules,
such as in the aforementioned example of explicitly mapping `*.conf` files to
unknown. Generally this should not be much of a concern though, since rules
should be written as specifically as possible for each application.
Rules within each TOML file are processed (and therefore matched) in the order
in which they are defined. At runtime, the syntax selection algorithm will
short-circuit and return the target of the first matching rule.

View File

@ -1,2 +0,0 @@
[mappings]
"Bourne Again Shell (bash)" = ["/etc/os-release", "/var/run/os-release"]

View File

@ -1,2 +0,0 @@
[mappings]
"Apache Conf" = ["httpd.conf"]

View File

@ -1,2 +0,0 @@
[mappings]
"INI" = ["**/.aws/credentials", "**/.aws/config"]

View File

@ -1,2 +0,0 @@
[mappings]
"Bourne Again Shell (bash)" = ["**/bat/config"]

View File

@ -1,2 +0,0 @@
[mappings]
"Dockerfile" = ["Containerfile"]

View File

@ -1,6 +0,0 @@
[mappings]
"C++" = [
# probably better than the default Objective C mapping #877
"*.h",
]
"YAML" = [".clang-format"]

View File

@ -1,2 +0,0 @@
[mappings]
"F#" = ["*.fs"]

View File

@ -1,10 +0,0 @@
# Global git config files rooted in `$XDG_CONFIG_HOME/git/` or `$HOME/.config/git/`
# See e.g. https://git-scm.com/docs/git-config#FILES
[mappings]
"Git Config" = ["${XDG_CONFIG_HOME}/git/config", "${HOME}/.config/git/config"]
"Git Ignore" = ["${XDG_CONFIG_HOME}/git/ignore", "${HOME}/.config/git/ignore"]
"Git Attributes" = [
"${XDG_CONFIG_HOME}/git/attributes",
"${HOME}/.config/git/attributes",
]

View File

@ -1,3 +0,0 @@
# JSON Lines is a simple variation of JSON #2535
[mappings]
"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson"]

View File

@ -1,2 +0,0 @@
[mappings]
"Markdown" = ["*.mkd"]

View File

@ -1,2 +0,0 @@
[mappings]
"nginx" = ["nginx.conf", "mime.types"]

View File

@ -1,3 +0,0 @@
[mappings]
# See #2151, https://nmap.org/book/nse-language.html
"Lua" = ["*.nse"]

View File

@ -1,3 +0,0 @@
# 1515
[mappings]
"JavaScript (Babel)" = ["*.pac"]

View File

@ -1,3 +0,0 @@
# Rusty Object Notation #2427
[mappings]
"Rust" = ["*.ron"]

View File

@ -1,3 +0,0 @@
# SARIF is a format for reporting static analysis results #2695
[mappings]
"JSON" = ["*.sarif"]

View File

@ -1,2 +0,0 @@
[mappings]
"SSH Config" = ["**/.ssh/config"]

View File

@ -1,5 +0,0 @@
[mappings]
"MappingTarget::MapExtensionToUnknown" = [
# common extension used for all kinds of formats
"*.conf",
]

View File

@ -1,7 +0,0 @@
[mappings]
"MappingTarget::MapToUnknown" = [
# "NAnt Build File" should only match *.build files, not files named "build"
"build",
# "bin/rails" scripts in a Ruby project misidentified as HTML (Rails) #1008
"rails",
]

View File

@ -1,3 +0,0 @@
# Xonsh shell (https://xon.sh/)
[mappings]
"Python" = ["*.xsh", "*.xonshrc"]

View File

@ -1,8 +0,0 @@
# see https://github.com/containers/image/tree/main/docs
[mappings]
"TOML" = [
"/usr/share/containers/**/*.conf",
"/etc/containers/**/*.conf",
"${HOME}/.config/containers/**/*.conf",
"${XDG_CONFIG_HOME}/containers/**/*.conf",
]

View File

@ -1,7 +0,0 @@
[mappings]
"Bourne Again Shell (bash)" = [
"/etc/os-release",
"/usr/lib/os-release",
"/etc/initrd-release",
"/usr/lib/extension-release.d/extension-release.*",
]

View File

@ -1,3 +0,0 @@
[mappings]
# pacman hooks
"INI" = ["/usr/share/libalpm/hooks/*.hook", "/etc/pacman.d/hooks/*.hook"]

View File

@ -1,7 +0,0 @@
# see `man quadlet`
[mappings]
"INI" = [
"**/containers/systemd/*.{container,volume,network,kube,image}",
"**/containers/systemd/users/*.{container,volume,network,kube,image}",
"**/containers/systemd/users/*/*.{container,volume,network,kube,image}",
]

View File

@ -1,21 +0,0 @@
[mappings]
"INI" = [
"**/systemd/**/*.conf",
"**/systemd/**/*.example",
"*.automount",
"*.device",
"*.dnssd",
"*.link",
"*.mount",
"*.netdev",
"*.network",
"*.nspawn",
"*.path",
"*.service",
"*.scope",
"*.slice",
"*.socket",
"*.swap",
"*.target",
"*.timer",
]

View File

@ -1,2 +0,0 @@
[mappings]
"Apache Conf" = ["/etc/apache2/**/*.conf", "/etc/apache2/sites-*/**/*"]

View File

@ -1,2 +0,0 @@
[mappings]
"YAML" = ["fish_history"]

View File

@ -1,3 +0,0 @@
# KornShell is backward-compatible with the Bourne shell #2633
[mappings]
"Bourne Again Shell (bash)" = ["*.ksh"]

View File

@ -1,2 +0,0 @@
[mappings]
"Email" = ["/var/spool/mail/*", "/var/mail/*"]

View File

@ -1,2 +0,0 @@
[mappings]
"nginx" = ["/etc/nginx/**/*.conf", "/etc/nginx/sites-*/**/*"]

View File

@ -1,5 +0,0 @@
[mappings]
"Bourne Again Shell (bash)" = [
# used by lots of shells
"/etc/profile",
]

View File

@ -1,3 +0,0 @@
# see `man wg-quick`
[mappings]
"INI" = ["/etc/wireguard/*.conf"]

View File

@ -73,7 +73,7 @@ fn internal_suffixes() {
let file_names = ignored_suffixes
.values
.iter()
.map(|suffix| format!("test.json{suffix}"));
.map(|suffix| format!("test.json{}", suffix));
for file_name_str in file_names {
let file_name = OsStr::new(&file_name_str);
let expected_stripped_file_name = OsStr::new("test.json");
@ -95,7 +95,7 @@ fn external_suffixes() {
let file_names = ignored_suffixes
.values
.iter()
.map(|suffix| format!("test.json{suffix}"));
.map(|suffix| format!("test.json{}", suffix));
for file_name_str in file_names {
let file_name = OsStr::new(&file_name_str);
let expected_stripped_file_name = OsStr::new("test.json");

View File

@ -1,8 +1,4 @@
use std::{
fmt::{Display, Formatter},
iter::Peekable,
str::CharIndices,
};
use std::fmt::{Display, Formatter};
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
pub struct AnsiStyle {
@ -14,7 +10,7 @@ impl AnsiStyle {
AnsiStyle { attributes: None }
}
pub fn update(&mut self, sequence: EscapeSequence) -> bool {
pub fn update(&mut self, sequence: &str) -> bool {
match &mut self.attributes {
Some(a) => a.update(sequence),
None => {
@ -23,13 +19,6 @@ impl AnsiStyle {
}
}
}
pub fn to_reset_sequence(&self) -> String {
match self.attributes {
Some(ref a) => a.to_reset_sequence(),
None => String::new(),
}
}
}
impl Display for AnsiStyle {
@ -42,8 +31,6 @@ impl Display for AnsiStyle {
}
struct Attributes {
has_sgr_sequences: bool,
foreground: String,
background: String,
underlined: String,
@ -74,20 +61,11 @@ struct Attributes {
/// ON: ^[9m
/// OFF: ^[29m
strike: String,
/// The hyperlink sequence.
/// FORMAT: \x1B]8;{ID};{URL}\e\\
///
/// `\e\\` may be replaced with BEL `\x07`.
/// Setting both {ID} and {URL} to an empty string represents no hyperlink.
hyperlink: String,
}
impl Attributes {
pub fn new() -> Self {
Attributes {
has_sgr_sequences: false,
foreground: "".to_owned(),
background: "".to_owned(),
underlined: "".to_owned(),
@ -98,56 +76,34 @@ impl Attributes {
underline: "".to_owned(),
italic: "".to_owned(),
strike: "".to_owned(),
hyperlink: "".to_owned(),
}
}
/// Update the attributes with an escape sequence.
/// Returns `false` if the sequence is unsupported.
pub fn update(&mut self, sequence: EscapeSequence) -> bool {
use EscapeSequence::*;
match sequence {
Text(_) => return false,
Unknown(_) => { /* defer to update_with_unsupported */ }
OSC {
raw_sequence,
command,
..
} => {
if command.starts_with("8;") {
return self.update_with_hyperlink(raw_sequence);
}
/* defer to update_with_unsupported */
}
CSI {
final_byte,
parameters,
..
} => {
match final_byte {
"m" => return self.update_with_sgr(parameters),
_ => {
// NOTE(eth-p): We might want to ignore these, since they involve cursor or buffer manipulation.
/* defer to update_with_unsupported */
pub fn update(&mut self, sequence: &str) -> bool {
let mut chars = sequence.char_indices().skip(1);
if let Some((_, t)) = chars.next() {
match t {
'(' => self.update_with_charset('(', chars.map(|(_, c)| c)),
')' => self.update_with_charset(')', chars.map(|(_, c)| c)),
'[' => {
if let Some((i, last)) = chars.last() {
// SAFETY: Always starts with ^[ and ends with m.
self.update_with_csi(last, &sequence[2..i])
} else {
false
}
}
_ => self.update_with_unsupported(sequence),
}
NF { nf_sequence, .. } => {
let mut iter = nf_sequence.chars();
match iter.next() {
Some('(') => return self.update_with_charset('(', iter),
Some(')') => return self.update_with_charset(')', iter),
_ => { /* defer to update_with_unsupported */ }
}
}
} else {
false
}
self.update_with_unsupported(sequence.raw())
}
fn sgr_reset(&mut self) {
self.has_sgr_sequences = false;
self.foreground.clear();
self.background.clear();
self.underlined.clear();
@ -165,14 +121,13 @@ impl Attributes {
.map(|p| p.parse::<u16>())
.map(|p| p.unwrap_or(0)); // Treat errors as 0.
self.has_sgr_sequences = true;
while let Some(p) = iter.next() {
match p {
0 => self.sgr_reset(),
1 => self.bold = "\x1B[1m".to_owned(),
2 => self.dim = "\x1B[2m".to_owned(),
3 => self.italic = "\x1B[3m".to_owned(),
4 => self.underline = "\x1B[4m".to_owned(),
1 => self.bold = format!("\x1B[{}m", parameters),
2 => self.dim = format!("\x1B[{}m", parameters),
3 => self.italic = format!("\x1B[{}m", parameters),
4 => self.underline = format!("\x1B[{}m", parameters),
23 => self.italic.clear(),
24 => self.underline.clear(),
22 => {
@ -183,7 +138,7 @@ impl Attributes {
40..=49 => self.background = Self::parse_color(p, &mut iter),
58..=59 => self.underlined = Self::parse_color(p, &mut iter),
90..=97 => self.foreground = Self::parse_color(p, &mut iter),
100..=107 => self.background = Self::parse_color(p, &mut iter),
100..=107 => self.foreground = Self::parse_color(p, &mut iter),
_ => {
// Unsupported SGR sequence.
// Be compatible and pretend one just wasn't was provided.
@ -194,23 +149,19 @@ impl Attributes {
true
}
fn update_with_csi(&mut self, finalizer: char, sequence: &str) -> bool {
if finalizer == 'm' {
self.update_with_sgr(sequence)
} else {
false
}
}
fn update_with_unsupported(&mut self, sequence: &str) -> bool {
self.unknown_buffer.push_str(sequence);
false
}
fn update_with_hyperlink(&mut self, sequence: &str) -> bool {
if sequence == "8;;" {
// Empty hyperlink ID and HREF -> end of hyperlink.
self.hyperlink.clear();
} else {
self.hyperlink.clear();
self.hyperlink.push_str(sequence);
}
true
}
fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool {
self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>());
true
@ -221,42 +172,20 @@ impl Attributes {
8 => match parameters.next() {
Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)),
Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)),
Some(c) => format!("\x1B[{color};{c}m"),
Some(c) => format!("\x1B[{};{}m", color, c),
_ => "".to_owned(),
},
9 => "".to_owned(),
_ => format!("\x1B[{color}m"),
_ => format!("\x1B[{}m", color),
}
}
/// Gets an ANSI escape sequence to reset all the known attributes.
pub fn to_reset_sequence(&self) -> String {
let mut buf = String::with_capacity(17);
// TODO: Enable me in a later pull request.
// if self.has_sgr_sequences {
// buf.push_str("\x1B[m");
// }
if !self.hyperlink.is_empty() {
buf.push_str("\x1B]8;;\x1B\\"); // Disable hyperlink.
}
// TODO: Enable me in a later pull request.
// if !self.charset.is_empty() {
// // https://espterm.github.io/docs/VT100%20escape%20codes.html
// buf.push_str("\x1B(B\x1B)B"); // setusg0 and setusg1
// }
buf
}
}
impl Display for Attributes {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}{}{}{}{}{}{}{}{}",
"{}{}{}{}{}{}{}{}{}",
self.foreground,
self.background,
self.underlined,
@ -266,7 +195,6 @@ impl Display for Attributes {
self.underline,
self.italic,
self.strike,
self.hyperlink,
)
}
}
@ -282,672 +210,3 @@ fn join(
.collect::<Vec<String>>()
.join(delimiter)
}
/// A range of indices for a raw ANSI escape sequence.
#[derive(Debug, PartialEq)]
pub enum EscapeSequenceOffsets {
Text {
start: usize,
end: usize,
},
Unknown {
start: usize,
end: usize,
},
#[allow(clippy::upper_case_acronyms)]
NF {
// https://en.wikipedia.org/wiki/ANSI_escape_code#nF_Escape_sequences
start_sequence: usize,
start: usize,
end: usize,
},
#[allow(clippy::upper_case_acronyms)]
OSC {
// https://en.wikipedia.org/wiki/ANSI_escape_code#OSC_(Operating_System_Command)_sequences
start_sequence: usize,
start_command: usize,
start_terminator: usize,
end: usize,
},
#[allow(clippy::upper_case_acronyms)]
CSI {
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
start_sequence: usize,
start_parameters: usize,
start_intermediates: usize,
start_final_byte: usize,
end: usize,
},
}
impl EscapeSequenceOffsets {
/// Returns the byte-index of the first character in the escape sequence.
pub fn index_of_start(&self) -> usize {
use EscapeSequenceOffsets::*;
match self {
Text { start, .. } => *start,
Unknown { start, .. } => *start,
NF { start_sequence, .. } => *start_sequence,
OSC { start_sequence, .. } => *start_sequence,
CSI { start_sequence, .. } => *start_sequence,
}
}
/// Returns the byte-index past the last character in the escape sequence.
pub fn index_past_end(&self) -> usize {
use EscapeSequenceOffsets::*;
match self {
Text { end, .. } => *end,
Unknown { end, .. } => *end,
NF { end, .. } => *end,
OSC { end, .. } => *end,
CSI { end, .. } => *end,
}
}
}
/// An iterator over the offests of ANSI/VT escape sequences within a string.
///
/// ## Example
///
/// ```ignore
/// let iter = EscapeSequenceOffsetsIterator::new("\x1B[33mThis is yellow text.\x1B[m");
/// ```
pub struct EscapeSequenceOffsetsIterator<'a> {
text: &'a str,
chars: Peekable<CharIndices<'a>>,
}
impl<'a> EscapeSequenceOffsetsIterator<'a> {
pub fn new(text: &'a str) -> EscapeSequenceOffsetsIterator<'a> {
return EscapeSequenceOffsetsIterator {
text,
chars: text.char_indices().peekable(),
};
}
/// Takes values from the iterator while the predicate returns true.
/// If the predicate returns false, that value is left.
fn chars_take_while(&mut self, pred: impl Fn(char) -> bool) -> Option<(usize, usize)> {
self.chars.peek()?;
let start = self.chars.peek().unwrap().0;
let mut end: usize = start;
while let Some((i, c)) = self.chars.peek() {
if !pred(*c) {
break;
}
end = *i + c.len_utf8();
self.chars.next();
}
Some((start, end))
}
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
self.chars_take_while(|c| c != '\x1B')
.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");
match self.chars.peek() {
None => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end: start_sequence + c.len_utf8(),
}),
Some((_, ']')) => self.next_osc(start_sequence),
Some((_, '[')) => self.next_csi(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(),
}),
},
}
}
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 mut start_terminator: usize;
let mut end_sequence: usize;
loop {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
None => {
start_terminator = self.text.len();
end_sequence = start_terminator;
break;
}
Some((_, end)) => {
start_terminator = end;
end_sequence = end;
}
}
match self.chars.next() {
Some((ti, '\x07')) => {
end_sequence = ti + '\x07'.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;
}
_ => {
// Repeat, since `\\`(anything) isn't a valid ST.
}
}
}
None => {
// Prematurely ends.
break;
}
Some((_, tc)) => {
panic!("this should not be reached: char {tc:?}")
}
}
}
Some(EscapeSequenceOffsets::OSC {
start_sequence,
start_command: osc_open_index + osc_open_char.len_utf8(),
start_terminator,
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, '[');
let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
// 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')) {
start_intermediates = end;
}
// Keep iterating while within the range of `0x20-0x2F`.
let mut start_final_byte: usize = start_intermediates;
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) {
start_final_byte = end;
}
// Take the last char.
let end_of_sequence = match self.chars.next() {
None => start_final_byte,
Some((i, c)) => i + c.len_utf8(),
};
Some(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> {
let (nf_open_index, nf_open_char) = self.chars.next().expect("to not be finished");
debug_assert!(matches!(nf_open_char, '\x20'..='\x2F'));
let start: usize = nf_open_index;
let mut end: usize = start;
// Keep iterating while within the range of `0x20-0x2F`.
match self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) {
Some((_, i)) => end = i,
None => {
return Some(EscapeSequenceOffsets::NF {
start_sequence,
start,
end,
})
}
}
// Get the final byte.
if let Some((i, c)) = self.chars.next() {
end = i + c.len_utf8()
}
Some(EscapeSequenceOffsets::NF {
start_sequence,
start,
end,
})
}
}
impl<'a> Iterator for EscapeSequenceOffsetsIterator<'a> {
type Item = EscapeSequenceOffsets;
fn next(&mut self) -> Option<Self::Item> {
match self.chars.peek() {
Some((_, '\x1B')) => self.next_sequence(),
Some((_, _)) => self.next_text(),
None => None,
}
}
}
/// An iterator over ANSI/VT escape sequences within a string.
///
/// ## Example
///
/// ```ignore
/// let iter = EscapeSequenceIterator::new("\x1B[33mThis is yellow text.\x1B[m");
/// ```
pub struct EscapeSequenceIterator<'a> {
text: &'a str,
offset_iter: EscapeSequenceOffsetsIterator<'a>,
}
impl<'a> EscapeSequenceIterator<'a> {
pub fn new(text: &'a str) -> EscapeSequenceIterator<'a> {
return EscapeSequenceIterator {
text,
offset_iter: EscapeSequenceOffsetsIterator::new(text),
};
}
}
impl<'a> Iterator for EscapeSequenceIterator<'a> {
type Item = EscapeSequence<'a>;
fn next(&mut self) -> Option<Self::Item> {
use EscapeSequenceOffsets::*;
self.offset_iter.next().map(|offsets| match offsets {
Unknown { start, end } => EscapeSequence::Unknown(&self.text[start..end]),
Text { start, end } => EscapeSequence::Text(&self.text[start..end]),
NF {
start_sequence,
start,
end,
} => EscapeSequence::NF {
raw_sequence: &self.text[start_sequence..end],
nf_sequence: &self.text[start..end],
},
OSC {
start_sequence,
start_command,
start_terminator,
end,
} => EscapeSequence::OSC {
raw_sequence: &self.text[start_sequence..end],
command: &self.text[start_command..start_terminator],
terminator: &self.text[start_terminator..end],
},
CSI {
start_sequence,
start_parameters,
start_intermediates,
start_final_byte,
end,
} => EscapeSequence::CSI {
raw_sequence: &self.text[start_sequence..end],
parameters: &self.text[start_parameters..start_intermediates],
intermediates: &self.text[start_intermediates..start_final_byte],
final_byte: &self.text[start_final_byte..end],
},
})
}
}
/// A parsed ANSI/VT100 escape sequence.
#[derive(Debug, PartialEq)]
pub enum EscapeSequence<'a> {
Text(&'a str),
Unknown(&'a str),
#[allow(clippy::upper_case_acronyms)]
NF {
raw_sequence: &'a str,
nf_sequence: &'a str,
},
#[allow(clippy::upper_case_acronyms)]
OSC {
raw_sequence: &'a str,
command: &'a str,
terminator: &'a str,
},
#[allow(clippy::upper_case_acronyms)]
CSI {
raw_sequence: &'a str,
parameters: &'a str,
intermediates: &'a str,
final_byte: &'a str,
},
}
impl<'a> EscapeSequence<'a> {
pub fn raw(&self) -> &'a str {
use EscapeSequence::*;
match *self {
Text(raw) => raw,
Unknown(raw) => raw,
NF { raw_sequence, .. } => raw_sequence,
OSC { raw_sequence, .. } => raw_sequence,
CSI { raw_sequence, .. } => raw_sequence,
}
}
}
#[cfg(test)]
mod tests {
use crate::vscreen::{
EscapeSequence, EscapeSequenceIterator, EscapeSequenceOffsets,
EscapeSequenceOffsetsIterator,
};
#[test]
fn test_escape_sequence_offsets_iterator_parses_text() {
let mut iter = EscapeSequenceOffsetsIterator::new("text");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_text_stops_at_esc() {
let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[ming");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_osc_with_bel() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]abc\x07");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::OSC {
start_sequence: 0,
start_command: 2,
start_terminator: 5,
end: 6,
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_osc_with_st() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]abc\x1B\\");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::OSC {
start_sequence: 0,
start_command: 2,
start_terminator: 5,
end: 7,
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_osc_thats_broken() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]ab");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::OSC {
start_sequence: 0,
start_command: 2,
start_terminator: 4,
end: 4,
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_csi() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 2,
start_final_byte: 2,
end: 3
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_csi_with_parameters() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1;34m");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 6,
start_final_byte: 6,
end: 7
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_csi_with_intermediates() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[$m");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 2,
start_final_byte: 3,
end: 4
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_csi_with_parameters_and_intermediates() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1$m");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 3,
start_final_byte: 4,
end: 5
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_csi_thats_broken() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 2,
start_final_byte: 2,
end: 2
})
);
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 3,
start_final_byte: 3,
end: 3
})
);
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1$");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 3,
start_final_byte: 4,
end: 4
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_nf() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B($0");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::NF {
start_sequence: 0,
start: 1,
end: 4
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_nf_thats_broken() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B(");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::NF {
start_sequence: 0,
start: 1,
end: 1
})
);
}
#[test]
fn test_escape_sequence_offsets_iterator_iterates() {
let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[33m\x1B]OSC\x07\x1B(0");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 4,
start_parameters: 6,
start_intermediates: 8,
start_final_byte: 8,
end: 9
})
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::OSC {
start_sequence: 9,
start_command: 11,
start_terminator: 14,
end: 15
})
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::NF {
start_sequence: 15,
start: 16,
end: 18
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_iterator_iterates() {
let mut iter = EscapeSequenceIterator::new("text\x1B[33m\x1B]OSC\x07\x1B]OSC\x1B\\\x1B(0");
assert_eq!(iter.next(), Some(EscapeSequence::Text("text")));
assert_eq!(
iter.next(),
Some(EscapeSequence::CSI {
raw_sequence: "\x1B[33m",
parameters: "33",
intermediates: "",
final_byte: "m",
})
);
assert_eq!(
iter.next(),
Some(EscapeSequence::OSC {
raw_sequence: "\x1B]OSC\x07",
command: "OSC",
terminator: "\x07",
})
);
assert_eq!(
iter.next(),
Some(EscapeSequence::OSC {
raw_sequence: "\x1B]OSC\x1B\\",
command: "OSC",
terminator: "\x1B\\",
})
);
assert_eq!(
iter.next(),
Some(EscapeSequence::NF {
raw_sequence: "\x1B(0",
nf_sequence: "(0",
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_sgr_attributes_do_not_leak_into_wrong_field() {
let mut attrs = crate::vscreen::Attributes::new();
// Bold, Dim, Italic, Underline, Foreground, Background
attrs.update(EscapeSequence::CSI {
raw_sequence: "\x1B[1;2;3;4;31;41m",
parameters: "1;2;3;4;31;41",
intermediates: "",
final_byte: "m",
});
assert_eq!(attrs.bold, "\x1B[1m");
assert_eq!(attrs.dim, "\x1B[2m");
assert_eq!(attrs.italic, "\x1B[3m");
assert_eq!(attrs.underline, "\x1B[4m");
assert_eq!(attrs.foreground, "\x1B[31m");
assert_eq!(attrs.background, "\x1B[41m");
// Bold, Bright Foreground, Bright Background
attrs.sgr_reset();
attrs.update(EscapeSequence::CSI {
raw_sequence: "\x1B[1;94;103m",
parameters: "1;94;103",
intermediates: "",
final_byte: "m",
});
assert_eq!(attrs.bold, "\x1B[1m");
assert_eq!(attrs.foreground, "\x1B[94m");
assert_eq!(attrs.background, "\x1B[103m");
}
}

View File

@ -9,13 +9,6 @@ if ! command -v hyperfine > /dev/null 2>&1; then
exit 1
fi
# Check that jq is installed.
if ! command -v jq > /dev/null 2>&1; then
echo "'jq' does not seem to be installed."
echo "You can get it here: https://jqlang.github.io/jq/download/"
exit 1
fi
# Check that python3 is installed.
if ! command -v python3 > /dev/null 2>&1; then
echo "'python3' does not seem to be installed."
@ -56,8 +49,7 @@ REPORT="$RESULT_DIR/report.md"
TARGET_DIR="$(get_cargo_target_dir)"
TARGET_RELEASE="${TARGET_DIR}/release/bat"
: ${WARMUP_COUNT:=3}
: ${RUN_COUNT:=10}
WARMUP_COUNT=3
# Determine which target to benchmark.
BAT=''
@ -96,28 +88,16 @@ hyperfine \
"$(printf "%q" "$BAT") --no-config" \
--command-name "bat" \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/startup-time.md" \
--export-json "$RESULT_DIR/startup-time.json"
cat "$RESULT_DIR/startup-time.md" >> "$REPORT"
heading "Startup time without syntax highlighting"
hyperfine \
"$(printf "%q" "$BAT") --no-config startup-time-src/small-CpuInfo-file.cpuinfo" \
--command-name "bat … small-CpuInfo-file.cpuinfo" \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/startup-time-without-syntax-highlighting.md" \
--export-json "$RESULT_DIR/startup-time-without-syntax-highlighting.json"
cat "$RESULT_DIR/startup-time-without-syntax-highlighting.md" >> "$REPORT"
heading "Startup time with syntax highlighting"
hyperfine \
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/small-CpuInfo-file.cpuinfo" \
--command-name "bat … --color=always small-CpuInfo-file.cpuinfo" \
--command-name "bat … small-CpuInfo-file.cpuinfo" \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/startup-time-with-syntax-highlighting.md" \
--export-json "$RESULT_DIR/startup-time-with-syntax-highlighting.json"
cat "$RESULT_DIR/startup-time-with-syntax-highlighting.md" >> "$REPORT"
@ -128,52 +108,16 @@ hyperfine \
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/small-Markdown-file.md" \
--command-name "bat … small-Markdown-file.md" \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/startup-time-with-syntax-with-dependencies.md" \
--export-json "$RESULT_DIR/startup-time-with-syntax-with-dependencies.json"
cat "$RESULT_DIR/startup-time-with-syntax-with-dependencies.md" >> "$REPORT"
heading "Startup time with indeterminant syntax"
hyperfine \
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/mystery-file" \
--shell none \
--command-name 'bat … mystery-file' \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/startup-time-with-indeterminant-syntax.md" \
--export-json "$RESULT_DIR/startup-time-with-indeterminant-syntax.json"
cat "$RESULT_DIR/startup-time-with-indeterminant-syntax.md" >> "$REPORT"
heading "Startup time with manually set syntax"
hyperfine \
"$(printf "%q" "$BAT") --no-config --color=always --language=Dockerfile startup-time-src/mystery-file" \
--shell none \
--command-name 'bat … --language=Dockerfile mystery-file' \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/startup-time-with-manually-set-syntax.md" \
--export-json "$RESULT_DIR/startup-time-with-manually-set-syntax.json"
cat "$RESULT_DIR/startup-time-with-manually-set-syntax.md" >> "$REPORT"
heading "Startup time with mapped syntax"
hyperfine \
"$(printf "%q" "$BAT") --no-config --color=always startup-time-src/Containerfile" \
--shell none \
--command-name 'bat … Containerfile' \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/startup-time-with-mapped-syntax.md" \
--export-json "$RESULT_DIR/startup-time-with-mapped-syntax.json"
cat "$RESULT_DIR/startup-time-with-mapped-syntax.md" >> "$REPORT"
heading "Plain-text speed"
hyperfine \
"$(printf "%q" "$BAT") --no-config --language=txt --style=plain highlighting-speed-src/numpy_test_multiarray.py" \
--command-name 'bat … --language=txt numpy_test_multiarray.py' \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/plain-text-speed.md" \
--export-json "$RESULT_DIR/plain-text-speed.json"
cat "$RESULT_DIR/plain-text-speed.md" >> "$REPORT"
@ -185,7 +129,6 @@ for wrap in character never; do
heading "Syntax highlighting speed --wrap=${wrap}: \`$filename\`"
hyperfine --warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
"$(printf "%q" "$BAT") --no-config --style=full --color=always --wrap=${wrap} --terminal-width=80 '$SRC'" \
--command-name "bat … ${filename}" \
--export-markdown "$RESULT_DIR/syntax-highlighting-speed-${filename}.md" \
@ -200,7 +143,6 @@ hyperfine \
"$(printf "%q" "$BAT") --no-config --language=txt --style=plain many-small-files/*.txt" \
--command-name 'bat … --language=txt *.txt' \
--warmup "$WARMUP_COUNT" \
--runs "$RUN_COUNT" \
--export-markdown "$RESULT_DIR/many-small-files-speed.md" \
--export-json "$RESULT_DIR/many-small-files-speed.json"
cat "$RESULT_DIR/many-small-files-speed.md" >> "$REPORT"

View File

@ -1,3 +0,0 @@
FROM docker.io/alpine:latest
COPY foo /root/bar
RUN sleep 60

View File

@ -1,3 +0,0 @@
FROM docker.io/alpine:latest
COPY foo /root/bar
RUN sleep 60

Some files were not shown because too many files have changed in this diff Show More