From fcaf1acdc80ca42ff43b9b4614d05a195f9583f3 Mon Sep 17 00:00:00 2001 From: upstream Date: Sat, 17 Oct 2026 03:30:42 +0200 Subject: [PATCH] Upstream sharkdp/bat state of the touched files before db8b7248c6 --- CHANGELOG.md | 1420 +++++++++++ README.md | 941 +++++++ src/output.rs | 268 ++ tests/integration_tests.rs | 4830 ++++++++++++++++++++++++++++++++++++ 4 files changed, 7459 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 src/output.rs create mode 100644 tests/integration_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b7e460a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1420 @@ +# unreleased + + +## Other + +- Update Cargo dependencies to resolve current RustSec advisories, see #3861 (@TyceHerrman) +- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536 +- Add .NET slnx extension, see #3682 (@ltrzesniewski) + +## Features + +- Add `-b` / `--number-nonblank` flag to only number non-blank lines, for `cat -b` compatibility. Closes #3856, see #3857 (@MeGaurav4) +- Add a `--sanitize=` flag for safe display of untrusted input. It implies `--strip-ansi` at the same value and additionally substitutes terminal-active control bytes (cursor moves, charset switches, beep, etc.) and Unicode bidi / zero-width formatting characters with the Unicode replacement character (U+FFFD). Mitigates Trojan-Source-style spoofing (CVE-2021-42574). See #3729 (@curious-rabbit) +- Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta) +- Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) +- Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) +- Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) +- Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar) +- Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula) +- Added an initial `flake.nix` for a ready made development environment; see #3578 (@vorburger) +- Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX) +- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) +- Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) +- Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger) +- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres) + +## Bugfixes +- Exit quietly with status 0 instead of panicking when stdout is closed early (broken pipe) on auxiliary output paths such as `--completion`, `--config-file`, `--diagnostic`, `--generate-config-file`, `--set-terminal-title` and `cache --build`/`--clear`, matching the main output path since #232, closes #4034, see #4035 (@Mathjk) +- Track strikethrough (SGR 9 / 29) in the ANSI style tracker so it is re-emitted like bold, dim, italic, and underline, and is no longer dropped after the first highlighted token when displaying ANSI input with syntax highlighting, see #4015 (@mikehasa) +- Allow boolean flags to be given more than once, so that a flag set in the config file can also be passed on the command line without erroring, see #3912 (@logarithmone1128) +- Use parsed CLI arguments to detect number flags, see #3908 (@cuishuang) +- Detect binary content beyond the first line, preventing encrypted files with early line breaks from being treated as text. Closes #3554, see #3877 (@Matei02355) +- Use the last occurrence of numbering and plain flags in combined short arguments, see #3897 (@cuishuang) +- Avoid a spurious Cargo manifest error during installation from Git, see #3899 (@rootsec1) +- Avoid repeated scans of long lines in Log syntax highlighting, see #3876 (@Matei02355) +- Fix `--list-languages` respecting `--paging=never`, see #3828 (@cyphercodes) +- Fix `--sanitize` passing through the bidi control characters U+200E, U+200F and U+061C, see #3862 (@lenamonj) +- `--strip-ansi`: also strip 8-bit C1 introducers (U+0090, U+0098, U+009B, U+009D, U+009E, U+009F) and DCS/SOS/PM/APC sequence bodies, which previously passed through. See #3729 (@curious-rabbit) +- Fix `--ignored-suffix` not falling back to first-line/shebang detection when the ignored suffix is also a registered extension (e.g. `--ignored-suffix .txt` on a shebang script), see #2745 and #3816 (@adnrivera) +- Fix `capacity overflow` panic when printing a snip separator at `--terminal-width=1` with multiple line ranges. Closes #3803, see #3804 (@leeewee) +- Pass `--no-paging` to `bat` invocations inside the bash / zsh / fish / PowerShell shell completion scripts so that shell-level pager wiring (e.g. `LESSOPEN='|-bat -f -pp %s'`) cannot inject ANSI escape sequences into the completion candidates. Closes #3760 (@mvanhorn) +- Quote filenames before substituting them into `$LESSOPEN` / `$LESSCLOSE` templates, preventing shell injection when a filename contains shell metacharacters, see #3726 (@curious-rabbit) +- Fix `--list-themes` unconditionally probing the terminal via OSC 10/11 even when `--theme` was set to an explicit value, see #3700 (regression introduced in bc42149a). (@optimistiCli) +- Fix inverted `$LESSCLOSE` warning so bat warns on nonzero exit, not on success. See #3654 (@cuiweixie) +- Sanitize control characters in filenames before displaying them in the file header, error messages, and the terminal title, preventing ANSI escape injection via crafted filenames. Closes #3054, see #3691 (@curious-rabbit) +- Report initial input read errors instead of treating them as empty input. Closes #3002, see #3706 (@lawrence3699) +- Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar) +- Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) +- Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) +- Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman) +- Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) +- Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir) +- Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney) +- Fix `--wrap=never` and `-S` flags being ignored when piping to pager, see #3592 (@IMaloney) +- Fix crash with BusyBox `less` on Windows, see #3527 (@Anchal-T) +- Fix `bat cache --help` failing with 'unexpected argument' error, see #3580 and #3560 (@NORMAL-EX) +- `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) +- `--help` now correctly honors custom themes. See #3524 (@keith-hall) +- Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) +- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) +- Fixed syntax tests path, see #3610 (@foxfromworld) +- Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz) +- Fix zsh tab completion offering invalid `-l` arguments (file globs, paths, hidden filenames) sourced from the second column of `--list-languages`. Closes #3735, see #3737 (@truffle-dev) +- Fix `usize` underflow in `--list-languages` when `--terminal-width` is smaller than the longest language name, see #3812 (@greymoth-jp) + +## Other +- Use git version of cross. See #3533 (@OctopusET) +- Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) +- Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall) +- Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon) +- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406) +- Replace `libgit2` with a pure Rust implementation of git called `gitoxide`, see PR #3703 (@blinxen) + +## Syntaxes + +- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn) +- Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake) +- Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall) +- Add syntax highlighting support for COBOL, see #3584 (@adukhan99) +- Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire) +- Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp) +- Map all ignore dotfiles to Git Ignore syntax #3636 (@victor-gp) +- Improved Kotlin syntax, see #3699 (@guille) +- Include subdirectories in SSH Config syntax mapping, see #3758 (@injust) +- Add Ghostty syntax mapping, see #3759 (@injust) +- Add syntax highlighting for `Caddyfile` #3789 (@CosmicHorrorDev) +- Include `.code-workspace` as a JSON extension #3809 (@dhruvkb) +- Add syntax mapping for DNF repo configuration files, see #3814 (@injust) + +## Themes + +- Remove the Visual Studio Dark+ theme, see #3552 (@CosmicHorrorDev) + +## `bat` as a library + +# v0.26.1 + +## Features + +- Add paging to '-h' and '--help' see PR #3478 (@MuntasirSZN) + +## Bugfixes + +- Fix hang when using `--list-themes` with an explicit pager, see #3457 (@abhinavcool42) +- Fix negative values of N not being parsed in line ranges without `=` flag value separator, see #3442 (@lmmx) +- Fix broken Docker syntax preventing use of custom assets, see #3476 (@keith-hall) +- Fix decorations being applied unexpectedly when piping. Now only line numbers explicitly required on the command line should be applied in auto decorations mode for `cat` compatibility. See #3496 (@keith-hall) +- Fix diagnostics attempting to find the version of an executable named builtin when builtin pager is used. See #3498 (@keith-hall) +- `--help` now correctly reads the config file for theme information etc. See #3507 (@keith-hall) + +## Other +- Improve README documentation on pager options passed to less, see #3443 (@injust) +- Make PowerShell completions compatible with PowerShell v5.1, see #3495 (@keith-hall) +- Use more robust approach to escaping in Bash completions, see #3448 (@akinomyoga) + +## Syntaxes + +- Update quadlet syntax mapping to include *.{build,pod} files #3484 (@cyqsimon) +- Fix inconsistencies in Ada syntax, see #3481 (@AldanTanneo) +- Add syntax mapping for podman's `artifact` quadlet files, see #3497 (@xduugu) +- Highlight Korn Shell scripts (i.e. with a shebang of ...`ksh`) using Bash syntax, see #3509 (@keith-hall) + +## Themes + +## `bat` as a library + +# v0.26.0 + +## Features +- Add build for windows/ARM64 platform. #3190 (@alcroito) +- Add paging to `--list-themes`, see PR #3239 (@einfachIrgendwer0815) +- Support negative relative line ranges, e.g. `bat -r :-10` / `bat -r='-10:'`, see #3068 (@ajesipow) +- Support context in line ranges, e.g. `bat -r 30::5` / `bat -r 30:40:5`, see #3345 (@cavanaug) +- Add built-in 'minus' pager, e.g. `bat --pager=builtin` see PR #3402 (@academician) + +## Bugfixes + +- Fix UTF-8 BOM not being stripped for syntax detection, see #3314 (@krikera) +- Fix `BAT_THEME_DARK` and `BAT_THEME_LIGHT` being ignored, see issue #3171 and PR #3168 (@bash) +- Prevent `--list-themes` from outputting default theme info to stdout when it is piped, see #3189 (@einfachIrgendwer0815) +- Rename some submodules to fix Dependabot submodule updates, see issue #3198 and PR #3201 (@victor-gp) +- Make highlight tests fail when new syntaxes don't have fixtures PR #3255 (@dan-hipschman) +- Fix crash for multibyte characters in file path, see issue #3230 and PR #3245 (@HSM95) +- Add missing mappings for various bash/zsh files, see PR #3262 (@AdamGaskins) +- Send all bat errors to stderr by default, see #3336 (@JerryImMouse) +- Make --map-syntax target case insensitive to match --language, see #3206 (@keith-hall) +- Correctly determine the end of the line in UTF16LE/BE input #3369 (@keith-hall) +- `--style=changes` no longer prints a two-space indent when the file is unmodified, see issue #2710 and PR #3406 (@jyn514) +- Add missing shell completions, see #3411 (@keith-hall) +- Execute help/version/diagnostic commands even with invalid config/arguments present, see #3414 (@keith-hall) +- Fixed line numbers (`-n`) and style components not printing when piping output, see issue #2935 and PR #3438 (@lmmx) + +## Other + +- Update base16 README links to community driven base16 work #2871 (@JamyGolden) +- Work around build failures when building `bat` from vendored sources #3179 (@dtolnay) +- CICD: Stop building for x86_64-pc-windows-gnu which fails #3261 (Enselic) +- CICD: replace windows-2019 runners with windows-2025 #3339 (@cyqsimon) +- Build script: replace string-based codegen with quote-based codegen #3340 (@cyqsimon) +- Improve code coverage of `--list-languages` parameter #2942 (@sblondon) +- Only start offload worker thread when there's more than 1 core #2956 (@cyqsimon) +- Update terminal-colorsaurus (the library used for dark/light detection) to 1.0, see #3347 (@bash) +- Update console dependency to 0.16, see #3351 (@musicinmybrain) +- Fixed some typos #3244 (@ssbarnea) +- Update onig_sys dependency to 69.9.1 to fix a gcc build failure #3400 (@CosmicHorrorDev) +- Add a cargo feature (`vendored-libgit2`) to build with vendored libgit2 version without depending on the system's one #3426 (@0x61nas) +- Update syntect dependency to v5.3.0 to fix a few minor bugs, see #3410 (@keith-hall) + +## Syntaxes + +- Add syntax mapping for `paru` configuration files #3182 (@cyqsimon) +- Add support for [Idris 2 programming language](https://www.idris-lang.org/) #3150 (@buzden) +- Add syntax mapping for `nix`'s '`flake.lock` lockfiles #3196 (@odilf) +- Improvements to CSV/TSV highlighting, with autodetection of delimiter and support for TSV files, see #3186 (@keith- +- Improve (Sys)log error highlighting, see #3205 (@keith-hall) +- Map `ndjson` extension to JSON syntax, see #3209 (@keith-hall) +- Map files with `csproj`, `vbproj`, `props` and `targets` extensions to XML syntax, see #3213 (@keith-hall) +- Add debsources syntax to highlight `/etc/apt/sources.list` files, see #3215 (@keith-hall) +- Add syntax definition and test file for GDScript highlighting, see #3236 (@chetanjangir0) +- Add syntax test file for Odin highlighting, see #3241 (@chetanjangir0) +- Update quadlet syntax mapping rules to cover quadlets in subdirectories #3299 (@cyqsimon) +- Add syntax Typst #3300 (@cskeeters) +- Map `.mill` files to Scala syntax for Mill build tool configuration files #3311 (@krikera) +- Add syntax highlighting for VHDL, see #3337 (@JerryImMouse) +- Add syntax mapping for certbot certificate configuration #3338 (@cyqsimon) +- Update Lean syntax from Lean 3 to Lean 4 #3322 (@YDX-2147483647) +- Map `.flatpakref` and `.flatpakrepo` files to INI syntax #3353 (@Ferenc-) +- Update hosts syntax #3368 (@keith-hall) +- Map `.kshrc` files to Bash syntax #3364 (@ritoban23) +- Map `/var/log/dmesg` files to Syslog syntax #3412 (@keith-hall) +- Add syntax definition and test file for Go modules(`go.mod` and `go.sum`) highlighting, see #3424 (@DarkMatter-999) +- Syntax highlighting for typescript code blocks within Markdown files, see #3435 (@MuntasirSZN) + +## Themes + +- Add Catppuccin, see #3317 (@SchweGELBin) +- Updated Catppuccin, see #3333 (@SchweGELBin) +- Updated gruvbox, see #3372 (@Nicholas42) +- Updated GitHub theme, see #3382 (@CosmicHorrorDev) +- Updated ANSI theme to highlight JSON object keys differently from values, see #3413 (@keith-hall) + +# v0.25.0 + +## 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) +- Automatically choose theme based on the terminal's color scheme, see #2896 (@bash) +- Add option `--binary=as-text` for printing binary content, see issue #2974 and PR #2976 (@einfachIrgendwer0815) +- Make shell completions available via `--completion `, see issue #2057 and PR #3126 (@einfachIrgendwer0815) +- Syntax highlighting for puppet code blocks within Markdown files, see #3152 (@liliwilson) + +## 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) +- Fix override behavior of --plain and --paging, see issue #2731 and PR #3108 (@einfachIrgendwer0815) +- Fix bugs in `$LESSOPEN` support, see #2805 (@Anomalocaridid) + +## 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) +- Update nix dev-dependency to v0.29.0, see #3112 (@decathorpe) +- Bump MSRV to [1.74](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html), see #3154 (@keith-hall) +- Update clircle dependency to remove winapi transitive dependency, see #3113 (@niklasmohrin) + +## 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) +- Add syntax mapping for CITATION.cff, see #3103 (@Ugzuzg) +- Add syntax mapping for kubernetes config files #3049 (@cyqsimon) +- Adds support for pipe delimiter for CSV #3115 (@pratik-m) +- Add syntax mapping for `/etc/pacman.conf` #2961 (@cyqsimon) +- Associate `uv.lock` with `TOML` syntax, see #3132 (@fepegar) + +## Themes + +- Patched/improved themes for better Manpage syntax highlighting support, see #2994 (@keith-hall). + +## `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) +- Add `theme::theme` for choosing an appropriate theme based on the + terminal's color scheme, see #2896 (@bash) + - [BREAKING] Remove `HighlightingAssets::default_theme`. Use `theme::default_theme` instead. +- Add `PrettyPrinter::print_with_writer` for custom output destinations, see #3070 (@kojix2) + +# v0.24.0 + +## Features + +- Add environment variable `BAT_PAGING`, see #2629 (@einfachIrgendwer0815) +- Add opt-in (`--features lessopen`) support for `LESSOPEN` and `LESSCLOSE`. See #1597, #1739, #2444, #2602, and #2662 (@Anomalocaridid) + +## Bugfixes + +- Fix `more` not being found on Windows when provided via `BAT_PAGER`, see #2570, #2580, and #2651 (@mataha) +- Switched default behavior of `--map-syntax` to be case insensitive #2520 +- Updated version of `serde_yaml` to `0.9`. See #2627 (@Raghav-Bell) +- Fix arithmetic overflow in `LineRange::from` and `LineRange::parse_range`, see #2674, #2698 (@skoriop) +- Fix paging not happening when stdout is interactive but stdin is not, see #2574 (@Nigecat) +- Make `-pp` override `--paging` and vice versa when passed as a later argument, see #2660 (@J-Kappes) + +## Other + +- Output directory for generated assets (completion, manual) can be customized, see #2515 (@tranzystorek-io) +- Use the `is-terminal` crate instead of `atty`, see #2530 (@nickelc) +- Add Winget Releaser workflow, see #2519 (@sitiom) +- Bump MSRV to 1.70, see #2651 (@mataha) + +## Syntaxes + +- Associate `os-release` with `bash` syntax, see #2587 (@cyqsimon) +- Associate `Containerfile` with `Dockerfile` syntax, see #2606 (@einfachIrgendwer0815) +- Replaced quotes with double quotes so fzf integration example script works on windows and linux. see #2095 (@johnmatthiggins) +- Associate `ksh` files with `bash` syntax, see #2633 (@johnmatthiggins) +- Associate `sarif` files with `JSON` syntax, see #2695 (@rhysd) +- Associate `ron` files with `rust` syntax, see #2427 (@YeungOnion) +- Add support for [WebGPU Shader Language](https://www.w3.org/TR/WGSL/), see #2692 (@rhysd) +- Add `.dpkg-new` and `.dpkg-tmp` to ignored suffixe, see #2595 (@scop) +- fix: Add syntax mapping `*.jsonl` => `json`, see #2539 (@WinterCore) +- Update `Julia` syntax, see #2553 (@dependabot) +- add `NSIS` support, see #2577 (@idleberg) +- Update `ssh-config`, see #2697 (@mrmeszaros) +- Add syntax mapping `*.debdiff` => `diff`, see #2947 (@jacg) + +## `bat` as a library + +- Add optional output_buffer arg to `Controller::run()` and `Controller::run_with_error_handler()`, see #2618 (@Piturnah) + + +# v0.23.0 + +## Features + +- Implemented `-S` and `--chop-long-lines` flags as aliases for `--wrap=never`. See #2309 (@johnmatthiggins) +- Breaking change: Environment variables can now override config file settings (but command-line arguments still have the highest precedence), see #1152, #1281, and #2381 (@aaronkollasch) +- Implemented `--nonprintable-notation=caret` to support showing non-printable characters using caret notation. See #2429 (@einfachIrgendwer0815) + +## Bugfixes + +- Fix `bat cache --clear` not clearing the `--target` dir if specified. See #2393 (@miles170) + +## Other + +- Various bash completion improvements, see #2310 (@scop) +- Disable completion of `cache` subcommand, see #2399 (@cyqsimon) +- Signifigantly improve startup performance on macOS, see #2442 (@BlackHoleFox) +- Bump MSRV to 1.62, see #2496 (@Enselic) + +## Syntaxes + +- Added support for Ada, see #1300 and #2316 (@dkm) +- Added `todo.txt` syntax, see #2375 (@BANOnotIT) +- Improve Manpage.sublime-syntax. See #2364 (@Freed-Wu) and #2461 (@keith-hall) +- Added a new `requirements.txt` syntax, see #2361 (@Freed-Wu) +- Added a new VimHelp syntax, see #2366 (@Freed-Wu) +- Associate `pdm.lock` with `TOML` syntax, see #2410 +- `Todo.txt`: Fix highlighting of contexts and projects at beginning of done.txt, see #2411 +- `cmd-help`: overhaul scope names (colors) to improve theme support; misc syntax improvements. See #2419 (@victor-gp) +- Added support for Crontab, see #2509 (@keith-hall) + +## Themes + +## `bat` as a library + +- `PrettyPrinter::header` correctly displays a header with the filename, see #2378 and #2406 (@cstyles) + + +# v0.22.1 + +## Bugfixes + +- Bring back pre-processing of ANSI escape characters to so that some common `bat` use cases starts working again. See #2308 (@Enselic) + +# v0.22.0 + +## Features + +- Make the default macOS theme depend on Dark Mode. See #2197, #1746 (@Enselic) +- Support for separate system and user config files. See #668 (@patrickpichler) + +## Bugfixes + +- Prevent fork nightmare with `PAGER=batcat`. See #2235 (@johnmatthiggins) +- Make `--no-paging`/`-P` override `--paging=...` if passed as a later arg, see #2201 (@themkat) +- `--map-syntax` and `--ignored-suffix` now works together, see #2093 (@czzrr) +- Strips byte order mark from output when in non-loop-through mode. See #1922 (@dag-h) + +## Other + +- Relaxed glibc requirements on amd64, see #2106 and #2194 (@sharkdp) +- Improved fish completions. See #2275 (@zgracem) +- Stop pre-processing ANSI escape characters. Syntax highlighting on ANSI escaped input is not supported. See #2185 and #2189 (@Enselic) + +## Syntaxes + +- NSE (Nmap Scripting Engine) is mapped to Lua, see #2151 (@Cre3per) +- Correctly color `fstab` dump and pass fields, see #2246 (@yuvalmo) +- Update `Command Help` syntax, see #2255 +- `Julia`: Fix syntax highlighting for function name starting with `struct`, see #2230 +- Minor update to `LiveScript`, see #2291 +- Associate `.mts` and `.cts` files with the `TypeScript` syntax. See #2236 (@kidonng) +- Fish history is mapped to YAML. See #2237 (@kidonng) + +## `bat` as a library + +- Make `bat::PrettyPrinter::syntaxes()` iterate over new `bat::Syntax` struct instead of `&syntect::parsing::SyntaxReference`. See #2222 (@Enselic) +- Clear highlights after printing, see #1919 and #1920 (@rhysd) + + +# v0.21.0 + +## Features + +- Correctly render tab stops in `--show-all`, see #2038 (@Synthetica9) +- Add a `--style=default` option and make it the default. It is less verbose than `full`, see #2061 (@IsaacHorvath) +- Enable BusyBox `less` as pager, see #2162 (@nfisher1226) +- File extensions are now matched case-insensitively. See #1854, #2181 (@Enselic) + +## Bugfixes + +- Bump `regex` dependency from 1.5.4 to 1.5.5 to fix [CVE-2022-24713](https://blog.rust-lang.org/2022/03/08/cve-2022-24713.html), see #2145, #2139 (@Enselic) +- `bat` no longer crashes when encountering files that references missing syntaxes. See #915, #2181 (@Enselic) + +## Performance + +- Skip syntax highlighting on long lines (> 16384 chars) to help improve performance. See #2165 (@keith-hall) +- Vastly improve startup time by lazy-loading syntaxes via syntect 5.0.0. This makes bat display small files ~75% faster than before. See #951, #2181 (@Enselic) + +## Other + +- Include info about custom assets in `--diagnostics` if used. See #2107, #2144 (@Enselic) + +## Syntaxes + +- Mapped clang-format config file (.clang-format) to YAML syntax (@TruncatedDinosour) +- log syntax: improved handling of escape characters in double quoted strings. See #2123 (@keith-hall) +- Associate `/var/spool/mail/*` and `/var/mail/*` with the `Email` syntax. See #2156 (@cyqsimon) +- Added cmd-help syntax to scope --help messages. See #2148 (@victor-gp) +- Slightly adjust Zig syntax. See #2136 (@Enselic) +- Associate `.inf` files with the `INI` syntax. See #2190 (@Enselic) + +## `bat` as a library + +- Allow configuration of `show_nonprintable` with `PrettyPrinter`, see #2142 +- The binary format of syntaxes.bin has been changed due to syntaxes now being lazy-loaded via syntect 5.0.0. See #2181 (@Enselic) +- Mark `bat::error::Error` enum as `#[non_exhaustive]` to allow adding new variants without future semver breakage. See #2181 (@Enselic) +- Change `Error::SyntectError(syntect::LoadingError)` to `Error::SyntectError(syntect::Error)`. See #2181 (@Enselic) +- Add `Error::SyntectLoadingError(syntect::LoadingError)` enum variant. See #2181 (@Enselic) + + +# v0.20.0 + +## Features + +- New style component `header-filesize` to show size of the displayed file in the header. See #1988 (@mdibaiee) +- Use underline for line highlighting on ANSI, see #1730 (@mdibaiee) + +## Bugfixes + +- Fix bash completion on bash 3.x and bash-completion 1.x. See #2066 (@joshpencheon) + +## Syntaxes + +- `GraphQL`: Add support for interfaces implementing interfaces and consider ampersand an operator. See #2000 +- Associate `_vimrc` and `_gvimrc` files with the `VimL` syntax. See #2002 +- Associate `poetry.lock` files with the `TOML` syntax. See #2049 +- Associate `.mesh`, `.task`, `.rgen`, `.rint`, `.rahit`, `.rchit`, `.rmiss`, and `.rcall` with the `GLSL` syntax. See #2050 +- Added support for `JQ` syntax, see #2072 +- Properly associate global git config files rooted in `$XDG_CONFIG_HOME/git/` or `$HOME/.config/git/`. See #2067 (@cyqsimon) + +## `bat` as a library + +- Exposed `get_syntax_set` and `get_theme` methods on `HighlightingAssets`. See #2030 (@dandavison) +- Added `HeaderFilename` and `HeaderFilesize` to `StyleComponent` enum, and mark it `#[non_exhaustive]`. See #1988 (@mdibaiee) + + +# v0.19.0 + +## Performance + +- Reduce startup time in loop-through mode (e.g. when redirecting output) by 90%. See #1747 (@Enselic) +- Load themes lazily to make bat start 25% faster when disregarding syntax load time. See #1969 (@Enselic) +- Python syntax highlighting no longer suffers from abysmal performance in specific scenarios. See #1688 (@keith-hall) +- Fix for poor performance when ANSI escape sequences are piped to `bat`, see #1596 (@eth-p) +- Fix for incorrect handling of ANSI escape sequences when using `--wrap=never`, see #1596 (@eth-p) +- Load custom assets as fast as integrated assets, see #1753 (@Enselic) + +## Features + +- Support for `x:-delta` (minus) syntax in line ranges (e.g. `20:-10`). See #1901 (@bojan88) +- Support for `--ignored-suffix` argument. See #1892 (@bojan88) +- `$BAT_CONFIG_DIR` is now a recognized environment variable. It has precedence over `$XDG_CONFIG_HOME`, see #1727 (@billrisher) +- Support for `x:+delta` syntax in line ranges (e.g. `20:+10`). See #1810 (@bojan88) +- Add new `--acknowledgements` option that gives credit to theme and syntax definition authors. See #1971 (@Enselic) +- Include git hash in `bat -V` and `bat --version` output if present. See #1921 (@Enselic) + +## Bugfixes + +- First line not shown in diff context. See #1891 (@divagant-martian) +- Do not ignore syntaxes that handle file names with a `*.conf` extension. See #1703 (@cbolgiano) + +## Other + +- Add PowerShell completion, see #1826 (@rashil2000) +- Minimum supported Rust version (MSRV) bumped to 1.51, see #1994 (@mdibaiee) + +## Syntaxes + +- Groff, see #1685 (@scop) +- HTTP Requests and Responses, see #1748 (@keith-hall) +- LLVM, see #1777 (@ioncodes) +- Highlight for `vimrc` and `gvimrc` files, see #1763 (@SuperSandro2000) +- Syslog highlighting improvements, see #1793 (@scop) +- Added support for `slim` syntax, see #1693 (@mfinelli) +- Racket, see #1884 (@jubnzv) +- LiveScript, see #1915 (@Enselic) +- MediaWiki, see #1925 (@sorairolake) +- The `requirements.txt` syntax has been removed due to incompatible license requirements. +- Dart, new highlighter, see #1959 (@Ersikan) +- SCSS and Sass syntaxes updated, see #1766 (@Enselic) +- PowerShell syntax updated, see #1935 (@Enselic) +- TypeScript syntax updated, see #1834 (@Enselic) + +## `bat` as a library + +- Deprecate `HighlightingAssets::syntaxes()` and `HighlightingAssets::syntax_for_file_name()`. Use `HighlightingAssets::get_syntaxes()` and `HighlightingAssets::get_syntax_for_path()` instead. They return a `Result` which is needed for upcoming lazy-loading work to improve startup performance. They also return which `SyntaxSet` the returned `SyntaxReference` belongs to. See #1747, #1755, #1776, #1862 (@Enselic) +- Remove `HighlightingAssets::from_files` and `HighlightingAssets::save_to_cache`. Instead of calling the former and then the latter you now make a single call to `bat::assets::build`. See #1802, #1971 (@Enselic) +- Replace the `error::Error(error::ErrorKind, _)` struct and enum with an `error::Error` enum. `Error(ErrorKind::UnknownSyntax, _)` becomes `Error::UnknownSyntax`, etc. Also remove the `error::ResultExt` trait. These changes stem from replacing `error-chain` with `thiserror`. See #1820 (@Enselic) +- Add new `MappingTarget` enum variant `MapExtensionToUnknown`. Refer to its documentation for more information. Also mark `MappingTarget` as `#[non_exhaustive]` since more enum variants might be added in the future. See #1703 (@cbolgiano), #2012 (@Enselic) + + +# v0.18.3 + +## Bugfixes + +- Bump `git2` dependency to fix build with Rust 1.54, see #1761 + + +# v0.18.2 + +## Features + +- Ignore known backup/template filename suffixes when selecting the syntax, see #1687 (@scop) + +## Bugfixes + +- Fix for a security vulnerability on Windows. Prior to this release, `bat` would execute programs called `less`/`less.exe` from the current working directory (instead of the one from `PATH`) with priority. An attacker might be able to use this by placing a malicious program in a shared directory where the user would execute `bat`. `bat` users on Windows are advised to upgrade to this version. See #1724 and #1472 (@Ry0taK). + +## Other + +- Add bash completion, see #1678 (@scop) +- Fix Clippy lints, see #1661 (@mohamed-abdelnour) +- Add syntax highlighting test files, see #1213 and #1668 (@mohamed-abdelnour) + +## Syntaxes + +- Upgraded Julia syntax to fix a highlighting bug, see #1692 +- Added support for `dash` syntax, see #1654 (@mohamed-abdelnour) +- Added support for `XAML` syntax, see #1590 and #1655 (@mohamed-abdelnour) +- Apply `DotENV` syntax also for `.env.default` and `.env.defaults` files, see #1669 + + +# v0.18.1 + +## Bugfixes + +- Mouse support and screen clearing broken for `less` versions with minor version number (581.2), see #1629 and #1639 (@aswild) + +## Other + +- `Input::ordinary_file` and `Input::with_name` now accept `Path` rather than `OsStr` see #1571 (@matklad) +- The `LESS` environment variable is now included in `bat --diagnostic`, see #1589 (@Enselic) +- Increased min. required Rust version to 1.45 + +## Syntaxes + +- Improved the Syslog syntax highlighting, see #1606 (@keith-hall) +- Replaced "Advanced CSV" with a custom CSV syntax definition written especially for `bat`; see #1574 (@keith-hall) +- Added SystemVerilog file syntax, see #1580 (@SeanMcLoughlin) +- Added Solidity and Vyper syntax, see #1602 (@Ersikan) + +## New themes + +- Dark+ VS Code theme, see #1588 and #1598 (@PatriotRossii) + + + +# v0.18.0 + +## Features + +- Use a pager when `bat --list-languages` is called, see #1394 (@stku1985) + +## Bugfixes + +- If the last line doesn't end with a newline character, don't add it if `--style=plain`, see #1438 (@Enselic) +- Only print themes hint in interactive mode (`bat --list-themes`), see #1439 (@rsteube) +- Make ./tests/syntax-tests/regression_test.sh work on recent versions of macOS, see #1443 (@Enselic) +- VimL syntax highlighting fix, see #1450 (@esensar) +- Print an 'Invalid syntax theme settings' error message if a custom theme is broken, see #614 (@Enselic) +- If plain mode is set and wrap is not explicitly opted in, long lines will no be truncated, see #1426 +- If `PAGER` (but not `BAT_PAGER` or `--pager`) is `more` or `most`, silently use `less` instead to ensure support for colors, see #1063 (@Enselic) +- If `PAGER` is `bat`, silently use `less` to prevent recursion. For `BAT_PAGER` or `--pager`, exit with error, see #1413 (@Enselic) +- Manpage highlighting fix, see #1511 (@keith-hall) +- `BAT_CONFIG_PATH` ignored by `bat` if non-existent, see #1550 (@sharkdp) + +## Other + +- Performance improvements, see #1421 (@LovecraftianHorror) +- Added a new `--diagnostic` option to collect information for bug reports, see #1459 (@sharkdp) +- Modified default theme colors to differentiate between a JSON key and a string value, see #1400 (@keith-hall) +- Upped min required Rust version to 1.42 + +## Syntaxes + +- Added Zig syntax, see #1470 (@paulsmith) +- Added Lean syntax, see #1446 (@Julian) +- Added `.resource` extension for Robot Framework files, see #1386 +- Added `gnuplot` syntax, see #1431 (@sharkdp) +- Highlight *.pac (Proxy auto-config) files as JavaScript, see #1515 (@sharkdp) + +## New themes + +- `ansi` replaces `ansi-dark` and `ansi-light`, see #1104 and #1412 (@mk12). **Breaking change:** users that were previously using one of the `ansi-*` themes should switch to `ansi`. +- The Gruvbox theme has been updated, see #1291 (@j0hnmeow). **Breaking change:** users that were previously using `gruvbox` or `gruvbox-white` should update and use `gruvbox-dark`/`gruvbox-light` instead. + +## `bat` as a library + +- The following `PrettyPrinter` methods have been removed (they were previously deprecated): + - `input_stdin_with_name` + - `input_from_bytes_with_name` + - `input_from_reader_with_name` + - `vcs_modification_markers` (if the `git` feature is not enabled) + + +# v0.17.1 + +## Bugfixes + +- Running `bat` without arguments fails ("output file is also an input"), see #1396 + + +# v0.17.0 + +## Features + +- Added a new `--style` value, `rule`, which adds a simple horizontal ruled line between files, see #1276 (@tommilligan) +- Pass `-S` ("chop long lines") to `less` if `--wrap=never` is set in `bat`, see #1255 (@gahag) + +## Bugfixes + +- Detect infinite loop when input and output are the same, see #1193 and #1197 (@niklasmohrin) +- Throw an error when `bat` is being used as `pager`, see #1343 (@adrian-rivera) +- Bash syntax highlighting not selected for `*.ebuild` and `*.eclass` files, see #1292 (@sharkdp) +- Fix `zsh` completion when using `-p`, see #1320 (@xzfc) + +## Other + +- Add note to refer to see detailed help with `--help` (and vice versa with `-h`), see #1215 (@henil) +- Add a `Contributors` section to `README`, see #1348 (@adrian-rivera) + +## Syntaxes + +- Manpage syntax highlighting has been improved, see #1315 (@keith-hall) +- Add Svelte file syntax, see #1285 (@kjmph) + +## New themes + +- Coldark, see #1329 (@armandphilippot) + + +# v0.16.0 + +## Features + +- Added support for the `NO_COLOR` environment variable, see #1021 and #1031 (@eth-p) +- Added `-P` short flag to disable paging, see #1075 and #1082 (@LordFlashmeow) +- Added `--force-colorization`/`-f` flag to provide an alias for forced color and decoration output, see #1141 (@alexanderkarlis) + +## Bugfixes + +- Fixed non-printable characters display for redirected output, see #1061 (@gsomix) +- Handle file extension conflicts in `--list-languages`, see #1076 and #1135 (@Kienyew) + +## Other + +- Switched to "·" (U+00B7) Middle Dot from "•" (U+2022) Bullet for non-printing spaces, see #1056 and #1100 (@LordFlashmeow) +- Added zsh shell completion script, see #1136 (@Kienyew) +- Improved `--help` text (@sharkdp) +- Added custom languages/themes sections to manpage (@eth-p) + +## Syntaxes + +- Update AsciiDoc syntax, see #1034 (@rxt1077) +- GLSL (@caioalonso) +- Add Nginx and Apache config file syntax, see #1137 (@kjmph, @niklasmohrin) +- Use `fstab` syntax for `crypttab` files, see #1073 (@sharkdp) +- Support syntax highlighting for files in `$XDG_CONFIG_HOME/git/`, see #1191 (@ahmedelgabri) + +## New themes + +- Gruvbox, see #1069 (@kyleondy) +- base16-256 for [base16-shell](https://github.com/chriskempson/base16-shell) users, see #1111 (@mk12) + +## `bat` as a library + +- Add APIs to provide `Input` descriptions with `InputDescription` (@eth-p) +- Add function to directly provide `Input`s to `PrettyPrinter` (@eth-p) +- **Breaking:** `Input::theme_preview_file` is no longer available. (@eth-p) + +## Packaging + +- Removed build dependency on `liquid` (@sharkdp). + +# v0.15.4 + +## Bugfixes + +- Added missing Solarized themes, see #1027 +- Fixed highlighting bug in Haskell source files, see #1026 + +# v0.15.3 + +## Bugfixes + +- Cannot run `bat` with relative paths, see #1022 +- bat mishighlights Users that start with digits in SSH config, see #984 + +## New syntaxes + +- SML, see #1005 (@kopecs) + +## Other + +- Some syntaxes and themes have been updated to the latest version + +# v0.15.2 + +## Bugfixes + +- Fix syntax detection for files called 'rails', see #1008 +- Fix potential errors with syntax detection for symlinked files, see #1001 +- `--map-syntax` doesn't work with names provided through `--file-name` (@eth-p) + +## Other + +- Add padding above headers when not using a grid, see #968 and #981 (@pt2121) +- bat now prints an error if an invalid syntax is specified via `-l` or `--map-syntax`, see #1004 (@eth-p) + +## `bat` as a library + +- `PrettyPrinter::vcs_modification_markers` has been marked deprecated when building without the `git` feature, see #997 and #1020 (@eth-p, @sharkdp) + +## Packaging + +- Compilation problems with `onig_sys` on various platforms have been resolved by upgrading to `syntect 4.2`, which includes a new `onig` version that allows to build `onig_sys` without the `bindgen` dependency. This removes the need for `libclang(-dev)` to be installed to compile `bat`. Package maintainers might want to remove `clang` as a build dependency. See #650 for more details. + +# v0.15.1 + +## Bugfixes + +- Fix highlighting of Markdown files, see #963 and #977 +- Fix `base16` theme (was broken since in v0.14), see #972, #934 and #979 (@mk12). + Users suffering from #865 ("no color for bat in ssh from a Windows client") can use the `ansi-dark` and `ansi-light` themes from now on. + +## New syntaxes + +- Fortran, see #957 +- Email (@mariozaizar) +- QML, see #962 (@pylipp) + +# v0.15.0 + +## Features + +- Add a new `--diff`/`-d` option that can be used to only show lines surrounding + Git changes, i.e. added, removed or modified lines. The amount of additional + context can be controlled with `--diff-context=N`. See #23 and #940 + +## Bugfixes + +- Error message printed in the middle of the output for another file, see #946 +- Performance improvements when using custom caches (via `bat cache --build`): the `bat` startup time should now be twice as fast (@lzutao). + +## Themes + +- Updated version of the Solarized dark/light themes, see #941 + +## `bat` as a library + +- There are a few changes in the "low level" API (the `Config` struct has changed and + the error handler needs a new `&mut dyn Write` argument). The high-level API is not + affected. + +# v0.14.0 + +## Features + +- Added a new `--file-name …` option to overwrite the displayed filename(s) + in the header. This is useful when piping input into `bat`. See #654 and #892 (@neuronull). +- Added a new `--generate-config-file` option to create an initial configuration file + at the right place. See #870 (@jmick414) + +## Bugfixes + +- Performance problems with C# source code have been fixed, see #677 (@keith-hall) +- Performance problems with Makefiles have been fixed, see #750 (@keith-hall) +- Fix bug when highlighting Ruby files with unindented heredocs, see #914 (@keith-hall) +- A highlighting problem with Rust source code has been fixed, see #924 (@keith-hall) +- Windows: short files that do not require paging are displayed and then lost, see #887 +- `--highlight-line` did not work correctly in combination with `--tabs=0` and `--wrap=never`, + see #937 + +## Other + +- When saving/reading user-provided syntaxes or themes, `bat` will now maintain a + `metadata.yaml` file which includes information about the `bat` version which was + used to create the cached files. When loading cached files, we now print an error + if they have been created with an incompatible version. See #882 +- Updated `liquid` dependency to 0.20, see #880 (@ignatenkobrain) + +## `bat` as a library + +- A completely new "high level" API has been added that is much more convenient + to use. See the `examples` folder for the updated code. The older "low level" + API is still available (basically everything that is not in the root `bat` + module), but has been refactored quite a bit. It is recommended to only use + the new "high level" API, if possible. This will be much easier to keep stable. + Note that this should still be considered a "beta" release of `bat`-as-a-library. + For more details and some screenshots of the example programs, see #936. +- Stripped out a lot of binary-only dependencies, see #895 and #899 (@dtolnay) + + This introduces a `features = ["application"]` which is enabled by default and pulls in + everything required by `bat` the application. When depending on bat as a library, downstream + `Cargo.toml` should disable this feature to cut out inapplicable heavy dependencies: + ``` toml + [dependencies] + bat = { version = "0.14", default-features = false } + ``` + Other optional functionality has also been put behind features: `paging` and `git` support. +- Allow using the library with older syntect, see #896 and #898 (@dtolnay) + +## New syntaxes + +- Rego, see #872 (@patrick-east) +- Stylo, see #917 + + +# v0.13.0 + +## `bat` as a library + +Beginning with this release, `bat` can be used as a library (#423). + +This was a huge effort and I want to thank all people who made this possible: @DrSensor, @mitsuhiko, @mre, @eth-p! + +- Initial attempt in #469 (@mitsuhiko) +- Second attempt, complete restructuring of the `bat` crate, see #679 (@DrSensor) +- Updates to example, public API, error handling, further refactoring: #693 #873 #875 (@sharkdp) + +I want to stress that this is the very first release of the library. Things are very likely to change. A lot of things are still missing (including the documentation). + +That being said, you can start using it! See the example programs in [`examples/`](https://github.com/sharkdp/bat/tree/master/examples). + +You can see the API documentation here: https://docs.rs/bat/ + +## Features + +- (**Breaking change**) Glob-based syntax mapping, see #877 and #592. With this change, + users need to update their bat config files (`bat --config-file`), if they have any `--map-syntax` settings + present. + + The option now works like this: + ```bash + --map-syntax : + ``` + + For more information, see the `--help` text, the man page or the README. + + This new feature allows us to properly highlight files like: + * `/etc/profile` + * `~/.ssh/config` + +- `--highlight-line` now accepts line ranges, see #809 (@lkalir) +- Proper wrapping support for output with wide Unicode characters, see #811 #787 and #815 (@Kogia-sima) +- A lot of updates to existing syntaxes via #644 (@benwaffle, @keith-hall) +- `BAT_CACHE_PATH` can be used to place cached `bat` assets in a non-standard path, see #829 (@neuronull) +- Support combination of multiple styles at the same time, see #857 (@aslpavel) + +## Bugfixes + +- Do not pass '--no-init' on newer less versions, see #749 and #786 (@sharkdp) +- 'bat cache' still takes precedence over existing files, see #666 (@sharkdp) +- `.sc` files should be treated as scala files, see #443 (@benwaffle) +- Allow underscores and dashes in page names, see #670 (@LunarLambda) +- Keep empty lines empty, see #601 (@mbarbar) +- Wrapping does not work when piping, see #758 (@fusillicode, @allevo, @gildo) +- Allow for non-unicode filenames, see #225 (@sharkdp) +- Empty file without header produces incomplete grid, see #798 (@eth-p) +- Files named `build` don't respect shebang lines, see #685 (@sharkdp) + +## Other + +- Parametrizable names for man page and shell completion files, see #659 #673 #656 (@eth-p) +- Enabled LTO, making `bat` about 10% faster, see #719 (@bolinfest, @sharkdp) +- Suggestions non how to configure `bat` for MacOS dark mode, see README (@jerguslejko) +- Extended ["Integration with other tools"](https://github.com/sharkdp/bat#integration-with-other-tools) section (@eth-p) +- Updated [instrutions on how to use `bat` as a `man`-pager](https://github.com/sharkdp/bat#man), see #652, see #667 (@sharkdp) +- Add section concerning file encodings, see #688 and #568 (@sharkdp) +- Updated sort order of command-line options in `--help` text and manpage, see #653 and #700 (@hrlmartins) +- Updates to the man page syntax, see #718 (@sharkdp) +- Japanese documentation updates, see #863 (@k-ta-yamada, @sorairolake and @wt-l00) +- Accept "default" as a theme, see #757 (@fvictorio) +- Updated Windows installation instructions, see #852 (@sorenbug) +- Updated man page, see #573 (@sharkdp) + +## New syntaxes + +- Jinja2, see #648 (@Martin819) +- SaltStack SLS, see #658 (@Martin819) +- `/etc/fstab`, see #696 (@flopp and @eth-p) +- `/etc/group` and `/etc/passwd`, see #698 (@argentite) +- `/proc/cpuinfo` and `/proc/meminfo`, see #593 (@sharkdp) +- Nim, see #542 (@sharkdp) +- Vue, see #826 (@chaaaaarlotte) +- CoffeScript, see #833 (@sharkdp) + +## New themes + +- Dracula, see #687 (@clarfon) +- Nord, see #760 (@crabique) +- Solarized light and dark, see #768 (@hakamadare) + +## Packaging + +- `bat` is now in the official Ubuntu and Debian repositories, see #323 and #705 (@MarcoFalke) +- `bat` can now be installed via MacPorts, see #675 (@bn3t) +- Install fish completions into 'vendor_completions.d', see #651 (@sharkdp) + +## Thanks + +- To @eth-p for joining me as a maintainer! I'm very grateful for all the work you put into + managing and responding to issues, improving our deployment, adding PR/issue templates (#837) as + well as fixing bugs and implementing new features. + +# v0.12.1 + +## Bugfixes + +- Fixes a bug for older Windows versions (*"The procedure entry point `CreateFile2` could not be located"*), see #643 (@rivy) + +# v0.12.0 + +## Features + +- Binary file content can now be viewed with `bat -A`, see #623, #640 (@pjsier and @sharkdp) +- `bat` can now be used as a man pager. Take a look at the README and #523 for more details. +- Add new style component to separate multiple `--line-range`s, see #570 (@eth-p) +- Added `-L` as an alias for `--list-languages` + +## Bugfixes + +- Output looks unbalanced when using '--style=grid,numbers' without 'header', see #571 (@eth-p) +- issues with filenames starting with "cache", see #584 +- Can't build cache with new theme without creating cache dir, see #576 (@frm) +- `--terminal-width -10` is parsed incorrectly, see #611 + +## Other + +- Added fish completions to DEB package, see #554 + +## New syntaxes + +- Emacs Org mode, see #36 (@bricewge) +- `requirements.txt` +- DotENV `.env` +- SSH config syntax (`-l ssh_config`), see #582 (@issmirnov) +- `/etc/hosts`, see #583 (@issmirnov) +- GraphQL, see #625 (@dandavison) +- Verilog, see #616 +- SCSS and Sass, see #637 +- `strace` syntax, see #599 + +## Packaging + +- `bat` is now in the official Gentoo repositories, see #588 (@toku-sa-n) +- `bat` is now in the official Alpine Linux repositories, see #586 (@5paceToast) +- `bat` is in the official Fedora repositories, see #610 (@ignatenkobrain) + +# v0.11.0 + +## Features + +- Three new special color themes are available: `ansi-light`, `ansi-dark` and `base16`. These + are useful for people that often switch from dark to light themes in their terminal emulator + or for people that want the colors to match their terminal theme colors. For more details, + see #543 and #490 (@mk12, implementation idea by @trishume) +- Hand-written auto completion script for Fish shell, see #524 and #555 (@ev-dev and @eth-p) +- The `-p`/`--plain` option can now be used twice (typically `-pp`). The first `-p` switches the + `--style` to "plain". The second `-p` disables the pager. See #560 and #552 (@eth-p) + +## Bugfixes + +- Do not replace arguments to `less` when using `--pager`, see #509 +- Binary files will now be indicated by a warning in interactive mode, see #530 #466 #550 (@maxfilov) +- Empty files are (once again) printed with a single header line, see #504 and #566 (@reidwagner + and @sharkdp) +- `--terminal-width=0` is now disallowed, see #559 (@eth-p) +- Accidental printing of files named `cache`, see #557 + +## Other + +- New integration tests, see #500 and #502 (@reidwagner and @sharkdp) +- New ["Integration with other tools"](https://github.com/sharkdp/bat#integration-with-other-tools) section in the README. +- Migrated to Rust 2018 (@expobrain) + +## New syntaxes + +- F# syntax has been updated, see #531 (@stroborobo) +- Fish shell, see #548 (@sanga) + +## Packaging + +- `bat` is now available on Chocolatey, see #541 (@rasmuskriest) + +# v0.10.0 + +## Features + +- Added new `--highlight-line ` option, see #453, #346 and #175 (@tskinn and @sharkdp) + +## Changes + +- **Change the default configuration directory on macOS** to `~/.config/bat`, see #442 (@lavifb). If you are on macOS, you need to copy your configuration directory from the previous place (`~/Library/Preferences/bat`) to the new place (`~/.config/bat`). +- Completely disabled the generation of shell completion files, see #372 +- Properly set arguments to `less` if `PAGER` environment variable contains something like `less -F` (which is missing the `-R` option), see #430 (@majecty) +- Report the name of missing files, see #444 (@ufuji1984) +- Don't start pager if file doesn't exist, see #387 +- Rename `bat cache --init` to `bat cache --build`, see #498 +- Move the `--config-dir` and `--cache-dir` options from `bat cache` to `bat` and hide them from the help text. + +## Bugfixes + +- Blank line at the end of output when using `--style=plain`, see #379 +- EOF must be sent twice on stdin if no other input is sent, see #477 (@reidwagner) + +## New syntaxes + +- Twig (@ahmedelgabri) +- `.desktop` files (@jleclanche) +- AsciiDoc (@markusthoemmes) +- Assembly (x86_64 and ARM) +- Log files (@caos21) +- Protobuf and ProtobufText (@caos21) +- Terraform (@caos21) +- Jsonnet (@hfm) +- Varlink (@haraldh) + +## Other + +- Added Japanese version of README (@sh-tech and @object1037) +- Code improvements (@barskern) + +# v0.9.0 + +## Features + +- A new `-A`/`--show-all` option has been added to show and highlight non-printable characters (in analogy to GNU `cat`s option): + + ![](https://camo.githubusercontent.com/c3e769482ef3184f6be6adaa34bdc8d19c378254/68747470733a2f2f692e696d6775722e636f6d2f324b54486859542e706e67) + + see #395 and #381 for more details. + +- Added `--pager` option (to configure the pager from the configuration file), see #362 (@majecty) + +- Added `BAT_CONFIG_PATH` environment variable to set a non-default path for `bat`s configuration file, see #375 (@deg4uss3r) + +- Allow for multiple occurrences of `--style` to allow for the configuration + of styles from the config file, see #367 (@sindreij) + +- Allow for multiple `--line-range` arguments, see #23 + +- The `--terminal-width` option can now also accept offsets, see #376 + +## Changes + +- Use of italics is now *disabled by default* (see #389 for details). They can be + re-enabled by adding `--italic-text=always` to your configuration file. + +- The default tab-width has been set to 4. + +- Added new "Sublime Snazzy" theme. + +- Shell completions are currently *not* shipped anymore, see #372 for details. + +## Bugfixes + +- Avoid endless recursion when `PAGER="bat"`, see #383 (@rodorgas) + +## Other + +- `bat` is now available on openSUSE, see #405 (@dmarcoux) + +- Added section about the new configuration file in the README (@deg4uss3r) + +- Chinese translation of README (@chinanf-boy) + +- Re-written tests for `--tabs` (@choznerol) + +- Speed up integration tests, see #394 + +# v0.8.0 + +## Features + +- Support for a configuration file with the following simple format: + + ```bash + --tabs=4 + --theme="Sublime Snazzy" + + # A line-comment + --map-syntax .ignore:.gitignore + --map-syntax PKGBUILD:bash + --map-syntax Podfile:ruby + + # Flags and options can also be on a single line: + --wrap=never --paging=never + ``` + + The configuration file path can be accessed via `bat --config-file`. On Linux, + it is stored in `~/.config/bat/config`. + +- Support for the `BAT_OPTS` environment variable with the same format as specified + above (in a single line). This takes precedence over the configuration file. + + See also #310. + +- Support for custom syntax mappings via the `-m`/`--max-syntax` option. + + This allows users to (re)map certain file extensions or file names to an existing syntax: + + ``` bash + bat --map-syntax .config:json ... + ``` + + The option can be use multiple times. Note that you can easily make these mappings permanent by using bats new configuration file. + + See #169 + +- Support pager command-line arguments in `PAGER` and `BAT_PAGER`, see #352 (@Foxboron) + +- Add support for wildcards in Windows CMD, see #309 (@zxey) + +- First-line syntax detection for all input types, see #205 + +- Encoding support for UTF-16LE and UTF-16BE, see #285 + +- New syntaxes: Robot framework (@sanga) + +## Changes + +- Binary files are now detected and not displayed when the output goes to an interactive terminal, see #205 + +## Bugfixes + +- JavaDoc comments break syntax highlighting in .java files, see #81 + +- Bat Panics on Haskell Source Code, see #314 + +## Other + +- Better `-h` and `--help` texts. + +- Updated documentation on how to configure `bat`s pager + +- Updated documentation for light backgrounds, see #328 (@russtaylor) + +- Generate shell completions during build, see #115 (@davideGiovannini) + +- A lot of new tests have been written + +- `bat` is now available via [Termux](https://termux.com/), see #341 (@fornwall) + +- `bat` is now available via [nix](https://nixos.org/nix), see #344 (@mgttlinger) + +- `bat` is now available via [Docker](https://hub.docker.com/r/danlynn/bat/), see #331 (@danlynn) + +# v0.7.1 + +## Features + +- Use the `ansi_colours` package by @mina86 for better true-color approximation on 8 bit color terminals, see #319 and #202. + +## Bugfixes + +- Bat Panics on Haskell Source Code, see #314 +- Disable wrapping when `--style=plain`/`-p` is used, see #289 + +## Other + +- Added Ansible install instructions (@aeimer) +- Added section about Cygwin to the README (@eth-p) + +# v0.7.0 + +## Features + +- Tabs are now (optionally) expanded to spaces. This can be controlled with the new + `--tabs` command-line option or the `BAT_TABS` environment variable. The + new feature also closes two bugs #166 and #184. For more information, see #302 (@eth-p). + +- Added support for the `BAT_STYLE` environment variable, see #208 (@ms2300) + +- Added `OneHalf` theme for terminals with a light-gray background, see #256 + +- Added new syntaxes for CSV, JSX in JavaScript and TypeScript, Cabal, Dart, + F#, PureScript, Swift, Crystal, PowerShell (Many Thanks to @tobenna and @mimadrid) + +## Changes + +- Query `git diff` only when needed, see #303 (@ShikChen) + +- Disable wrapping when `--plain` is used, see #289 (@eth-p) + +## Bugfixes + +- Can read files named `cache`, see #275 (@BK1603) + +- A lot of bugfixes for Windows, see #252, #264 + +- Detect `less` reliably and in a portable way, see #271 and #290 (@Aankhen) + +- last decoration line is not formatted properly with `--wrap never`, see #299 (@Rogach) + +- Do not show file header for directories, see #292 + +## Other + +- Enabled a new `aarch64` build target, see #258 (@rachitchokshi) + +- Provide Debian packages for `armhf`, see #280 (@rachitchokshi) + +- Added README section about "`bat` on Windows" (@Aankhen) + +- Windows installation via scoop (@meltinglava) + +# v0.6.1 + +## Bugfixes + +- Fixed panic when running `bat --list-languages | head`, see #232 (@mchlrhw) +- Respect `--color` settings for `--list-themes` and `--list-languages`, see #233 +- Git modifications now work on Windows + +## Other + +- There will be auto-generated Windows releases, starting with this version (@anykao) + +# v0.6.0 + +## Features + +- The `--list-themes` option now shows a preview for each highlighting theme (@ms2300) +- Added `-p`/`--plain` as an alias for `--style=plain`, see #212 (@ms2300) +- Major refactorings, enabling some progress on #150. In non-interactive mode, `bat` will now copy input bytes 1:1. +- New languages: Elm, Kotlin, Puppet, TypeScript, see #215 #216 #217 #218 +- New syntax highlighting theme: zenburn (@colindean) + +## Changes + +- New themes in `$BAT_CONFIG_DIR/themes` are now loaded *in addition* to + the default themes (they may also override), see #172 +- The `Default.tmTheme` symlink is not necessary anymore. + +## Bugfixes + +* Using `bat cache --init` leads to duplicated syntaxes, see #206 + +## Other + +* Extended and cleaned-up `--help` text. +* Added initial version of a man page, see #52 +* New README sections: *Development* and *Troubleshooting*, see #220 + +# v0.5.0 + +## Features + +- Added `--line-range n:m` option to print a range of lines, see #159 (@tskinn) +- The syntax highlighting theme can now be controlled by the `BAT_THEME` environment variable, see [README](https://github.com/sharkdp/bat#highlighting-theme) and #177 (@mandx) +- The `PAGER` and `BAT_PAGER` environment variables can be used to control the pager that `bat` uses, see #158 and the [new README section](https://github.com/sharkdp/bat#using-a-different-pager) +- Added syntax highlighting for Nix, see #180 +- Added syntax highlighting for AWK (Gert Hulselmans) + +## Changes + +- The customization of syntax sets and theme sets is now separated. Syntax definitions are now loaded *in addition* to the ones that are stored in the `bat` binary by default. Please refer to these new sections in the README: [Adding new syntaxes](https://github.com/sharkdp/bat#adding-new-syntaxes--language-definitions), [Adding new themes](https://github.com/sharkdp/bat#adding-new-themes), also see #172 +- The color for the filename is now the default foreground color. The colors for the grid and the line numbers is now determined from the syntax highlighting theme, which now also works for light backgrounds, see #178. + +## Bugfixes + +- Escape Sequences get partially stripped, see #182 (@eth-p) +- Use separate Git repository for snapshot testing, see #165 and #161 +- Markdown breaking on JavaScript, see #183 + +## Other + +- Binaries for armv7 are now provided, see #196 +- `bat` is now in the official [Arch package repositories](https://www.archlinux.org/packages/community/x86_64/bat/). +- Optimizations in the RGB => 8-bit conversion (@mina86) + +# v0.4.1 + +(this is just a small bugfix release, see 0.4.0 for all features and changes) + +## Bugfixes + +- Fix problem with `cargo test` when `bat` is not checked out in a Git repository, see #161 + +# v0.4.0 + +## Features + +* Support for line-wrapping, see #54 and #102 (@eth-p) +* New and updated `--style` parameter, see #74 and README (@pitkley) +* Added `--theme` and `--list-themes` options, see #89 (@rleungx) +* Added syntax highlighting for: Julia (@iamed2), Dockerfiles, VimL, CMake, INI, Less +* Added a few popular Sublime Text highlighting themes, see #133 +* Support for bold, italic and underline font styles, see #96 +* Support for 32bit systems is now available, see #84 +* Added `-u` and `-n` options, see #134 +* ANSI color support on Windows 10 + +## Changes + +* The customization folder for own syntaxes has been renamed from `syntax` to `syntaxes`, see README. +* Changed Markdown syntax to the default Sublime Text syntax, see #157 +* Sorted language listing (@rleungx) +* Command line arguments like `--theme` or `--color` can now override themselves. +* Improved `--help` text. + +## Bugfixes + +- Fixed crash for (really) small terminal sizes, see #117 (@eth-p) +- Syntax detection for `.bashrc`, `CMakeLists.txt` etc., see #100 +- Properly handle lines with invalid UTF-8, see #7 (@BrainMaestro) +- Better error handling, see #17 (@rleungx and @sharkdp) +- Proper handling of UTF-8 characters in `less`, see #98 (@ghuls) +- Build fix on nightly, see #148 (@tathanhdinh) + +## Other + +- [Comparison with alternative projects](https://github.com/sharkdp/bat/blob/master/doc/alternatives.md). +- New "bat" logo in the README, see #119 (@jraulhernandezi) +- Output test cases (@BrainMaestro) +- Lots of great refactoring work (@BrainMaestro) + +# v0.3.0 + +## Features + +* Automatic paging by integrating with `less`, see #29 (@BrainMaestro) +* Added support for reading from standard input, see #2 +* Added support for writing to non-interactive terminals (pipes, files, ..); new + `--color=auto/always/never` option, see #26 (@BrainMaestro) +* Added `--list-languages` option to print all available syntaxes, see #69 (@connorkuehl) +* New option to specify the syntax via `-l`/`--language`, see #19 (@BrainMaestro) +* New option to control the output style (`--style`), see #5 (@nakulcg) +* Added syntax highlighting support for TOML files, see #37 + +## Changes + +* The `init-cache` sub-command has been removed. The cache can now be controlled via + `bat cache`. See `bat cache -h` for all available commands. + +## Bug fixes + +* Get git repository from file path instead of current directory, see #22 (@nakulcg) +* Process substitution can now be used with bat (`bat <(echo a) <(echo b)`), see #80 + +## Thanks + +I'd like to say a big THANK YOU to all contributors and everyone that has given us +some form of feedback. + +Special thanks go to @BrainMaestro for his huge support with new features, bug reports +and code reviews! + +# v0.2.3 + +- Added a new statically linked version of bat (`..-musl-..`) + +# v0.2.2 + +- Remove openssl dependency completely, see #30. + +# v0.2.1 + +- Added Elixir syntax, see #25. +- Use libcurl-openssl instead of libcurl-gnutls, see #30. + +# v0.2.0 + +- Support for custom syntaxes, add 'Markdown extended' theme +- Bugfix: Git modifications not shown from child folder + +# v0.1.0 + +Initial release diff --git a/README.md b/README.md new file mode 100644 index 0000000..8826d4e --- /dev/null +++ b/README.md @@ -0,0 +1,941 @@ +

+ bat - a cat clone with wings
+ Build Status + license + Version info
+ A cat(1) clone with syntax highlighting and Git integration. +

+ +

+ Key Features • + How To Use • + Installation • + Customization • + Project goals, alternatives
+ [English] + [中文] + [日本語] + [한국어] + [Русский] +

+ +### Syntax highlighting + +`bat` supports syntax highlighting for a large number of programming and markup +languages: + +![Syntax highlighting example](https://imgur.com/rGsdnDe.png) + +### Git integration + +`bat` communicates with `git` to show modifications with respect to the index +(see left sidebar): + +![Git integration example](https://i.imgur.com/2lSW4RE.png) + +### Show non-printable characters + +You can use the `-A`/`--show-all` option to show and highlight non-printable +characters: + +![Non-printable character example](https://i.imgur.com/WndGp9H.png) + +### Automatic paging + +By default, `bat` pipes its own output to a pager (e.g. `less`) if the output is too large for one screen. +If you would rather `bat` work like `cat` all the time (never page output), you can set `--paging=never` as an option, either on the command line or in your configuration file. +If you intend to alias `cat` to `bat` in your shell configuration, you can use `alias cat='bat --paging=never'` to preserve the default behavior. + +#### File concatenation + +Even with a pager set, you can still use `bat` to concatenate files :wink:. +Whenever `bat` detects a non-interactive terminal (i.e. when you pipe into another process or into a file), `bat` will act as a drop-in replacement for `cat` and fall back to printing the plain file contents, regardless of the `--pager` option's value. + +## How to use + +Display a single file on the terminal + +```bash +bat README.md +``` + +Display multiple files at once + +```bash +bat src/*.rs +``` + +Read from stdin, determine the syntax automatically (note, highlighting will +only work if the syntax can be determined from the first line of the file, +usually through a shebang such as `#!/bin/sh`) + +```bash +curl -s https://sh.rustup.rs | bat +``` + +Read from stdin, specify the language explicitly + +```bash +yaml2json .travis.yml | json_pp | bat -l json +``` + +Show and highlight non-printable characters: +```bash +bat -A /etc/hosts +``` + +Use it as a `cat` replacement: + +```bash +bat > note.md # quickly create a new file + +bat header.md content.md footer.md > document.md + +bat -n main.rs # show line numbers (only) + +bat f - g # output 'f', then stdin, then 'g'. +``` + +### Integration with other tools + +#### `fzf` + +You can use `bat` as a previewer for [`fzf`](https://github.com/junegunn/fzf). To do this, +use `bat`'s `--color=always` option to force colorized output. You can also use `--line-range` +option to restrict the load times for long files: + +```bash +fzf --preview "bat --color=always --style=numbers --line-range=:500 {}" +``` + +For more information, see [`fzf`'s `README`](https://github.com/junegunn/fzf#preview-window). + +#### `find` or `fd` + +You can use the `-exec` option of `find` to preview all search results with `bat`: + +```bash +find … -exec bat {} + +``` + +If you happen to use [`fd`](https://github.com/sharkdp/fd), you can use the `-X`/`--exec-batch` option to do the same: + +```bash +fd … -X bat +``` + +#### `ripgrep` + +With [`batgrep`](https://github.com/eth-p/bat-extras/blob/master/doc/batgrep.md), `bat` can be used as the printer for [`ripgrep`](https://github.com/BurntSushi/ripgrep) search results. + +```bash +batgrep needle src/ +``` + +#### `tail -f` + +`bat` can be combined with `tail -f` to continuously monitor a given file with syntax highlighting. + +```bash +tail -f /var/log/pacman.log | bat --paging=never -l log +``` + +Note that we have to switch off paging in order for this to work. We have also specified the syntax +explicitly (`-l log`), as it can not be auto-detected in this case. + +#### `git` + +You can combine `bat` with `git show` to view an older version of a given file with proper syntax +highlighting: + +```bash +git show v0.6.0:src/main.rs | bat -l rs +``` + +#### `git diff` + +You can combine `bat` with `git diff` to view lines around code changes with proper syntax +highlighting: +```bash +batdiff() { + git diff --name-only --relative --diff-filter=d -z | xargs -0 bat --diff +} +``` +If you prefer to use this as a separate tool, check out `batdiff` in [`bat-extras`](https://github.com/eth-p/bat-extras). + +If you are looking for more support for git and diff operations, check out [`delta`](https://github.com/dandavison/delta). + +#### `xclip` + +The line numbers and Git modification markers in the output of `bat` can make it hard to copy +the contents of a file. To prevent this, you can call `bat` with the `-p`/`--plain` option or +simply pipe the output into `xclip`: +```bash +bat main.cpp | xclip +``` +`bat` will detect that the output is being redirected and print the plain file contents. + +#### `man` + +`bat` can be used as a colorizing pager for `man`, by setting the +`MANPAGER` environment variable: + +```bash +export MANPAGER="bat -plman" +man 2 select +``` +(on some older Debian or Ubuntu releases, the executable is named `batcat` instead of `bat`) + +If you prefer to have this bundled in a new command, you can also use [`batman`](https://github.com/eth-p/bat-extras/blob/master/doc/batman.md). + +Note that the [Manpage syntax](assets/syntaxes/02_Extra/Manpage.sublime-syntax) is developed in this repository and still needs some work. + +#### `prettier` / `shfmt` / `rustfmt` + +The [`prettybat`](https://github.com/eth-p/bat-extras/blob/master/doc/prettybat.md) script is a wrapper that will format code and print it with `bat`. + +#### Highlighting `--help` messages + +You can use `bat` to colorize help text: `$ cp --help | bat -plhelp` + +You can also use a wrapper around this: + +```bash +# in your .bashrc/.zshrc/*rc +alias bathelp='bat --plain --language=help' +help() { + "$@" --help 2>&1 | bathelp +} +``` + +Then you can do `$ help cp` or `$ help git commit`. + +When you are using `zsh`, you can also use global aliases to override `-h` and `--help` entirely: + +```bash +alias -g -- -h='-h 2>&1 | bat --language=help --style=plain' +alias -g -- --help='--help 2>&1 | bat --language=help --style=plain' +``` + +For `fish`, you can use abbreviations: + +```fish +abbr -a --position anywhere -- --help '--help | bat -plhelp' +abbr -a --position anywhere -- -h '-h | bat -plhelp' +``` + +This way, you can keep on using `cp --help`, but get colorized help pages. + +> [!TIP] +> To remove these abbreviations later, run: +> ```fish +> abbr -e -- --help +> abbr -e -- -h +> ``` +> The `--` before the abbreviation name is required because `--help` and `-h` start with dashes, which would otherwise be interpreted as flags to `abbr` itself. + +Be aware that in some cases, `-h` may not be a shorthand of `--help` (for example with `ls`). In cases where you need to use `-h` +as a command argument you can prepend `\` to the argument (eg. `ls \-h`) to escape the aliasing defined above. + +Please report any issues with the help syntax in [this repository](https://github.com/victor-gp/cmd-help-sublime-syntax). + + +## Installation + + + +[![Packaging status](https://repology.org/badge/vertical-allrepos/bat-cat.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/bat-cat/versions) + +### On Ubuntu (using `apt`) +*... and other Debian-based Linux distributions.* + +`bat` is available on [Ubuntu since 20.04 ("Focal")](https://packages.ubuntu.com/search?keywords=bat&exact=1) and [Debian since August 2021 (Debian 11 - "Bullseye")](https://packages.debian.org/bullseye/bat). + +If your Ubuntu/Debian installation is new enough you can simply run: + +```bash +sudo apt install bat +``` + +**Important**: On some older Ubuntu/Debian releases, the executable is installed as `batcat` instead of `bat` (due to [a name +clash with another package](https://github.com/sharkdp/bat/issues/982)). On newer releases, the executable is available as `bat`. If `bat --version` does not work after installation, try `batcat --version` instead. You can set up a `bat -> batcat` symlink or alias to prevent any issues that may come up because of this and to be consistent with other distributions: +``` bash +mkdir -p ~/.local/bin +ln -s /usr/bin/batcat ~/.local/bin/bat +``` + +an example alias for `batcat` as `bat`: +```bash +alias bat="batcat" +``` + +### On Ubuntu (using most recent `.deb` packages) +*... and other Debian-based Linux distributions.* + +If the package has not yet been promoted to your Ubuntu/Debian installation, or you want +the most recent release of `bat`, download the latest `.deb` package from the +[release page](https://github.com/sharkdp/bat/releases) and install it via: + +```bash +sudo dpkg -i bat_0.18.3_amd64.deb # adapt version number and architecture +``` + +### On Alpine Linux + +You can install [the `bat` package](https://pkgs.alpinelinux.org/packages?name=bat) +from the official sources, provided you have the appropriate repository enabled: + +```bash +apk add bat +``` + +### On Arch Linux + +You can install [the `bat` package](https://www.archlinux.org/packages/extra/x86_64/bat/) +from the official sources: + +```bash +pacman -S bat +``` + +### On Fedora + +You can install [the `bat` package](https://packages.fedoraproject.org/pkgs/rust-bat/bat/) from the official sources: + +```bash +dnf install bat +``` + +### On Gentoo Linux + +You can install [the `bat` package](https://packages.gentoo.org/packages/sys-apps/bat) +from the official sources: + +```bash +emerge sys-apps/bat +``` + +### On FreeBSD + +You can install a precompiled [`bat` package](https://www.freshports.org/textproc/bat) with pkg: + +```bash +pkg install bat +``` + +or build it on your own from the FreeBSD ports: + +```bash +cd /usr/ports/textproc/bat +make install +``` + +### On OpenBSD + +You can install `bat` package using [`pkg_add(1)`](https://man.openbsd.org/pkg_add.1): + +```bash +pkg_add bat +``` + +### Via nix + +You can install `bat` using the [nix package manager](https://nixos.org/nix): + +```bash +nix-env -i bat +``` + +### On openSUSE + +You can install `bat` with zypper: + +```bash +zypper install bat +``` + +### Via snap package + +There is currently no recommended snap package available. +Existing packages may be available, but are not officially supported and may contain [issues](https://github.com/sharkdp/bat/issues/1519). + +### On macOS (or Linux) via Homebrew + +You can install `bat` with [Homebrew](https://formulae.brew.sh/formula/bat): + +```bash +brew install bat +``` + +### On macOS via MacPorts + +Or install `bat` with [MacPorts](https://ports.macports.org/port/bat/summary): + +```bash +port install bat +``` + +### On Windows + +There are a few options to install `bat` on Windows. Once you have installed `bat`, +take a look at the ["Using `bat` on Windows"](#using-bat-on-windows) section. + +#### Prerequisites + +You will need to install the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) + +#### With WinGet + +You can install `bat` via [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/winget): + +```bash +winget install sharkdp.bat +``` + +#### With Chocolatey + +You can install `bat` via [Chocolatey](https://chocolatey.org/packages/Bat): +```bash +choco install bat +``` + +#### With Scoop + +You can install `bat` via [scoop](https://scoop.sh/): +```bash +scoop install bat +``` + +#### From prebuilt binaries: + +You can download prebuilt binaries from the [Release page](https://github.com/sharkdp/bat/releases), + +You will need to install the [Visual C++ Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads) package. + +### From binaries + +Check out the [Release page](https://github.com/sharkdp/bat/releases) for +prebuilt versions of `bat` for many different architectures. Statically-linked +binaries are also available: look for archives with `musl` in the file name. + +### From source + +If you want to build `bat` from source, you need Rust 1.79.0 or +higher. You can then use `cargo` to build everything: + +#### From local source +```bash +cargo install --path . --locked +``` +> [!NOTE] +> The `--path .` above specifies the directory of the source code and NOT where `bat` will be installed. +> For more information see the docs for [`cargo install`](https://doc.rust-lang.org/cargo/commands/cargo-install.html). + +#### From `crates.io` +```bash +cargo install --locked bat +``` + +Note that additional files like the man page or shell completion +files can not be installed automatically in both these ways. +If installing from a local source, they will be generated by `cargo` +and should be available in the cargo target folder under `build`. + +Furthermore, shell completions are also available by running: +```bash +bat --completion +# see --help for supported shells +``` + +## Customization + +### Highlighting theme + +Use `bat --list-themes` to get a list of all available themes for syntax +highlighting. By default, `bat` uses `Monokai Extended` or `Monokai Extended Light` +for dark and light themes respectively. To select the `TwoDark` theme, call `bat` +with the `--theme=TwoDark` option or set the `BAT_THEME` environment variable to +`TwoDark`. Use `export BAT_THEME="TwoDark"` in your shell's startup file to +make the change permanent. Alternatively, use `bat`'s +[configuration file](#configuration-file). + +If you want to preview the different themes on a custom file, you can use +the following command (you need [`fzf`](https://github.com/junegunn/fzf) for this): +```bash +bat --list-themes | fzf --preview="bat --theme={} --color=always /path/to/file" +``` + +`bat` automatically picks a fitting theme depending on your terminal's background color. +You can use the `--theme-dark` / `--theme-light` options or the `BAT_THEME_DARK` / `BAT_THEME_LIGHT` environment variables +to customize the themes used. This is especially useful if you frequently switch between dark and light mode. + +You can also use a custom theme by following the +['Adding new themes' section below](#adding-new-themes). + +### 8-bit themes + +`bat` has three themes that always use [8-bit colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors), +even when truecolor support is available: + +- `ansi` looks decent on any terminal. It uses 3-bit colors: black, red, green, + yellow, blue, magenta, cyan, and white. +- `base16` is designed for [base16](https://github.com/tinted-theming/home) terminal themes. It uses + 4-bit colors (3-bit colors plus bright variants) in accordance with the + [base16 styling guidelines](https://github.com/tinted-theming/home/blob/main/styling.md). +- `base16-256` is designed for [tinted-shell](https://github.com/tinted-theming/tinted-shell). + It replaces certain bright colors with 8-bit colors from 16 to 21. **Do not** use this simply + because you have a 256-color terminal but are not using tinted-shell. + +Although these themes are more restricted, they have three advantages over truecolor themes. They: + +- Enjoy maximum compatibility. Some terminal utilities do not support more than 3-bit colors. +- Adapt to terminal theme changes. Even for already printed output. +- Visually harmonize better with other terminal software. + +### Output style + +You can use the `--style` option to control the appearance of `bat`'s output. +You can use `--style=numbers,changes`, for example, to show only Git changes +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](#configuration-file). + +By default, `bat` enables `changes`, `grid`, `header-filename`, `numbers`, and `snip`. + +The available pre-defined styles are: + +| Style | Description | +|-------|-------------| +| `default` | Enables the recommended style components listed above. | +| `full` | Enables all available components. | +| `auto` | Same as `default`, unless the output is piped. | +| `plain` | Disables all available components. | + +The available individual components are: + +| Component | Description | +|-----------|-------------| +| `changes` | Show Git modification markers. | +| `header` | Alias for `header-filename`. | +| `header-filename` | Show filenames before the content. | +| `header-filesize` | Show file sizes before the content. | +| `grid` | Vertical/horizontal lines to separate the side bar and header from the content. | +| `rule` | Horizontal lines to delimit files. | +| `numbers` | Show line numbers in the side bar. | +| `snip` | Draw separation lines between distinct line ranges. | + +>[!tip] +> 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. + +### Decorations + +By default, `bat` only shows decorations (such as line numbers, file headers, grid borders, etc.) when outputting to an interactive terminal. You can control this behavior with the `--decorations` option. Use `--decorations=always` to show decorations even when piping output to another command, or `--decorations=never` to disable them entirely. Possible values are `auto` (default), `never`, and `always`. + +There is also the `--force-colorization` option, which is an alias for `--decorations=always --color=always`. This is useful if you want to keep colorization and decorations when piping `bat`'s output to another program. + +### Adding new syntaxes / language definitions + +Should you find that a particular syntax is not available within `bat`, you can follow these +instructions to easily add new syntaxes to your current `bat` installation. + +`bat` uses the excellent [`syntect`](https://github.com/trishume/syntect/) +library for syntax highlighting. `syntect` can read any +[Sublime Text `.sublime-syntax` file](https://www.sublimetext.com/docs/3/syntax.html) +and theme. + +A good resource for finding Sublime Syntax packages is [Package Control](https://packagecontrol.io/). Once you found a +syntax: + +1. Create a folder with syntax definition files: + + ```bash + mkdir -p "$(bat --config-dir)/syntaxes" + cd "$(bat --config-dir)/syntaxes" + + # Put new '.sublime-syntax' language definition files + # in this folder (or its subdirectories), for example: + git clone https://github.com/tellnobody1/sublime-purescript-syntax + ``` + +2. Now use the following command to parse these files into a binary cache: + + ```bash + bat cache --build + ``` + +3. Finally, use `bat --list-languages` to check if the new languages are available. + + If you ever want to go back to the default settings, call: + + ```bash + bat cache --clear + ``` + +4. If you think that a specific syntax should be included in `bat` by default, please + consider opening a "syntax request" ticket after reading the policies and + instructions [here](doc/assets.md): [Open Syntax Request](https://github.com/sharkdp/bat/issues/new?labels=syntax-request&template=syntax_request.md). + +### Adding new themes + +This works very similar to how we add new syntax definitions. +> [!NOTE] +> Custom themes must be stored in [`.tmTheme` files](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). +> Newer `.sublime-color-scheme` files are currently not supported. + +First, create a folder with the new syntax highlighting themes: +```bash +mkdir -p "$(bat --config-dir)/themes" +cd "$(bat --config-dir)/themes" + +# Download a theme in '.tmTheme' format, for example: +git clone https://github.com/greggb/sublime-snazzy + +# Update the binary cache +bat cache --build +``` + +Finally, use `bat --list-themes` to check if the new themes are available. +> [!NOTE] +> `bat` uses the name of the `.tmTheme` file for the theme's name. + +### Adding or changing file type associations + +You can add new (or change existing) file name patterns using the `--map-syntax` +command line option. The option takes an argument of the form `pattern:syntax` where +`pattern` is a glob pattern that is matched against the file name and +the absolute file path. The `syntax` part is the full name of a supported language +(use `bat --list-languages` for an overview). + +**Note:** You probably want to use this option as [an entry in `bat`'s configuration file](#configuration-file) +for persistence instead of passing it on the command line as a one-off. Generally +you'd just use `-l` if you want to manually specify a language for a file. + +Example: To use "INI" syntax highlighting for all files with a `.conf` file extension, use +```bash +--map-syntax='*.conf:INI' +``` + +Example: To open all files called `.ignore` (exact match) with the "Git Ignore" syntax, use: +```bash +--map-syntax='.ignore:Git Ignore' +``` + +Example: To open all `.conf` files in subfolders of `/etc/apache2` with the "Apache Conf" +syntax, use (this mapping is already built in): +```bash +--map-syntax='/etc/apache2/**/*.conf:Apache Conf' +``` + +### Using a different pager + +`bat` uses the pager that is specified in the `PAGER` environment variable. If this variable is not +set, `less` is used by default. You can also use bat's built-in pager with `--pager=builtin` or +by setting the `BAT_PAGER` environment variable to "builtin". + +If you want to use a different pager, you can either modify the `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. + +If you want to pass command-line arguments to the pager, you can also set them via the +`PAGER`/`BAT_PAGER` variables: + +```bash +export BAT_PAGER="less -RFK" +``` + +Instead of using environment variables, you can also use `bat`'s [configuration file](#configuration-file) to configure the pager (`--pager` option). + + +### Using `less` as a pager + +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`, +`-K`/`--quit-on-intr` 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`/`--RAW-CONTROL-CHARS` option is needed to interpret ANSI colors correctly. + +The `-F`/`--quit-if-one-screen` 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 `-K`/`--quit-on-intr` option instructs `less` to exit immediately when an interrupt signal is received. +This is useful to ensure that `less` quits together with `bat` on SIGINT. + +The `-X`/`--no-init` option is added to versions of `less` older than version 530 (older than 558 on Windows) to +fix a bug with the `-F`/`--quit-if-one-screen` feature. 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`/`--chop-long-lines` 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. + +### Indentation + +`bat` expands tabs to 4 spaces by itself, not relying on the pager. To change this, simply add the +`--tabs` argument with the number of spaces you want to be displayed. + +**Note**: Defining tab stops for the pager (via the `--pager` argument by `bat`, or via the `LESS` +environment variable for `less`) won't be taken into account because the pager will already get +expanded spaces instead of tabs. This behaviour is added to avoid indentation issues caused by the +sidebar. Calling `bat` with `--tabs=0` will override it and let tabs be consumed by the pager. + +### Dark mode + +If you make use of the dark mode feature in **macOS**, you might want to configure `bat` to use a different +theme based on the OS theme. The following snippet uses the `default` theme when in the _dark mode_ +and the `GitHub` theme when in the _light mode_. + +```bash +alias cat="bat --theme auto:system --theme-dark default --theme-light GitHub" +``` + +The same dark mode feature is now available in **GNOME** and affects the `org.gnome.desktop.interface color-scheme` setting. The following code converts the above to use said setting. + +```bash +# .bashrc +sys_color_scheme_is_dark() { + condition=$(gsettings get org.gnome.desktop.interface color-scheme) + condition=$(echo "$condition" | tr -d "[:space:]'") + if [ $condition == "prefer-dark" ]; then + return 0 + else + return 1 + fi +} + +bat_alias_wrapper() { + #get color scheme + sys_color_scheme_is_dark + if [[ $? -eq 0 ]]; then + # bat command with dark color scheme + bat --theme=default "$@" + else + # bat command with light color scheme + bat --theme=GitHub "$@" + fi +} +alias cat='bat_alias_wrapper' +``` + + +## Configuration file + +`bat` can also be customized with a configuration file. The location of the file is dependent +on your operating system. To get the default path for your system, call +```bash +bat --config-file +``` + +Alternatively, you can use `BAT_CONFIG_PATH` or `BAT_CONFIG_DIR` environment variables to point `bat` +to a non-default location of the configuration file or the configuration directory respectively: +```bash +export BAT_CONFIG_PATH="/path/to/bat/bat.conf" +export BAT_CONFIG_DIR="/path/to/bat" +``` + +A default configuration file can be created with the `--generate-config-file` option. +```bash +bat --generate-config-file +``` + +There is also now a systemwide configuration file, which is located under `/etc/bat/config` on +Linux and Mac OS and `C:\ProgramData\bat\config` on windows. If the system wide configuration +file is present, the content of the user configuration will simply be appended to it. + +### Format + +The configuration file is a simple list of command line arguments. Use `bat --help` to see a full list of possible options and values. In addition, you can add comments by prepending a line with the `#` character. + +Example configuration file: +```bash +# Set the theme to "TwoDark" +--theme="TwoDark" + +# Show line numbers, Git modifications and file header (but no grid) +--style="numbers,changes,header" + +# Use italic text on the terminal (not supported on all terminals) +--italic-text=always + +# Use C++ syntax for Arduino .ino files +--map-syntax "*.ino:C++" +``` + +## Using `bat` on Windows + +`bat` mostly works out-of-the-box on Windows, but a few features may need extra configuration. + +### Prerequisites + +You will need to install the [Visual C++ Redistributable](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads) package. + +### Paging + +Windows only includes a very limited pager in the form of `more`. You can download a Windows binary +for `less` [from its homepage](http://www.greenwoodsoftware.com/less/download.html) or [through +Chocolatey](https://chocolatey.org/packages/Less). To use it, place the binary in a directory in +your `PATH` or [define an environment variable](#using-a-different-pager). The [Chocolatey package](#on-windows) installs `less` automatically. + +### Colors + +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/). + +**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 don’t have any other pagers installed, you can disable paging entirely by passing `--paging=never` or by setting `BAT_PAGER` to an empty string. + +### Cygwin + +`bat` on Windows does not natively support Cygwin's unix-style paths (`/cygdrive/*`). When passed an absolute cygwin path as an argument, `bat` will encounter the following error: `The system cannot find the path specified. (os error 3)` + +This can be solved by creating a wrapper or adding the following function to your `.bash_profile` file: + +```bash +bat() { + local index + local args=("$@") + for index in $(seq 0 ${#args[@]}) ; do + case "${args[index]}" in + -*) continue;; + *) [ -e "${args[index]}" ] && args[index]="$(cygpath --windows "${args[index]}")";; + esac + done + command bat "${args[@]}" +} +``` + +## Troubleshooting + +### Garbled output + +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 +passing the `--color=never --wrap=never` options to `bat`. + +For untrusted input, the `--sanitize=auto|always|never` option additionally replaces terminal-active +control bytes and Unicode bidi / zero-width formatting characters with the Unicode replacement +character. It implies `--strip-ansi` at the same value. + +> [!NOTE] +> The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text. + +### Terminals & colors + +`bat` handles terminals *with* and *without* truecolor support. However, the colors in most syntax +highlighting themes are not optimized for 8-bit colors. It is therefore strongly recommended +that you use a terminal with 24-bit truecolor support (`terminator`, `konsole`, `iTerm2`, ...), +or use one of the basic [8-bit themes](#8-bit-themes) designed for a restricted set of colors. +See [this article](https://gist.github.com/XVilka/8346728) for more details and a full list of +terminals with truecolor support. + +Make sure that your truecolor terminal sets the `COLORTERM` variable to either `truecolor` or +`24bit`. Otherwise, `bat` will not be able to determine whether or not 24-bit escape sequences +are supported (and fall back to 8-bit colors). + +### Line numbers and grid are hardly visible + +Please try a different theme (see `bat --list-themes` for a list). The `OneHalfDark` and +`OneHalfLight` themes provide grid and line colors that are brighter. + +### File encodings + +`bat` natively supports UTF-8 as well as UTF-16. For every other file encoding, you may need to +convert to UTF-8 first because the encodings can typically not be auto-detected. You can `iconv` +to do so. +Example: if you have a PHP file in Latin-1 (ISO-8859-1) encoding, you can call: +``` bash +iconv -f ISO-8859-1 -t UTF-8 my-file.php | bat +``` +Note: you might have to use the `-l`/`--language` option if the syntax can not be auto-detected +by `bat`. + +## Development + +```bash +# 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 + +# Build a bat binary with modified syntaxes and themes +bash assets/create.sh +cargo install --path . --locked --force +``` + +If you want to build an application that uses `bat`'s pretty-printing +features as a library, check out the [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 + +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 + +See [`SECURITY.md`](SECURITY.md). + +## Project goals and alternatives + +`bat` tries to achieve the following goals: + +- 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 + +There are a lot of alternatives, if you are looking for similar programs. See +[this document](doc/alternatives.md) for a comparison. + +## License +Copyright (c) 2018-2025 [bat-developers](https://github.com/sharkdp/bat). + +`bat` is made available under the terms of either the MIT License or the Apache License 2.0, at your option. + +See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details. diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..8054be4 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,268 @@ +use std::fmt; +use std::io; +#[cfg(feature = "paging")] +use std::process::Child; +#[cfg(feature = "paging")] +use std::thread::{spawn, JoinHandle}; + +use crate::error::*; +#[cfg(feature = "paging")] +use crate::less::{retrieve_less_version, LessVersion}; +#[cfg(feature = "paging")] +use crate::paging::PagingMode; +#[cfg(feature = "paging")] +use crate::wrapping::WrappingMode; + +#[cfg(feature = "paging")] +pub struct BuiltinPager { + pager: minus::Pager, + handle: Option>>, +} + +#[cfg(feature = "paging")] +impl BuiltinPager { + fn new() -> Self { + let pager = minus::Pager::new(); + + let mut input_register = minus::input::HashedEventRegister::default(); + input_register.add_key_events(&["home"], |_, _| { + minus::input::InputEvent::UpdateUpperMark(0) + }); + input_register.add_key_events(&["end"], |_, _| { + minus::input::InputEvent::UpdateUpperMark(usize::MAX) + }); + pager + .set_input_classifier(Box::new(input_register)) + .expect("failed to set input classifier on newly created pager"); + + let handle = { + let pager = pager.clone(); + Some(spawn(move || { + minus::dynamic_paging(pager).map_err(Error::from) + })) + }; + Self { pager, handle } + } +} + +#[cfg(feature = "paging")] +impl std::fmt::Debug for BuiltinPager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BuiltinPager") + //.field("pager", &self.pager) /// minus::Pager doesn't implement fmt::Debug + .field("handle", &self.handle) + .finish() + } +} + +#[cfg(feature = "paging")] +#[derive(Debug, PartialEq)] +enum SingleScreenAction { + Quit, + Nothing, +} + +#[derive(Debug)] +pub enum OutputType { + #[cfg(feature = "paging")] + Pager(Child), + #[cfg(feature = "paging")] + BuiltinPager(BuiltinPager), + Stdout(io::Stdout), +} + +impl OutputType { + #[cfg(feature = "paging")] + pub fn from_mode( + paging_mode: PagingMode, + wrapping_mode: WrappingMode, + pager: Option<&str>, + ) -> Result { + use self::PagingMode::*; + Ok(match paging_mode { + Always => OutputType::try_pager(SingleScreenAction::Nothing, wrapping_mode, pager)?, + QuitIfOneScreen => { + OutputType::try_pager(SingleScreenAction::Quit, wrapping_mode, pager)? + } + _ => OutputType::stdout(), + }) + } + + /// Try to launch the pager. Fall back to stdout in case of errors. + #[cfg(feature = "paging")] + fn try_pager( + single_screen_action: SingleScreenAction, + wrapping_mode: WrappingMode, + pager_from_config: Option<&str>, + ) -> Result { + use crate::pager::{self, PagerKind, PagerSource}; + use std::process::{Command, Stdio}; + + let pager_opt = + pager::get_pager(pager_from_config).map_err(|_| "Could not parse pager command.")?; + + let pager = match pager_opt { + Some(pager) => pager, + None => return Ok(OutputType::stdout()), + }; + + if pager.kind == PagerKind::Bat { + return Err(Error::InvalidPagerValueBat); + } + + if pager.kind == PagerKind::Builtin { + return Ok(OutputType::BuiltinPager(BuiltinPager::new())); + } + + let resolved_path = match grep_cli::resolve_binary(&pager.bin) { + Ok(path) => path, + Err(_) => { + crate::bat_warning!( + "Pager '{}' not found, outputting to stdout instead", + pager.bin + ); + return Ok(OutputType::stdout()); + } + }; + + let mut p = Command::new(resolved_path); + let args = pager.args; + + if pager.kind == PagerKind::Less { + // less needs to be called with the '-R' option in order to properly interpret the + // ANSI color sequences printed by bat. If someone has set PAGER="less -F", we + // therefore need to overwrite the arguments and add '-R'. + // + // We only do this for PAGER (as it is not specific to 'bat'), not for BAT_PAGER + // or bats '--pager' command line option. + let replace_arguments_to_less = pager.source == PagerSource::EnvVarPager; + + if args.is_empty() || replace_arguments_to_less { + p.arg("-R"); // Short version of --RAW-CONTROL-CHARS for maximum compatibility + if single_screen_action == SingleScreenAction::Quit { + p.arg("-F"); // Short version of --quit-if-one-screen for compatibility + } + + if wrapping_mode == WrappingMode::NoWrapping(true) { + p.arg("-S"); // Short version of --chop-long-lines for compatibility + } + + let less_version = retrieve_less_version(&pager.bin); + + // Ensures that 'less' quits together with 'bat' + // The BusyBox version of less does not support -K + if less_version != Some(LessVersion::BusyBox) { + p.arg("-K"); // Short version of '--quit-on-intr' + } + + // Passing '--no-init' fixes a bug with '--quit-if-one-screen' in older + // versions of 'less'. Unfortunately, it also breaks mouse-wheel support. + // + // See: http://www.greenwoodsoftware.com/less/news.530.html + // + // For newer versions (530 or 558 on Windows), we omit '--no-init' as it + // is not needed anymore. + if single_screen_action == SingleScreenAction::Quit { + match less_version { + None => { + p.arg("--no-init"); + } + Some(LessVersion::Less(version)) + if (version < 530 || (cfg!(windows) && version < 558)) => + { + p.arg("--no-init"); + } + _ => {} + } + } + } else { + p.args(args); + } + p.env("LESSCHARSET", "UTF-8"); + + #[cfg(feature = "lessopen")] + // Ensures that 'less' does not preprocess input again if '$LESSOPEN' is set. + p.arg("--no-lessopen"); + } else { + p.args(args); + }; + + Ok(p.stdin(Stdio::piped()) + .spawn() + .map(OutputType::Pager) + .unwrap_or_else(|_| { + crate::bat_warning!( + "Pager '{}' not found, outputting to stdout instead", + &pager.bin + ); + OutputType::stdout() + })) + } + + pub(crate) fn stdout() -> Self { + OutputType::Stdout(io::stdout()) + } + + #[cfg(feature = "paging")] + pub(crate) fn is_pager(&self) -> bool { + matches!(self, OutputType::Pager(_) | OutputType::BuiltinPager(_)) + } + + #[cfg(not(feature = "paging"))] + pub(crate) fn is_pager(&self) -> bool { + false + } + + pub fn handle<'a>(&'a mut self) -> Result> { + Ok(match *self { + #[cfg(feature = "paging")] + OutputType::Pager(ref mut command) => OutputHandle::IoWrite( + command + .stdin + .as_mut() + .ok_or("Could not open stdin for pager")?, + ), + #[cfg(feature = "paging")] + OutputType::BuiltinPager(ref mut pager) => OutputHandle::FmtWrite(&mut pager.pager), + OutputType::Stdout(ref mut handle) => OutputHandle::IoWrite(handle), + }) + } +} + +#[cfg(feature = "paging")] +impl Drop for OutputType { + fn drop(&mut self) { + match *self { + OutputType::Pager(ref mut command) => { + let _ = command.wait(); + } + OutputType::BuiltinPager(ref mut pager) => { + if let Some(handle) = pager.handle.take() { + let _ = handle.join(); + } + } + OutputType::Stdout(_) => (), + } + } +} + +pub enum OutputHandle<'a> { + IoWrite(&'a mut dyn io::Write), + FmtWrite(&'a mut dyn fmt::Write), +} + +impl OutputHandle<'_> { + pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> Result<()> { + match self { + Self::IoWrite(handle) => handle.write_fmt(args).map_err(Into::into), + Self::FmtWrite(handle) => handle.write_fmt(args).map_err(Into::into), + } + } + + pub fn flush(&mut self) -> Result<()> { + match self { + Self::IoWrite(handle) => handle.flush().map_err(Into::into), + Self::FmtWrite(_) => Ok(()), + } + } +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..9463be3 --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,4830 @@ +use predicates::boolean::PredicateBooleanExt; +use predicates::{prelude::predicate, str::PredicateStrExt}; +use serial_test::serial; +use std::path::Path; +use std::str::from_utf8; +use tempfile::tempdir; + +#[cfg(unix)] +mod unix { + pub use std::fs::File; + pub use std::io::{self, Write}; + pub use std::path::PathBuf; + pub use std::process::Stdio; + pub use std::thread; + pub use std::time::Duration; + + pub use assert_cmd::assert::OutputAssertExt; + pub use nix::pty::{openpty, OpenptyResult}; + pub use wait_timeout::ChildExt; + + pub const SAFE_CHILD_PROCESS_CREATION_TIME: Duration = Duration::from_millis(100); + pub const CHILD_WAIT_TIMEOUT: Duration = Duration::from_secs(15); +} +#[cfg(unix)] +use unix::*; + +mod utils; +use utils::command::{bat, bat_raw_command, bat_raw_command_with_config, bat_with_config}; + +use utils::mocked_pagers; + +const EXAMPLES_DIR: &str = "tests/examples"; + +fn get_config() -> &'static str { + if cfg!(windows) { + "bat-windows.conf" + } else { + "bat.conf" + } +} + +#[test] +fn basic() { + bat() + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n") + .stderr(""); +} + +#[test] +fn stdin() { + bat() + .write_stdin("foo\nbar\n") + .assert() + .success() + .stdout("foo\nbar\n"); +} + +/// Runs `bat` with `args` while its stdout is a pipe whose read end has +/// already been closed, so the very first write fails with `BrokenPipe` +/// (EPIPE on Unix, `ERROR_NO_DATA` on Windows). +fn assert_quiet_exit_when_stdout_is_closed(command: &mut std::process::Command, args: &[&str]) { + use std::process::Stdio; + + let (pipe_reader, pipe_writer) = std::io::pipe().expect("could not create a pipe"); + drop(pipe_reader); + + let output = command + .args(args) + .stdin(Stdio::null()) + .stdout(pipe_writer) + .stderr(Stdio::piped()) + .spawn() + .and_then(|child| child.wait_with_output()) + .expect("could not run bat"); + + assert!( + output.status.success(), + "bat {args:?} should exit quietly when stdout is closed, status: {}, stderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr), + ); + assert!( + output.stderr.is_empty(), + "bat {args:?} should not print to stderr when stdout is closed, stderr: {}", + String::from_utf8_lossy(&output.stderr), + ); +} + +#[test] +fn auxiliary_output_does_not_panic_on_closed_stdout() { + // Auxiliary outputs used to be written via `println!`, which panics when + // the reader of a piped stdout is already gone (e.g. `bat --config-file | + // head -n 0`). The main output path has exited quietly on `BrokenPipe` + // since #232; auxiliary outputs should behave the same way. + assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--config-file"]); + assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--diagnostic"]); + + // The `cache` subcommand is only enabled when the working directory does + // not contain a file named `cache` (see `cache_clear`), and it conflicts + // with top-level flags like `--no-config`. Run it from a temporary + // directory that doubles as `BAT_CACHE_PATH`, leaving the real cache alone. + let temp_dir = tempdir().expect("could not create a temporary directory"); + let mut command = bat_raw_command_with_config(); + command + .current_dir(temp_dir.path()) + .env("BAT_CONFIG_PATH", temp_dir.path().join("config")) + .env("BAT_CACHE_PATH", temp_dir.path()); + assert_quiet_exit_when_stdout_is_closed(&mut command, &["cache", "--clear"]); +} + +#[cfg(feature = "application")] +#[test] +fn completion_does_not_panic_on_closed_stdout() { + for shell in ["bash", "fish", "ps1", "zsh"] { + assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--completion", shell]); + } +} + +#[test] +fn concatenate() { + bat() + .arg("test.txt") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\nhello world\n"); +} + +#[test] +fn concatenate_stdin() { + bat() + .arg("test.txt") + .arg("-") + .arg("test.txt") + .write_stdin("stdin\n") + .assert() + .success() + .stdout("hello world\nstdin\nhello world\n"); +} + +#[test] +fn concatenate_empty_first() { + bat() + .arg("empty.txt") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[test] +fn concatenate_empty_last() { + bat() + .arg("test.txt") + .arg("empty.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[test] +fn concatenate_empty_both() { + bat() + .arg("empty.txt") + .arg("empty.txt") + .assert() + .success() + .stdout(""); +} + +#[test] +fn concatenate_empty_between() { + bat() + .arg("test.txt") + .arg("empty.txt") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\nhello world\n"); +} + +#[test] +fn concatenate_empty_first_and_last() { + bat() + .arg("empty.txt") + .arg("test.txt") + .arg("empty.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[test] +fn concatenate_single_line() { + bat() + .arg("single-line.txt") + .arg("single-line.txt") + .assert() + .success() + .stdout("Single LineSingle Line"); +} + +#[test] +fn concatenate_single_line_empty() { + bat() + .arg("single-line.txt") + .arg("empty.txt") + .arg("single-line.txt") + .assert() + .success() + .stdout("Single LineSingle Line"); +} + +#[test] +fn line_numbers() { + bat() + .arg("multiline.txt") + .arg("--style=numbers") + .arg("--decorations=always") + .assert() + .success() + .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); +} + +// Test that -n on command line shows line numbers even when piping (similar to `cat -n`) +#[test] +fn line_numbers_from_cli_in_loop_through_mode() { + bat() + .arg("multiline.txt") + .arg("-n") + .assert() + .success() + .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); +} + +#[test] +fn style_from_env_var_ignored_and_line_numbers_from_cli_in_loop_through_mode() { + bat() + .env("BAT_STYLE", "full") + .arg("multiline.txt") + .arg("-n") + .arg("--decorations=auto") + .assert() + .success() + .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); +} + +#[test] +fn numbers_ignored_from_cli_when_followed_by_plain_in_loop_through_mode() { + bat() + .arg("multiline.txt") + .arg("-np") + .arg("--decorations=auto") + .assert() + .success() + .stdout( + "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n", + ); +} + +#[test] +fn numbers_honored_from_cli_when_preceeded_by_plain_in_loop_through_mode() { + bat() + .arg("multiline.txt") + .arg("-pn") + .arg("--decorations=auto") + .assert() + .success() + .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); +} + +#[test] +fn repeated_combined_flags_use_last_number_or_plain_flag() { + bat() + .arg("multiline.txt") + .arg("-npn") + .arg("--decorations=auto") + .assert() + .success() + .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); + + bat() + .arg("multiline.txt") + .arg("-pnp") + .arg("--decorations=auto") + .assert() + .success() + .stdout( + "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n", + ); + + bat() + .arg("empty_lines.txt") + .arg("-pbp") + .arg("--decorations=auto") + .assert() + .success() + .stdout("line 1\n\n\n\nline 5\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n"); + + bat() + .arg("empty_lines.txt") + .arg("-bpb") + .arg("--decorations=auto") + .assert() + .success() + .stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n"); +} + +#[test] +fn option_terminator_keeps_filename_from_number_flags() { + let tmp_dir = tempdir().expect("can create temporary directory"); + std::fs::write(tmp_dir.path().join("-b"), "hello\nworld\n").expect("can write temporary file"); + + bat() + .current_dir(tmp_dir.path()) + .args(["--", "-b"]) + .assert() + .success() + .stdout("hello\nworld\n"); +} + +#[test] +fn attached_short_option_values_are_not_treated_as_flags() { + let tmp_dir = tempdir().expect("can create temporary directory"); + std::fs::write(tmp_dir.path().join("input.txt"), "hello\nworld\n") + .expect("can write temporary file"); + + bat() + .current_dir(tmp_dir.path()) + .args(["-lbn", "input.txt"]) + .assert() + .success() + .stdout("hello\nworld\n"); +} + +#[test] +fn number_nonblank_style() { + bat() + .arg("empty_lines.txt") + .arg("-b") + .arg("--decorations=always") + .assert() + .success() + .stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n"); +} + +#[test] +fn number_nonblank_from_cli_in_loop_through_mode() { + bat() + .arg("empty_lines.txt") + .arg("-b") + .assert() + .success() + .stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n"); +} + +#[test] +fn number_nonblank_takes_precedence_over_number() { + // -bn should behave like -b + bat() + .arg("empty_lines.txt") + .arg("-bn") + .arg("--decorations=always") + .assert() + .success() + .stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n"); + + // -nb should also behave like -b + bat() + .arg("empty_lines.txt") + .arg("-nb") + .arg("--decorations=always") + .assert() + .success() + .stdout(" 1 line 1\n \n \n \n 2 line 5\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 3 line 20\n 4 line 21\n \n \n 5 line 24\n \n 6 line 26\n \n \n \n 7 line 30\n"); +} + +#[test] +fn number_nonblank_ignored_when_followed_by_plain() { + bat() + .arg("empty_lines.txt") + .arg("-bp") + .arg("--decorations=auto") + .assert() + .success() + .stdout("line 1\n\n\n\nline 5\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n"); +} + +#[test] +fn piped_output_with_number_nonblank_flag() { + bat() + .arg("-b") + .write_stdin("hello\n\nworld\n") + .assert() + .success() + .stdout(" 1 hello\n \n 2 world\n"); +} + +#[test] +fn line_range_2_3() { + bat() + .arg("multiline.txt") + .arg("--line-range=2:3") + .assert() + .success() + .stdout("line 2\nline 3\n"); +} + +#[test] +fn line_range_up_to_2_from_back() { + bat() + .arg("multiline.txt") + .arg("--line-range=:-2") + .assert() + .success() + .stdout("line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\n"); +} + +#[test] +fn line_range_up_to_2_from_back_single_line_is_empty() { + bat() + .arg("single-line.txt") + .arg("--line-range=:-2") + .assert() + .success() + .stdout(""); +} + +#[test] +fn line_range_from_back_last_two() { + bat() + .arg("multiline.txt") + .arg("--line-range=-2:") + .assert() + .success() + .stdout("line 9\nline 10\n"); +} + +#[test] +fn line_range_from_back_last_two_single_line_eq_sep() { + bat() + .arg("single-line.txt") + .arg("--line-range=-2:") + .assert() + .success() + .stdout("Single Line"); +} + +#[test] +fn line_range_from_back_last_two_single_line_no_sep() { + bat() + .arg("single-line.txt") + .arg("--line-range") + .arg("-2:") + .assert() + .success() + .stdout("Single Line"); +} + +#[test] +fn line_range_first_two() { + bat() + .arg("multiline.txt") + .arg("--line-range=:2") + .assert() + .success() + .stdout("line 1\nline 2\n"); +} + +#[test] +fn line_range_last_3() { + bat() + .arg("multiline.txt") + .arg("--line-range=8:") + .assert() + .success() + .stdout("line 8\nline 9\nline 10\n"); +} + +#[test] +fn line_range_multiple() { + bat() + .arg("multiline.txt") + .arg("--line-range=1:2") + .arg("--line-range=4:4") + .assert() + .success() + .stdout("line 1\nline 2\nline 4\n"); +} + +#[test] +fn snip_at_terminal_width_one_does_not_panic() { + bat() + .arg("multiline.txt") + .arg("--style=snip") + .arg("--color=always") + .arg("--terminal-width=1") + .arg("--line-range=1:2") + .arg("--line-range=4:4") + .assert() + .success(); +} + +#[test] +fn line_range_multiple_with_context() { + bat() + .arg("multiline.txt") + .arg("--line-range=2::1") + .arg("--line-range=8::1") + .assert() + .success() + .stdout("line 1\nline 2\nline 3\nline 7\nline 8\nline 9\n"); +} + +#[test] +fn line_range_context_around_single_line() { + bat() + .arg("multiline.txt") + .arg("--line-range=5::2") + .assert() + .success() + .stdout("line 3\nline 4\nline 5\nline 6\nline 7\n"); +} + +#[test] +fn line_range_context_around_single_line_minimal() { + bat() + .arg("multiline.txt") + .arg("--line-range=5::1") + .assert() + .success() + .stdout("line 4\nline 5\nline 6\n"); +} + +#[test] +fn line_range_context_around_range() { + bat() + .arg("multiline.txt") + .arg("--line-range=4:6:2") + .assert() + .success() + .stdout("line 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\n"); +} + +#[test] +fn line_range_context_at_file_boundaries() { + bat() + .arg("multiline.txt") + .arg("--line-range=1::2") + .assert() + .success() + .stdout("line 1\nline 2\nline 3\n"); +} + +#[test] +fn line_range_context_at_end_of_file() { + bat() + .arg("multiline.txt") + .arg("--line-range=10::2") + .assert() + .success() + .stdout("line 8\nline 9\nline 10\n"); +} + +#[test] +fn line_range_context_zero() { + bat() + .arg("multiline.txt") + .arg("--line-range=5::0") + .assert() + .success() + .stdout("line 5\n"); +} + +#[test] +fn line_range_context_negative_single_line() { + bat() + .arg("multiline.txt") + .arg("--line-range=5::-1") + .assert() + .failure() + .stderr(predicate::str::contains( + "Invalid context number in N::C format", + )); +} + +#[test] +fn line_range_context_negative_range() { + bat() + .arg("multiline.txt") + .arg("--line-range=5:6:-1") + .assert() + .failure() + .stderr(predicate::str::contains( + "Invalid context number in N:M:C format", + )); +} + +#[test] +fn line_range_context_non_numeric_single_line() { + bat() + .arg("multiline.txt") + .arg("--line-range=10::abc") + .assert() + .failure() + .stderr(predicate::str::contains( + "Invalid context number in N::C format", + )); +} + +#[test] +fn line_range_context_non_numeric_range() { + bat() + .arg("multiline.txt") + .arg("--line-range=10:12:xyz") + .assert() + .failure() + .stderr(predicate::str::contains( + "Invalid context number in N:M:C format", + )); +} + +#[test] +fn line_range_context_very_large() { + bat() + .arg("multiline.txt") + .arg("--line-range=10::999999") + .assert() + .success() + .stdout( + "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n", + ); +} + +#[test] +fn piped_output_with_implicit_auto_style() { + bat() + .write_stdin("hello\nworld\n") + .assert() + .success() + .stdout("hello\nworld\n"); +} + +#[test] +fn piped_output_with_line_number_flag() { + bat() + .arg("--number") + .write_stdin("hello\nworld\n") + .assert() + .success() + .stdout(" 1 hello\n 2 world\n"); +} + +#[test] +fn piped_output_with_line_numbers_style_flag() { + bat() + .arg("--style=numbers") + .write_stdin("hello\nworld\n") + .assert() + .success() + .stdout("hello\nworld\n"); +} + +#[test] +fn piped_output_with_line_numbers_with_header_grid_style_flag() { + // style ignored because non-interactive + bat() + .arg("--style=header,grid,numbers") + .write_stdin("hello\nworld\n") + .assert() + .success() + .stdout("hello\nworld\n"); +} + +#[test] +fn piped_output_with_auto_style() { + bat() + .arg("--style=auto") + .write_stdin("hello\nworld\n") + .assert() + .success() + .stdout("hello\nworld\n"); // Should be plain when piped +} + +#[test] +#[cfg(not(target_os = "windows"))] +fn piped_output_with_default_style_flag() { + bat() + .arg("--style=default") + .arg("--decorations=always") + .write_stdin("hello\nworld\n") + .assert() + .success() + .stdout( + "─────┬────────────────────────────────────────────────────────────────────────── + │ STDIN +─────┼────────────────────────────────────────────────────────────────────────── + 1 │ hello + 2 │ world +─────┴────────────────────────────────────────────────────────────────────────── +", + ); +} + +#[test] +fn repeated_boolean_flag_is_accepted() { + // A flag in the config file is prepended to the command line, so a user + // who has `--show-all` in their config and also types `-A` ends up passing + // it twice. That must not be an error. + bat() + .arg("empty_lines.txt") + .arg("--show-all") + .arg("--show-all") + .assert() + .success(); +} + +#[test] +fn boolean_flag_in_config_and_on_command_line() { + bat_with_config() + .env("BAT_CONFIG_PATH", "bat-show-all.conf") + .arg("empty_lines.txt") + .arg("--show-all") + .arg("--paging=never") + .assert() + .success(); +} + +#[test] +fn squeeze_blank() { + bat() + .arg("empty_lines.txt") + .arg("--squeeze-blank") + .assert() + .success() + .stdout("line 1\n\nline 5\n\nline 20\nline 21\n\nline 24\n\nline 26\n\nline 30\n"); +} + +#[test] +fn squeeze_blank_line_numbers() { + bat() + .arg("empty_lines.txt") + .arg("--squeeze-blank") + .arg("--decorations=always") + .arg("--number") + .assert() + .success() + .stdout(" 1 line 1\n 2 \n 5 line 5\n 6 \n 20 line 20\n 21 line 21\n 22 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 30 line 30\n"); +} + +#[test] +fn squeeze_limit() { + bat() + .arg("empty_lines.txt") + .arg("--squeeze-blank") + .arg("--squeeze-limit=2") + .assert() + .success() + .stdout("line 1\n\n\nline 5\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\nline 30\n"); + + bat() + .arg("empty_lines.txt") + .arg("--squeeze-blank") + .arg("--squeeze-limit=5") + .assert() + .success() + .stdout("line 1\n\n\n\nline 5\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n"); +} + +#[test] +fn squeeze_limit_line_numbers() { + bat() + .arg("empty_lines.txt") + .arg("--squeeze-blank") + .arg("--squeeze-limit=2") + .arg("--decorations=always") + .arg("--number") + .assert() + .success() + .stdout(" 1 line 1\n 2 \n 3 \n 5 line 5\n 6 \n 7 \n 20 line 20\n 21 line 21\n 22 \n 23 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 28 \n 30 line 30\n"); + + bat() + .arg("empty_lines.txt") + .arg("--squeeze-blank") + .arg("--squeeze-limit=5") + .arg("--decorations=always") + .arg("--number") + .assert() + .success() + .stdout(" 1 line 1\n 2 \n 3 \n 4 \n 5 line 5\n 6 \n 7 \n 8 \n 9 \n 10 \n 20 line 20\n 21 line 21\n 22 \n 23 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 28 \n 29 \n 30 line 30\n"); +} + +#[test] +fn list_themes_with_colors() { + let default_theme_chunk = "Monokai Extended\x1B[0m (default)"; + let default_light_theme_chunk = "Monokai Extended Light\x1B[0m (default light)"; + + bat() + .arg("--color=always") + .arg("--list-themes") + .assert() + .success() + .stdout(predicate::str::contains("DarkNeon").normalize()) + .stdout(predicate::str::contains(default_theme_chunk).normalize()) + .stdout(predicate::str::contains(default_light_theme_chunk).normalize()) + .stdout(predicate::str::contains("Output the square of a number.").normalize()); +} + +#[test] +fn list_themes_without_colors() { + let default_theme_chunk = "Monokai Extended (default)"; + let default_light_theme_chunk = "Monokai Extended Light (default light)"; + + bat() + .arg("--color=never") + .arg("--decorations=always") // trick bat into setting `Config::loop_through` to false + .arg("--list-themes") + .assert() + .success() + .stdout(predicate::str::contains("DarkNeon").normalize()) + .stdout(predicate::str::contains(default_theme_chunk).normalize()) + .stdout(predicate::str::contains(default_light_theme_chunk).normalize()); +} + +#[test] +fn list_themes_to_piped_output() { + bat().arg("--list-themes").assert().success().stdout( + predicate::str::contains("(default)") + .not() + .and(predicate::str::contains("(default light)").not()) + .and(predicate::str::contains("(default dark)").not()), + ); +} + +#[test] +#[serial] +fn list_languages() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("echo pager-output")) + .arg("--list-languages") + .arg("--paging=never") + .assert() + .success() + .stdout(predicate::str::contains("Rust").normalize()) + .stdout(predicate::str::contains("pager-output").not()); + }); +} + +#[test] +#[cfg_attr( + any(not(feature = "git"), feature = "lessopen", target_os = "windows"), + ignore +)] +fn short_help() { + test_help("-h", "../doc/short-help.txt"); +} + +#[test] +#[cfg_attr( + any(not(feature = "git"), feature = "lessopen", target_os = "windows"), + ignore +)] +fn long_help() { + test_help("--help", "../doc/long-help.txt"); +} + +fn test_help(arg: &str, expect_file: &str) { + let assert = bat().arg(arg).assert(); + expect_test::expect_file![expect_file] + .assert_eq(&String::from_utf8_lossy(&assert.get_output().stdout)); +} + +#[test] +fn short_help_with_highlighting() { + bat() + .arg("-h") + .arg("--paging=never") + .arg("--color=always") + .assert() + .success() + .stdout(predicate::str::contains("\x1B[")) + .stdout(predicate::str::contains("Usage:")) + .stdout(predicate::str::contains("Options:")); +} + +#[test] +fn long_help_with_highlighting() { + bat() + .arg("--help") + .arg("--paging=never") + .arg("--color=always") + .assert() + .success() + .stdout(predicate::str::contains("\x1B[")) + .stdout(predicate::str::contains("Usage:")) + .stdout(predicate::str::contains("Options:")); +} + +#[test] +fn help_with_color_never() { + bat() + .arg("--help") + .arg("--color=never") + .arg("--paging=never") + .assert() + .success() + .stdout(predicate::str::contains("\x1B[").not()) + .stdout(predicate::str::contains("Usage:")); +} + +#[cfg(unix)] +fn setup_temp_file(content: &[u8]) -> io::Result<(PathBuf, tempfile::TempDir)> { + let dir = tempfile::tempdir().expect("Couldn't create tempdir"); + let path = dir.path().join("temp_file"); + File::create(&path)?.write_all(content)?; + Ok((path, dir)) +} + +#[cfg(unix)] +#[test] +fn basic_io_cycle() -> io::Result<()> { + let (filename, dir) = setup_temp_file(b"I am not empty")?; + + let file_out = Stdio::from(File::create(&filename)?); + let res = bat_raw_command() + .arg("test.txt") + .arg(&filename) + .stdout(file_out) + .assert(); + drop(dir); + res.failure(); + Ok(()) +} + +#[cfg(unix)] +#[test] +fn first_file_cyclic_is_ok() -> io::Result<()> { + let (filename, dir) = setup_temp_file(b"I am not empty")?; + + let file_out = Stdio::from(File::create(&filename)?); + let res = bat_raw_command() + .arg(&filename) + .arg("test.txt") + .stdout(file_out) + .assert(); + drop(dir); + res.success(); + Ok(()) +} + +#[cfg(unix)] +#[test] +fn empty_file_cycle_is_ok() -> io::Result<()> { + let (filename, dir) = setup_temp_file(b"I am not empty")?; + + let file_out = Stdio::from(File::create(&filename)?); + let res = bat_raw_command() + .arg("empty.txt") + .arg(&filename) + .stdout(file_out) + .assert(); + drop(dir); + res.success(); + Ok(()) +} + +#[cfg(unix)] +#[test] +fn stdin_to_stdout_cycle() -> io::Result<()> { + let (filename, dir) = setup_temp_file(b"I am not empty")?; + let file_in = Stdio::from(File::open(&filename)?); + let file_out = Stdio::from(File::create(&filename)?); + let res = bat_raw_command() + .arg("test.txt") + .arg("-") + .stdin(file_in) + .stdout(file_out) + .assert(); + drop(dir); + res.failure(); + Ok(()) +} + +#[cfg(unix)] +#[test] +fn bat_error_to_stderr() { + bat() + .arg("/tmp") + .assert() + .failure() + .stderr(predicate::str::contains("[bat error]")); +} + +#[cfg(unix)] +#[test] +fn no_args_doesnt_break() { + // To simulate bat getting started from the shell, a process is created with stdin and stdout + // as the slave end of a pseudo terminal. Although both point to the same "file", bat should + // not exit, because in this case it is safe to read and write to the same fd, which is why + // this test exists. + + let OpenptyResult { master, slave } = openpty(None, None).expect("Couldn't open pty."); + let mut master = File::from(master); + let stdin_file = File::from(slave); + let stdout_file = stdin_file.try_clone().unwrap(); + let stdin = Stdio::from(stdin_file); + let stdout = Stdio::from(stdout_file); + + let mut child = bat_raw_command() + .stdin(stdin) + .stdout(stdout) + .env("TERM", "dumb") // Suppresses color detection + .spawn() + .expect("Failed to start."); + + // Some time for the child process to start and to make sure, that we can poll the exit status. + // Although this waiting period is not necessary, it is best to keep it in and be absolutely + // sure, that the try_wait does not error later. + thread::sleep(SAFE_CHILD_PROCESS_CREATION_TIME); + + // The child process should be running and waiting for input, + // therefore no exit status should be available. + let exit_status = child + .try_wait() + .expect("Error polling exit status, this should never happen."); + assert!(exit_status.is_none()); + + // Write Ctrl-D (end of transmission) to the pty. + master + .write_all(&[0x04]) + .expect("Couldn't write EOT character to master end."); + + let exit_status = child + .wait_timeout(CHILD_WAIT_TIMEOUT) + .expect("Error polling exit status, this should never happen.") + .expect("Exit status not set, but the child should have exited already."); + + assert!(exit_status.success()); +} + +#[test] +fn tabs_numbers() { + bat() + .arg("tabs.txt") + .arg("--tabs=4") + .arg("--style=numbers") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 1 2 3 4 + 2 1 ? + 3 22 ? + 4 333 ? + 5 4444 ? + 6 55555 ? + 7 666666 ? + 8 7777777 ? + 9 88888888 ? +", + ); +} + +#[test] +fn tabs_passthrough_wrapped() { + bat() + .arg("tabs.txt") + .arg("--tabs=0") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn tabs_4_wrapped() { + bat() + .arg("tabs.txt") + .arg("--tabs=4") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn tabs_8_wrapped() { + bat() + .arg("tabs.txt") + .arg("--tabs=8") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn tabs_passthrough() { + bat() + .arg("tabs.txt") + .arg("--tabs=0") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn tabs_4() { + bat() + .arg("tabs.txt") + .arg("--tabs=4") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn tabs_8() { + bat() + .arg("tabs.txt") + .arg("--tabs=8") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn tabs_4_env_overrides_config() { + bat_with_config() + .env("BAT_CONFIG_PATH", "bat-tabs.conf") + .env("BAT_TABS", "4") + .arg("tabs.txt") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn tabs_4_arg_overrides_env() { + bat_with_config() + .env("BAT_CONFIG_PATH", "bat-tabs.conf") + .env("BAT_TABS", "6") + .arg("tabs.txt") + .arg("--tabs=4") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn tabs_4_arg_overrides_env_noconfig() { + bat() + .env("BAT_TABS", "6") + .arg("tabs.txt") + .arg("--tabs=4") + .arg("--style=plain") + .arg("--decorations=always") + .assert() + .success() + .stdout( + " 1 2 3 4 +1 ? +22 ? +333 ? +4444 ? +55555 ? +666666 ? +7777777 ? +88888888 ? +", + ); +} + +#[test] +fn terminal_width_env_var_is_respected() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("long.txt"); + std::fs::write( + &tmp_path, + "0123456789abcdef0123456789abcdef0123456789abcdef\n", + ) + .expect("can write temporary file"); + + bat() + .env("BAT_WIDTH", "20") + .arg(&tmp_path) + .arg("--paging=never") + .arg("--color=never") + .arg("--style=numbers") + .arg("--decorations=always") + .arg("--wrap=character") + .assert() + .success() + .stdout(" 1 0123456789abcde\n f0123456789abcd\n ef0123456789abc\n def\n") + .stderr(""); +} + +#[test] +fn terminal_width_arg_overrides_env() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("long.txt"); + std::fs::write( + &tmp_path, + "0123456789abcdef0123456789abcdef0123456789abcdef\n", + ) + .expect("can write temporary file"); + + bat() + .env("BAT_WIDTH", "20") + .arg(&tmp_path) + .arg("--paging=never") + .arg("--color=never") + .arg("--style=numbers") + .arg("--decorations=always") + .arg("--wrap=character") + .arg("--terminal-width=10") + .assert() + .success() + .stdout( + " 1 01234\n 56789\n abcde\n f0123\n 45678\n 9abcd\n ef012\n 34567\n 89abc\n def\n", + ) + .stderr(""); +} + +#[test] +fn fail_non_existing() { + bat().arg("non-existing-file").assert().failure(); +} + +#[test] +fn fail_directory() { + bat().arg("sub_directory").assert().failure(); +} + +#[test] +fn do_not_exit_directory() { + bat() + .arg("sub_directory") + .arg("test.txt") + .assert() + .stdout("hello world\n") + .failure(); +} + +#[test] +#[serial] +fn pager_basic() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("echo pager-output")) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +#[serial] +fn pager_basic_arg() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .arg(format!( + "--pager={}", + mocked_pagers::from("echo pager-output") + )) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +#[serial] +fn pager_overwrite() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("echo other-pager")) + .env("BAT_PAGER", mocked_pagers::from("echo pager-output")) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +fn pager_disable() { + bat() + .env("PAGER", "echo other-pager") + .env("BAT_PAGER", "") + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n").normalize()); +} + +#[test] +#[serial] +fn pager_arg_override_env_withconfig() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat_with_config() + .env("BAT_CONFIG_PATH", get_config()) + .env("PAGER", mocked_pagers::from("echo another-pager")) + .env("BAT_PAGER", mocked_pagers::from("echo other-pager")) + .arg(format!( + "--pager={}", + mocked_pagers::from("echo pager-output") + )) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +#[serial] +fn pager_arg_override_env_noconfig() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("echo another-pager")) + .env("BAT_PAGER", mocked_pagers::from("echo other-pager")) + .arg(format!( + "--pager={}", + mocked_pagers::from("echo pager-output") + )) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +#[serial] +fn pager_env_bat_pager_override_config() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat_with_config() + .env("BAT_CONFIG_PATH", get_config()) + .env("PAGER", mocked_pagers::from("echo other-pager")) + .env("BAT_PAGER", mocked_pagers::from("echo pager-output")) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +#[serial] +fn pager_env_pager_nooverride_config() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat_with_config() + .env("BAT_CONFIG_PATH", get_config()) + .env("PAGER", mocked_pagers::from("echo other-pager")) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("dummy-pager-from-config\n").normalize()); + }); +} + +#[test] +fn env_var_pager_value_bat() { + bat() + .env("PAGER", "bat") + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n").normalize()); +} + +#[test] +fn env_var_bat_pager_value_bat() { + bat() + .env("BAT_PAGER", "bat") + .arg("--paging=always") + .arg("test.txt") + .assert() + .failure() + .stderr(predicate::str::contains("bat as a pager is disallowed")); +} + +#[test] +fn pager_value_bat() { + bat() + .arg("--pager=bat") + .arg("--paging=always") + .arg("test.txt") + .assert() + .failure() + .stderr(predicate::str::contains("bat as a pager is disallowed")); +} + +/// We shall use less instead of most if PAGER is used since PAGER +/// is a generic env var +#[test] +#[serial] // Because of PATH +fn pager_most_from_pager_env_var() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + // If the output is not "I am most" then we know 'most' is not used + bat() + .env("PAGER", mocked_pagers::from("most")) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n").normalize()); + }); +} + +/// If the bat-specific BAT_PAGER is used, obey the wish of the user +/// and allow 'most' +#[test] +#[serial] // Because of PATH +fn pager_most_from_bat_pager_env_var() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("BAT_PAGER", mocked_pagers::from("most")) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("I am most")); + }); +} + +/// Same reasoning with --pager as with BAT_PAGER +#[test] +#[serial] // Because of PATH +fn pager_most_from_pager_arg() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .arg("--paging=always") + .arg(format!("--pager={}", mocked_pagers::from("most"))) + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("I am most")); + }); +} + +/// Make sure the logic for 'most' applies even if an argument is passed +#[test] +#[serial] // Because of PATH +fn pager_most_with_arg() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", format!("{} -w", mocked_pagers::from("most"))) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n").normalize()); + }); +} + +/// Sanity check that 'more' is treated like 'most' +#[test] +#[serial] // Because of PATH +fn pager_more() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("more")) + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n").normalize()); + }); +} + +#[test] +fn alias_pager_disable() { + bat() + .env("PAGER", "echo other-pager") + .arg("-P") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n").normalize()); +} + +#[test] +#[serial] +fn alias_pager_disable_long_overrides_short() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("echo pager-output")) + .arg("-P") + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +fn disable_pager_if_disable_paging_flag_comes_after_paging() { + bat() + .env("PAGER", "echo pager-output") + .arg("--paging=always") + .arg("-P") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n").normalize()); +} + +#[test] +fn disable_pager_if_pp_flag_comes_after_paging() { + bat() + .env("PAGER", "echo pager-output") + .arg("--paging=always") + .arg("-pp") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n").normalize()); +} + +#[test] +#[serial] +fn enable_pager_if_disable_paging_flag_comes_before_paging() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("echo pager-output")) + .arg("-P") + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +#[serial] +fn enable_pager_if_pp_flag_comes_before_paging() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("echo pager-output")) + .arg("-pp") + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +fn paging_does_not_override_simple_plain() { + bat() + .env("PAGER", "echo pager-output") + .arg("--decorations=always") + .arg("--plain") + .arg("--paging=never") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::eq("hello world\n")); +} + +#[test] +#[serial] +fn simple_plain_does_not_override_paging() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("PAGER", mocked_pagers::from("echo pager-output")) + .arg("--paging=always") + .arg("--plain") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +fn pager_failed_to_parse() { + bat() + .env("BAT_PAGER", "mismatched-quotes 'a") + .arg("--paging=always") + .arg("test.txt") + .assert() + .failure() + .stderr(predicate::str::contains("Could not parse pager command")); +} + +#[test] +#[serial] +fn pager_missing_warning() { + bat() + .env("BAT_PAGER", "nonexistent-pager-xyz-missing") + .arg("--paging=always") + .arg("test.txt") + .assert() + .success() + .stderr(predicate::str::contains("[bat warning]")) + .stderr(predicate::str::contains("not found")) + .stderr(predicate::str::contains("nonexistent-pager-xyz-missing")) + .stdout(predicate::str::contains("hello world\n")); +} + +#[test] +#[serial] +fn env_var_bat_paging() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .env("BAT_PAGER", mocked_pagers::from("echo pager-output")) + .env("BAT_PAGING", "always") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("pager-output\n").normalize()); + }); +} + +#[test] +fn basic_set_terminal_title() { + bat() + .env("BAT_PAGER", "cat") + .arg("--paging=always") + .arg("--set-terminal-title") + .arg("test.txt") + .assert() + .success() + .stdout("\u{1b}]0;bat: test.txt\x07hello world\n") + .stderr(""); +} + +#[test] +fn diagnostic_sanity_check() { + bat() + .arg("--diagnostic") + .assert() + .success() + .stdout(predicate::str::contains("BAT_PAGER=")) + .stdout(predicate::str::contains("BAT_WIDTH=")) + .stderr(""); +} + +#[test] +fn help_works_with_invalid_config() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_config_path = tmp_dir.path().join("invalid-config.conf"); + + // Write an invalid config file + std::fs::write(&tmp_config_path, "--invalid-option").expect("can write config file"); + + // --help should work despite invalid config + bat_with_config() + .env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap()) + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains( + "A cat(1) clone with syntax highlighting", + )); + + // -h should also work + bat_with_config() + .env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap()) + .arg("-h") + .assert() + .success() + .stdout(predicate::str::contains("A cat(1) clone with wings")); +} + +#[test] +fn help_uses_valid_config() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_config_path = tmp_dir.path().join("valid-config.conf"); + + // Write a valid config file with an invalid theme (so we can see a warning) + std::fs::write(&tmp_config_path, "--theme=NonExistentThemeName123") + .expect("can write config file"); + + // --help should read the config file and try to use the theme + // (we'll see a warning about unknown theme. This is the easiest way to prove the theme is read from config.) + bat_with_config() + .env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap()) + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains( + "A cat(1) clone with syntax highlighting", + )) + .stderr(predicate::str::contains("Unknown theme")); +} + +#[test] +fn version_works_with_invalid_config() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_config_path = tmp_dir.path().join("invalid-config.conf"); + + // Write an invalid config file + std::fs::write(&tmp_config_path, "--invalid-option").expect("can write config file"); + + // --version should work despite invalid config + bat_with_config() + .env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap()) + .arg("--version") + .assert() + .success() + .stdout(predicate::str::contains("bat ")); + + // -V should also work + bat_with_config() + .env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap()) + .arg("-V") + .assert() + .success() + .stdout(predicate::str::contains("bat ")); +} + +#[test] +fn diagnostic_works_with_invalid_config() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_config_path = tmp_dir.path().join("invalid-config.conf"); + + // Write an invalid config file + std::fs::write(&tmp_config_path, "--invalid-option").expect("can write config file"); + + // --diagnostic should work despite invalid config + bat_with_config() + .env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap()) + .arg("--diagnostic") + .assert() + .success() + .stdout(predicate::str::contains("#### Software version")); + + // --diagnostics (alias) should also work + bat_with_config() + .env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap()) + .arg("--diagnostics") + .assert() + .success() + .stdout(predicate::str::contains("#### Software version")); +} + +#[test] +fn config_location_test() { + bat_with_config() + .env("BAT_CONFIG_PATH", "bat.conf") + .arg("--config-file") + .assert() + .success() + .stdout("bat.conf\n"); + + bat_with_config() + .env("BAT_CONFIG_PATH", "not-existing.conf") + .arg("--config-file") + .assert() + .success() + .stdout("not-existing.conf\n"); +} + +#[test] +fn config_location_when_generating() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_config_path = tmp_dir.path().join("should-be-created.conf"); + + // Create the file with bat + bat_with_config() + .env("BAT_CONFIG_PATH", tmp_config_path.to_str().unwrap()) + .arg("--generate-config-file") + .assert() + .success() + .stdout( + predicate::str::is_match("Success! Config file written to .*should-be-created.conf\n") + .unwrap(), + ); + + // Now we expect the file to exist. If it exists, we assume contents are correct + assert!(tmp_config_path.exists()); +} + +#[test] +fn config_location_from_bat_config_dir_variable() { + bat_with_config() + .env("BAT_CONFIG_DIR", "conf/") + .arg("--config-file") + .assert() + .success() + .stdout(predicate::str::is_match("conf/config\n").unwrap()); +} + +#[test] +#[serial] +fn config_read_arguments_from_file() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat_with_config() + .env("BAT_CONFIG_PATH", get_config()) + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::contains("dummy-pager-from-config\n").normalize()); + }); +} + +#[cfg(unix)] +#[test] +fn cache_clear() { + let src_dir = "cache_source"; + let tmp_dir = tempdir().expect("can create temporary directory"); + let themes_filename = "themes.bin"; + let syntaxes_filename = "syntaxes.bin"; + let metadata_filename = "metadata.yaml"; + [themes_filename, syntaxes_filename, metadata_filename] + .iter() + .map(|filename| { + let fp = tmp_dir.path().join(filename); + let mut file = File::create(fp).expect("can create temporary file"); + writeln!(file, "dummy content").expect("can write to file"); + }) + .count(); + + // Clear the targeted cache + // Include the BAT_CONFIG_PATH and BAT_THEME environment variables to ensure that + // options loaded from a config or the environment are not inserted + // before the cache subcommand, which would break it. + bat_with_config() + .current_dir(Path::new(EXAMPLES_DIR).join(src_dir)) + .env("BAT_CONFIG_PATH", "bat.conf") + .env("BAT_THEME", "1337") + .arg("cache") + .arg("--clear") + .arg("--source") + .arg(".") + .arg("--target") + .arg(tmp_dir.path().to_str().unwrap()) + .assert() + .success() + .stdout( + predicate::str::is_match( + "Clearing theme set cache ... okay +Clearing syntax set cache ... okay +Clearing metadata file ... okay", + ) + .unwrap(), + ); + + // We expect these files to be removed + assert!(!tmp_dir.path().join(themes_filename).exists()); + assert!(!tmp_dir.path().join(syntaxes_filename).exists()); + assert!(!tmp_dir.path().join(metadata_filename).exists()); +} + +#[cfg(unix)] +#[test] +fn cache_build_blank() { + let src_dir = "cache_source"; + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_themes_path = tmp_dir.path().join("themes.bin"); + let tmp_syntaxes_path = tmp_dir.path().join("syntaxes.bin"); + let tmp_acknowledgements_path = tmp_dir.path().join("acknowledgements.bin"); + let tmp_metadata_path = tmp_dir.path().join("metadata.yaml"); + + // Build the cache + // Include the BAT_CONFIG_PATH and BAT_THEME environment variables to ensure that + // options loaded from a config or the environment are not inserted + // before the cache subcommand, which would break it. + bat_with_config() + .current_dir(Path::new(EXAMPLES_DIR).join(src_dir)) + .env("BAT_CONFIG_PATH", "bat.conf") + .env("BAT_THEME", "1337") + .arg("cache") + .arg("--build") + .arg("--blank") + .arg("--source") + .arg(".") + .arg("--target") + .arg(tmp_dir.path().to_str().unwrap()) + .arg("--acknowledgements") + .assert() + .success() + .stdout( + predicate::str::is_match( + "Writing theme set to .*/themes.bin ... okay +Writing syntax set to .*/syntaxes.bin ... okay +Writing acknowledgements to .*/acknowledgements.bin ... okay +Writing metadata to folder .* ... okay", + ) + .unwrap(), + ); + + // Now we expect the files to exist. If they exist, we assume contents are correct + assert!(tmp_themes_path.exists()); + assert!(tmp_syntaxes_path.exists()); + assert!(tmp_acknowledgements_path.exists()); + assert!(tmp_metadata_path.exists()); +} + +#[cfg(unix)] +#[test] +fn cache_build() { + let src_dir = "cache_source"; + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_themes_path = tmp_dir.path().join("themes.bin"); + let tmp_syntaxes_path = tmp_dir.path().join("syntaxes.bin"); + let tmp_acknowledgements_path = tmp_dir.path().join("acknowledgements.bin"); + let tmp_metadata_path = tmp_dir.path().join("metadata.yaml"); + + // Build the cache + // Include the BAT_CONFIG_PATH and BAT_THEME environment variables to ensure that + // options loaded from a config or the environment are not inserted + // before the cache subcommand, which would break it. + bat_with_config() + .current_dir(Path::new(EXAMPLES_DIR).join(src_dir)) + .env("BAT_CONFIG_PATH", "bat.conf") + .env("BAT_THEME", "1337") + .arg("cache") + .arg("--build") + // removed --blank + .arg("--source") + .arg(".") + .arg("--target") + .arg(tmp_dir.path().to_str().unwrap()) + .arg("--acknowledgements") + .assert() + .success() + .stdout( + predicate::str::is_match( + "Writing theme set to .*/themes.bin ... okay\nWriting syntax set to .*/syntaxes.bin ... okay\nWriting acknowledgements to .*/acknowledgements.bin ... okay\nWriting metadata to folder .* ... okay", + ) + .unwrap(), + ) + .stdout(predicate::str::contains("Some referenced contexts could not be found!").not()) + ; + + // Now we expect the files to exist. If they exist, we assume contents are correct + assert!(tmp_themes_path.exists()); + assert!(tmp_syntaxes_path.exists()); + assert!(tmp_acknowledgements_path.exists()); + assert!(tmp_metadata_path.exists()); +} + +#[test] +fn utf16() { + // The output will be converted to UTF-8 with the leading UTF-16 + // BOM removed. This behavior is wanted in interactive mode as + // some terminals seem to display the BOM character as a space, + // and it also breaks syntax highlighting. + bat() + .arg("--plain") + .arg("--decorations=always") + .arg("test_UTF-16LE.txt") + .assert() + .success() + .stdout("hello world\n"); + + bat() + .arg("--plain") + .arg("--decorations=always") + .arg("test_UTF-16BE.txt") + .assert() + .success() + .stdout("hello world\nthis is a test\n"); +} + +#[test] +fn utf16le() { + bat() + .arg("--decorations=always") + .arg("--style=numbers") + .arg("--color=never") + .arg("test_UTF-16LE-complicated.txt") + .assert() + .success() + .stdout(" 1 上一伊刀\n 2 foo bar\n 3 hello world\n"); +} + +#[test] +fn utf16be() { + bat() + .arg("--decorations=always") + .arg("--style=numbers") + .arg("--color=never") + .arg("test_UTF-16BE-complicated.txt") + .assert() + .success() + .stdout(" 1 上一伊刀\n 2 foo bar\n 3 hello world\n"); +} + +// Regression test for https://github.com/sharkdp/bat/issues/1922 +#[test] +fn bom_not_stripped_in_loop_through_mode() { + bat() + .arg("--plain") + .arg("--decorations=never") + .arg("--color=never") + .arg("test_BOM.txt") + .assert() + .success() + .stdout("\u{feff}hello world\n"); +} + +// Regression test for https://github.com/sharkdp/bat/issues/1922 +#[test] +fn bom_stripped_when_colored_output() { + bat() + .arg("--color=always") + .arg("--decorations=never") + .arg("test_BOM.txt") + .assert() + .success() + .stdout( + predicate::str::is_match("\u{1b}\\[38;5;[0-9]{3}mhello world\u{1b}\\[0m\n").unwrap(), + ); +} + +// Regression test for https://github.com/sharkdp/bat/issues/1922 +#[test] +fn bom_stripped_when_no_color_and_not_loop_through() { + bat() + .arg("--color=never") + .arg("--decorations=always") + .arg("--style=numbers,grid,header") + .arg("--terminal-width=80") + .arg("test_BOM.txt") + .assert() + .success() + .stdout( + "\ +─────┬────────────────────────────────────────────────────────────────────────── + │ File: test_BOM.txt +─────┼────────────────────────────────────────────────────────────────────────── + 1 │ hello world +─────┴────────────────────────────────────────────────────────────────────────── +", + ); +} + +// Regression test for https://github.com/sharkdp/bat/issues/2541 +#[test] +fn no_broken_osc_emit_with_line_wrapping() { + bat() + .arg("--color=always") + .arg("--decorations=never") + .arg("--wrap=character") + .arg("--terminal-width=40") + .arg("regression_tests/issue_2541.txt") + .assert() + .success() + .stdout(predicate::function(|s: &str| s.lines().count() == 1)); +} + +#[test] +fn can_print_file_named_cache() { + bat_with_config() + .arg("cache") + .assert() + .success() + .stdout("test\n") + .stderr(""); +} + +#[test] +fn can_print_file_named_cache_with_additional_argument() { + bat_with_config() + .arg("cache") + .arg("test.txt") + .assert() + .success() + .stdout("test\nhello world\n") + .stderr(""); +} + +#[test] +fn can_print_file_starting_with_cache() { + bat_with_config() + .arg("cache.c") + .assert() + .success() + .stdout("test\n") + .stderr(""); +} + +#[test] +fn does_not_print_unwanted_file_named_cache() { + bat_with_config().arg("cach").assert().failure(); +} + +#[test] +fn accepts_no_custom_assets_arg() { + // Just make sure --no-custom-assets is considered a valid arg + // Don't bother to actually verify that it works + bat() + .arg("--no-custom-assets") + .arg("test.txt") + .assert() + .success(); +} + +#[test] +fn unicode_wrap() { + bat_with_config() + .arg("unicode-wrap.txt") + .arg("--style=numbers,snip") + .arg("--decorations=always") + .arg("--terminal-width=40") + .assert() + .success() + .stdout( + " 1 ビタミンA ビタミンD ビタミンE ビ + タミンK ビタミンB1 ビタミンB2 ナ + イアシン パントテン酸 ビタミンB6 + ビタミンB12 葉酸 ビオチン ビタ + ミンC + 2 + 3 고양이 고양이 고양이 고양이 고양이 + 고양이 고양이 고양이 고양이 고양이 + 고양이 고양이 고양이 고양이 고양이 + 고양이 고양이 고양이 고양이 고양이 + 고양이 고양이 고양이 고양이 고양이 + 고양이 고양이 고양이 고양이 고양이 + 고양이 고양이 고양이 고양이 고양이 + 고양이 고양이 고양이 고양이 고양이 + 고양이 + 4 + 5 1 บวก 2 บวก 3 บวก 4 บวก 5 บวก 6 บวก + 7 บวก 8 บวก 9 บวก 10 บวก 11 บวก 12 + บวก 13 บวก 14 บวก 15 บวก 16 บวก 17 + บวก 18 บวก 19 บวก 20 + 6 + 7 Бельгия Болгария Чехия Дания Герман + ия Эстония Ирландия Греция Испания + Франция Хорватия Италия Кипр Латвия + Литва Люксембург Венгрия Мальта Ни + дерланды Австрия Польша Португалия + Румыния Словения Словакия Финляндия + Швеция Великобритания +", + ); +} + +#[test] +fn snip() { + bat() + .arg("multiline.txt") + .arg("--style=numbers,snip") + .arg("--decorations=always") + .arg("--line-range=1:2") + .arg("--line-range=4:") + .arg("--terminal-width=80") + .assert() + .success() + .stdout( + " 1 line 1 + 2 line 2 + ...─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 8< ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + 4 line 4 + 5 line 5 + 6 line 6 + 7 line 7 + 8 line 8 + 9 line 9 + 10 line 10 +", + ); +} + +#[test] +fn empty_file_leads_to_empty_output_with_grid_enabled() { + bat() + .arg("empty.txt") + .arg("--style=grid") + .arg("--decorations=always") + .arg("--terminal-width=80") + .assert() + .success() + .stdout(""); +} + +#[test] +fn empty_file_leads_to_empty_output_with_rule_enabled() { + bat() + .arg("empty.txt") + .arg("--style=rule") + .arg("--decorations=always") + .arg("--terminal-width=80") + .assert() + .success() + .stdout(""); +} + +#[test] +fn header_basic() { + bat() + .arg("test.txt") + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo\n") + .stderr(""); +} + +#[test] +fn header_full_basic() { + bat() + .arg("test.txt") + .arg("--decorations=always") + .arg("--style=header-filename,header-filesize") + .arg("-r=0:0") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo\nSize: 12 B\n") + .stderr(""); +} + +#[test] +fn header_env_basic() { + bat_with_config() + .env("BAT_STYLE", "header-filename,header-filesize") + .arg("test.txt") + .arg("--decorations=always") + .arg("-r=0:0") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo\nSize: 12 B\n") + .stderr(""); +} + +#[test] +fn header_arg_overrides_env() { + bat_with_config() + .env("BAT_STYLE", "header-filesize") + .arg("test.txt") + .arg("--decorations=always") + .arg("--style=header-filename") + .arg("-r=0:0") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo\n") + .stderr(""); +} + +#[test] +fn header_binary() { + bat() + .arg("test.binary") + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo \n") + .stderr(""); +} + +// Regression test for https://github.com/sharkdp/bat/issues/3554 +#[test] +fn header_binary_with_null_after_first_line() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("encrypted.gpg"); + std::fs::write(&tmp_path, b"packet-header\npayload\0bytes\n") + .expect("can write temporary file"); + + bat() + .arg(&tmp_path) + .arg("--decorations=always") + .arg("--style=header") + .arg("--line-range=0:0") + .arg("--file-name=encrypted.gpg") + .assert() + .success() + .stdout("File: encrypted.gpg \n") + .stderr(""); +} + +#[test] +fn header_zip_file_is_binary() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("test.zip"); + std::fs::write(&tmp_path, b"PK\x03\x04hello").expect("can write temporary file"); + + bat() + .arg(&tmp_path) + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("--file-name=test.zip") + .assert() + .success() + .stdout("File: test.zip \n") + .stderr(""); +} + +#[test] +fn header_full_binary() { + bat() + .arg("test.binary") + .arg("--decorations=always") + .arg("--style=header-filename,header-filesize") + .arg("-r=0:0") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo \nSize: 4 B\n") + .stderr(""); +} + +#[test] +#[cfg(not(feature = "git"))] +fn header_narrow_terminal() { + bat() + .arg("--terminal-width=30") + .arg("--decorations=always") + .arg("this-file-path-is-really-long-and-would-have-broken-the-layout-of-the-header.txt") + .assert() + .success() + .stdout( + "\ +─────┬──────────────────────── + │ File: this-file-path-is + │ -really-long-and-would- + │ have-broken-the-layout- + │ of-the-header.txt +─────┼──────────────────────── + 1 │ The header is not broke + │ n +─────┴──────────────────────── +", + ) + .stderr(""); +} + +#[test] +fn header_very_narrow_terminal() { + bat() + .arg("--terminal-width=10") + .arg("--decorations=always") + .arg("this-file-path-is-really-long-and-would-have-broken-the-layout-of-the-header.txt") + .assert() + .success() + .stdout( + "\ +────────── +File: this +-file-path +-is-really +-long-and- +would-have +-broken-th +e-layout-o +f-the-head +er.txt +────────── +The header + is not br +oken +────────── +", + ) + .stderr(""); +} + +#[test] +fn header_narrow_terminal_with_multibyte_chars() { + bat() + .arg("--terminal-width=30") + .arg("--decorations=always") + .arg("test.A—B가") + .assert() + .success() + .stderr(""); +} + +#[test] +#[cfg(feature = "git")] // Expected output assumes git is enabled +fn header_default() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("--style=default") + .arg("single-line.txt") + .assert() + .success() + .stdout( + "\ +─────┬────────────────────────────────────────────────────────────────────────── + │ File: single-line.txt +─────┼────────────────────────────────────────────────────────────────────────── + 1 │ Single Line +─────┴────────────────────────────────────────────────────────────────────────── +", + ) + .stderr(""); +} + +#[test] +#[cfg(feature = "git")] // Expected output assumes git is enabled +fn header_default_is_default() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("single-line.txt") + .assert() + .success() + .stdout( + "\ +─────┬────────────────────────────────────────────────────────────────────────── + │ File: single-line.txt +─────┼────────────────────────────────────────────────────────────────────────── + 1 │ Single Line +─────┴────────────────────────────────────────────────────────────────────────── +", + ) + .stderr(""); +} + +#[test] +#[cfg(feature = "git")] // Expected output assumes git is enabled + // Make sure indent isn't printed if there's no changes +fn header_and_changes_only() { + bat() + .arg("--style=header-filename,changes") + .arg("--decorations=always") + .arg("--color=never") + .arg("single-line.txt") + .assert() + .success() + .stdout("File: single-line.txt\nSingle Line\n") + .stderr(""); +} + +#[test] +fn filename_stdin() { + bat() + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("-") + .write_stdin("stdin\n") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo\n") + .stderr(""); +} + +#[test] +fn filename_stdin_binary() { + let vec = vec![0; 1]; + bat_with_config() + .arg("--decorations=always") + .arg("--style=header") + .write_stdin(vec) + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo \n") + .stderr(""); +} + +#[test] +fn filename_multiple_ok() { + bat() + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("test.txt") + .arg("--file-name=foo") + .arg("single-line.txt") + .arg("--file-name=bar") + .assert() + .success() + .stdout("File: foo\n\nFile: bar\n") + .stderr(""); +} + +#[test] +fn filename_multiple_err() { + bat() + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("test.txt") + .arg("--file-name=foo") + .arg("single-line.txt") + .assert() + .failure(); +} + +#[test] +fn header_padding() { + bat() + .arg("--decorations=always") + .arg("--style=header") + .arg("test.txt") + .arg("single-line.txt") + .assert() + .stdout("File: test.txt\nhello world\n\nFile: single-line.txt\nSingle Line\n") + .stderr(""); +} + +#[test] +fn header_full_padding() { + bat() + .arg("--decorations=always") + .arg("--style=header-filename,header-filesize") + .arg("test.txt") + .arg("single-line.txt") + .assert() + .stdout("File: test.txt\nSize: 12 B\nhello world\n\nFile: single-line.txt\nSize: 11 B\nSingle Line\n") + .stderr(""); +} + +#[test] +fn header_padding_rule() { + bat() + .arg("--decorations=always") + .arg("--style=header,rule") + .arg("--terminal-width=80") + .arg("test.txt") + .arg("single-line.txt") + .assert() + .stdout( + "File: test.txt +hello world +──────────────────────────────────────────────────────────────────────────────── +File: single-line.txt +Single Line +", + ) + .stderr(""); +} + +#[test] +fn header_full_padding_rule() { + bat() + .arg("--decorations=always") + .arg("--style=header-filename,header-filesize,rule") + .arg("--terminal-width=80") + .arg("test.txt") + .arg("single-line.txt") + .assert() + .stdout( + "File: test.txt +Size: 12 B +hello world +──────────────────────────────────────────────────────────────────────────────── +File: single-line.txt +Size: 11 B +Single Line +", + ) + .stderr(""); +} + +#[test] +fn grid_overrides_rule() { + bat() + .arg("--decorations=always") + .arg("--style=grid,rule") + .arg("--terminal-width=80") + .arg("test.txt") + .arg("single-line.txt") + .assert() + .stdout( + "\ +──────────────────────────────────────────────────────────────────────────────── +hello world +──────────────────────────────────────────────────────────────────────────────── +──────────────────────────────────────────────────────────────────────────────── +Single Line +──────────────────────────────────────────────────────────────────────────────── +", + ) + .stderr( + "\x1b[33m[bat warning]\x1b[0m: Style 'rule' is a subset of style 'grid', 'rule' will not be visible.\n", + ); +} + +#[cfg(target_os = "linux")] +#[test] +fn file_with_invalid_utf8_filename() { + use std::ffi::OsStr; + use std::fs::File; + use std::io::Write; + use std::os::unix::ffi::OsStrExt; + + let tmp_dir = tempdir().expect("can create temporary directory"); + let file_path = tmp_dir + .path() + .join(OsStr::from_bytes(b"test-invalid-utf8-\xC3(.rs")); + { + let mut file = File::create(&file_path).expect("can create temporary file"); + writeln!(file, "dummy content").expect("can write to file"); + } + + bat() + .arg(file_path.as_os_str()) + .assert() + .success() + .stdout("dummy content\n"); +} + +#[test] +fn do_not_panic_regression_tests() { + for filename in &[ + "issue_28.md", + "issue_190.md", + "issue_314.hs", + "issue_914.rb", + "issue_915.vue", + ] { + bat() + .arg("--color=always") + .arg(format!("regression_tests/{filename}")) + .assert() + .success(); + } +} + +#[test] +fn do_not_detect_different_syntax_for_stdin_and_files() { + let file = "regression_tests/issue_985.js"; + + let cmd_for_file = bat() + .arg("--color=always") + .arg("--map-syntax=*.js:Markdown") + .arg(format!("--file-name={file}")) + .arg("--style=plain") + .arg(file) + .assert() + .success(); + + let cmd_for_stdin = bat() + .arg("--color=always") + .arg("--map-syntax=*.js:Markdown") + .arg("--style=plain") + .arg(format!("--file-name={file}")) + .pipe_stdin(Path::new(EXAMPLES_DIR).join(file)) + .unwrap() + .assert() + .success(); + + assert_eq!( + from_utf8(&cmd_for_file.get_output().stdout).expect("output is valid utf-8"), + from_utf8(&cmd_for_stdin.get_output().stdout).expect("output is valid utf-8") + ); +} + +#[test] +fn no_first_line_fallback_when_mapping_to_invalid_syntax() { + let file = "regression_tests/first_line_fallback.invalid-syntax"; + + bat() + .arg("--color=always") + .arg("--map-syntax=*.invalid-syntax:InvalidSyntax") + .arg(format!("--file-name={file}")) + .arg("--style=plain") + .arg(file) + .assert() + .failure() + .stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'")); +} + +#[test] +fn stdin_detects_bash_from_first_line() { + let content = "#!/bin/bash\necho hi\n"; + + let detected_output = bat() + .arg("--color=always") + .arg("--style=plain") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let explicit_output = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=bash") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&detected_output).expect("output is valid utf-8"), + from_utf8(&explicit_output).expect("output is valid utf-8") + ); +} + +#[test] +fn stdin_detects_diff_from_first_line() { + let content = "diff --git a/x b/y\n--- a/x\n+++ b/y\n@@ -1 +1 @@\n-old\n+new\n"; + + let detected_output = bat() + .arg("--color=always") + .arg("--style=plain") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let explicit_output = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=diff") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&detected_output).expect("output is valid utf-8"), + from_utf8(&explicit_output).expect("output is valid utf-8") + ); +} + +#[test] +fn empty_stdin_does_not_detect_syntax() { + bat() + .arg("--color=always") + .arg("--style=plain") + .write_stdin("") + .assert() + .success() + .stdout("") + .stderr(""); +} + +#[test] +fn binary_stdin_does_not_detect_syntax_from_invalid_utf8_first_line() { + let content = b"#!/bin/bash\xff\necho hi\n"; + + let detected_output = bat() + .arg("--binary=as-text") + .arg("--color=always") + .arg("--style=plain") + .write_stdin(content.as_slice()) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let plain_text_output = bat() + .arg("--binary=as-text") + .arg("--color=always") + .arg("--style=plain") + .arg("--language=txt") + .write_stdin(content.as_slice()) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&detected_output).expect("output is valid utf-8"), + from_utf8(&plain_text_output).expect("output is valid utf-8") + ); +} + +#[test] +fn explicit_language_overrides_stdin_first_line_detection() { + let content = "diff --git a/x b/y\n--- a/x\n+++ b/y\n@@ -1 +1 @@\n-old\n+new\n"; + + let detected_output = bat() + .arg("--color=always") + .arg("--style=plain") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let explicit_output = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=json") + .arg("-") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_ne!( + from_utf8(&detected_output).expect("output is valid utf-8"), + from_utf8(&explicit_output).expect("output is valid utf-8") + ); +} + +#[test] +fn fallback_syntax_is_used_when_no_syntax_is_detected() { + let content = "# comment\nfoo=bar\n"; + + let fallback_output = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--file-name=unknown.fallbacksyntax") + .arg("--fallback-syntax=bash") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let explicit_output = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=bash") + .arg("--file-name=unknown.fallbacksyntax") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&fallback_output).expect("output is valid utf-8"), + from_utf8(&explicit_output).expect("output is valid utf-8") + ); +} + +#[test] +fn fallback_syntax_does_not_override_detected_syntax() { + let content = "fn main() { println!(\"hello\"); }\n"; + + let with_fallback = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--file-name=test.rs") + .arg("--fallback-syntax=json") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let without_fallback = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--file-name=test.rs") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&with_fallback).expect("output is valid utf-8"), + from_utf8(&without_fallback).expect("output is valid utf-8") + ); +} + +#[test] +fn fallback_syntax_does_not_override_explicit_language() { + let content = "{\"a\": 1}\n"; + + let with_fallback = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=json") + .arg("--fallback-syntax=rust") + .arg("--file-name=unknown.fallbacksyntax") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let without_fallback = bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--language=json") + .arg("--file-name=unknown.fallbacksyntax") + .write_stdin(content) + .assert() + .success() + .get_output() + .stdout + .clone(); + + assert_eq!( + from_utf8(&with_fallback).expect("output is valid utf-8"), + from_utf8(&without_fallback).expect("output is valid utf-8") + ); +} + +#[test] +fn invalid_fallback_syntax_returns_error() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--file-name=unknown.fallbacksyntax") + .arg("--fallback-syntax=InvalidSyntax") + .write_stdin("foo\n") + .assert() + .failure() + .stderr(predicate::str::contains("unknown syntax: 'InvalidSyntax'")); +} + +#[test] +fn show_all_mode() { + bat() + .arg("--show-all") + .arg("nonprintable.txt") + .assert() + .stdout("hello·world␊\n├──┤␍␀␇␈␛") + .stderr(""); +} + +#[test] +fn show_all_extends_tab_markers_to_next_tabstop() { + bat() + .arg("tabs.txt") + .arg("--show-all") + .arg("--tabs=4") + .arg("--style=plain") + .assert() + .success() + .stdout( + "├──┤1├─┤2├─┤3├─┤4␊ +1├─┤?␊ +22├┤?␊ +333↹?␊ +4444├──┤?␊ +55555├─┤?␊ +666666├┤?␊ +7777777↹?␊ +88888888├──┤?␊ +", + ); +} + +#[test] +fn show_all_extends_tab_markers_to_next_tabstop_width_8() { + bat() + .arg("tabs.txt") + .arg("--show-all") + .arg("--tabs=8") + .arg("--style=plain") + .assert() + .success() + .stdout( + "├──────┤1├─────┤2├─────┤3├─────┤4␊ +1├─────┤?␊ +22├────┤?␊ +333├───┤?␊ +4444├──┤?␊ +55555├─┤?␊ +666666├┤?␊ +7777777↹?␊ +88888888├──────┤?␊ +", + ); +} + +#[test] +fn show_all_with_caret_notation() { + bat() + .arg("--show-all") + .arg("--nonprintable-notation=caret") + .arg("nonprintable.txt") + .assert() + .stdout("hello·world^J\n├──┤^M^@^G^H^[") + .stderr(""); + + bat() + .arg("--show-all") + .arg("--nonprintable-notation=caret") + .arg("control_characters.txt") + .assert() + .stdout("^@^A^B^C^D^E^F^G^H├─┤^J\n^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\\^]^^^_^?") + .stderr(""); +} + +#[test] +fn show_all_with_unicode() { + bat() + .arg("--show-all") + .arg("--nonprintable-notation=unicode") + .arg("control_characters.txt") + .assert() + .stdout("␀␁␂␃␄␅␆␇␈├─┤␊\n␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␡") + .stderr(""); +} + +#[test] +fn binary_as_text() { + bat() + .arg("--binary=as-text") + .arg("control_characters.txt") + .assert() + .stdout("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F") + .stderr(""); +} + +#[test] +fn binary_as_text_control_char_width() { + // Control characters are displayed as caret notation (e.g. ^@) by the + // terminal, occupying 2 columns each. With 20 NUL bytes (40 columns) + + // "END" (3 columns) = 43 columns, wrapping at terminal width 40 must + // produce 2 lines, not 1. See #3631. + bat() + .arg("--binary=as-text") + .arg("--wrap=character") + .arg("--terminal-width=40") + .arg("--decorations=always") + .arg("--style=plain") + .arg("--color=never") + .arg("regression_tests/issue_3631.txt") + .assert() + .success() + .stdout(predicate::function(|s: &str| s.lines().count() == 2)); +} + +#[test] +fn no_strip_overstrike_for_plain_text() { + // Overstrike is preserved for plain text files (no syntax highlighting) + bat() + .arg("--color=never") + .arg("--decorations=never") + .arg("overstrike.txt") + .assert() + .success() + .stdout("B\x08Bold t\x08te\x08ex\x08xt\x08t and _\x08u_\x08n_\x08d_\x08e_\x08r_\x08l_\x08i_\x08n_\x08e\n") + .stderr(""); +} + +#[test] +fn strip_overstrike_with_syntax_highlighting() { + // Overstrike is stripped for certain syntax highlighting like command help. + bat() + .arg("--force-colorization") + .arg("--language=help") + .arg("overstrike.txt") + .assert() + .success() + .stdout(predicate::str::contains("Bold text and underline")) + .stderr(""); +} + +#[test] +fn strip_overstrike_for_manpage_syntax() { + // Overstrike is stripped for .man files (Manpage syntax) + bat() + .arg("--force-colorization") + .arg("git-commit.man") + .assert() + .success() + .stdout(predicate::str::contains("NAME")) + .stdout(predicate::str::contains("git-commit - Record changes")) + .stdout(predicate::str::is_match(r"\x1b\[38;[0-9;]+m--interactive\x1b\[").unwrap()) + .stderr(""); +} + +#[test] +fn no_strip_overstrike_for_other_syntax() { + // Overstrike is NOT stripped for other syntaxes (e.g., Rust) + bat() + .arg("--force-colorization") + .arg("--language=rust") + .arg("overstrike.txt") + .assert() + .success() + .stdout(predicate::str::contains("\x08")) + .stderr(""); +} + +#[test] +fn show_all_shows_backspace_with_caret_notation() { + // --show-all should display backspace characters (not strip them) + bat() + .arg("--show-all") + .arg("--nonprintable-notation=caret") + .arg("--decorations=never") + .arg("overstrike.txt") + .assert() + .success() + .stdout(predicate::str::contains("^H")) + .stderr(""); +} + +#[test] +fn no_paging_arg() { + bat() + .arg("--no-paging") + .arg("--color=never") + .arg("--decorations=never") + .arg("single-line.txt") + .assert() + .success() + .stdout("Single Line"); +} + +#[test] +fn no_paging_short_arg() { + bat() + .arg("-P") + .arg("--color=never") + .arg("--decorations=never") + .arg("single-line.txt") + .assert() + .success() + .stdout("Single Line"); +} + +#[test] +fn no_pager_arg() { + bat() + .arg("--no-pager") + .arg("--color=never") + .arg("--decorations=never") + .arg("single-line.txt") + .assert() + .success() + .stdout("Single Line"); +} + +#[test] +fn plain_mode_does_not_add_nonexisting_newline() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--decorations=always") + .arg("--style=plain") + .arg("single-line.txt") + .assert() + .success() + .stdout("Single Line"); +} + +// Regression test for https://github.com/sharkdp/bat/issues/299 +#[test] +#[cfg(feature = "git")] // Expected output assumes git is enabled +fn grid_for_file_without_newline() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("--style=full") + .arg("single-line.txt") + .assert() + .success() + .stdout( + "\ +─────┬────────────────────────────────────────────────────────────────────────── + │ File: single-line.txt + │ Size: 11 B +─────┼────────────────────────────────────────────────────────────────────────── + 1 │ Single Line +─────┴────────────────────────────────────────────────────────────────────────── +", + ) + .stderr(""); +} + +// For ANSI theme, use underscore as a highlighter +#[test] +fn ansi_highlight_underline() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("--theme=ansi") + .arg("--style=plain") + .arg("--highlight-line=1") + .write_stdin("Ansi Underscore Test\nAnother Line") + .assert() + .success() + .stdout("\x1B[4mAnsi Underscore Test\n\x1B[24mAnother Line") + .stderr(""); +} + +// we don't really test other color schemes in the syntax-tests/source vs highlighted stuff +// so here a simple integration test has been made for the ANSI theme, +// which lives directly inside the bat repository +#[test] +fn ansi_highlight_json_keys() { + bat() + .arg("--paging=never") + .arg("--color=always") + .arg("--decorations=never") + .arg("--theme=ansi") + .arg("--language=json") + .write_stdin("{\"foo\": \"bar\", \"test\": [123, \"baz\"] }") + .assert() + .success() + .stdout("{\x1B[34m\"\x1B[0m\x1B[34mfoo\x1B[0m\x1B[34m\"\x1B[0m: \x1B[32m\"\x1B[0m\x1B[32mbar\x1B[0m\x1B[32m\"\x1B[0m, \x1B[34m\"\x1B[0m\x1B[34mtest\x1B[0m\x1B[34m\"\x1B[0m: [\x1B[33m123\x1B[0m, \x1B[32m\"\x1B[0m\x1B[32mbaz\x1B[0m\x1B[32m\"\x1B[0m] }") + .stderr(""); +} + +// Ensure that ANSI passthrough is emitted properly for both wrapping and non-wrapping printer. +// See https://github.com/sharkdp/bat/issues/2307 for what common use case this test tests. +#[test] +fn ansi_passthrough_emit() { + for wrapping in &["never", "character"] { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg(format!("--wrap={wrapping}")) + .arg("--decorations=always") + .arg("--style=plain") + .write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n") + .assert() + .success() + .stdout("\x1B[33m\x1B[33mColor\n\x1B[33mColor \x1B[m\nPlain\n") + .stderr(""); + } +} + +// Ensure that a simple ANSI sequence passthrough is emitted properly on wrapped lines. +// This also helps ensure that escape sequences are counted as part of the visible characters when wrapping. +#[test] +fn ansi_sgr_emitted_when_wrapped() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=20") + .arg("--wrap=character") + .arg("--decorations=always") + .arg("--style=plain") + .write_stdin("\x1B[33mColor...............Also color.\n") + .assert() + .success() + .stdout("\x1B[33m\x1B[33mColor...............\n\x1B[33mAlso color.\n") + // FIXME: ~~~~~~~~ should not be emitted twice. + .stderr(""); +} + +// Ensure that a simple ANSI sequence passthrough is emitted properly on wrapped lines. +// This also helps ensure that escape sequences are counted as part of the visible characters when wrapping. +#[test] +fn ansi_hyperlink_emitted_when_wrapped() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=20") + .arg("--wrap=character") + .arg("--decorations=always") + .arg("--style=plain") + .write_stdin("\x1B]8;;http://example.com/\x1B\\Hyperlinks..........Wrap across lines.\n") + .assert() + .success() + .stdout("\x1B]8;;http://example.com/\x1B\\\x1B]8;;http://example.com/\x1B\\Hyperlinks..........\x1B]8;;\x1B\\\n\x1B]8;;http://example.com/\x1B\\Wrap across lines.\n") + // FIXME: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ should not be emitted twice. + .stderr(""); +} + +// Ensure that multiple ANSI sequence SGR attributes are combined when emitted on wrapped lines. +#[test] +fn ansi_sgr_joins_attributes_when_wrapped() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=20") + .arg("--wrap=character") + .arg("--decorations=always") + .arg("--style=plain") + .write_stdin("\x1B[33mColor. \x1B[1mBold.........Also bold and color.\n") + .assert() + .success() + .stdout("\x1B[33m\x1B[33mColor. \x1B[1m\x1B[33m\x1B[1mBold.........\n\x1B[33m\x1B[1mAlso bold and color.\n") + // FIXME: ~~~~~~~~ ~~~~~~~~~~~~~~~ should not be emitted twice. + .stderr(""); +} + +// Ensure that strikethrough (SGR 9) is tracked like the other attributes, so it is +// re-emitted (and therefore preserved) across a wrap boundary instead of being dropped. +#[test] +fn ansi_sgr_strike_joins_attributes_when_wrapped() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=20") + .arg("--wrap=character") + .arg("--decorations=always") + .arg("--style=plain") + .write_stdin("\x1B[33mColor. \x1B[9mStrike.......Struck+color.\n") + .assert() + .success() + .stdout("\x1B[33m\x1B[33mColor. \x1B[9m\x1B[33m\x1B[9mStrike.......\n\x1B[33m\x1B[9mStruck+color.\n") + // FIXME: ~~~~~~~~ ~~~~~~~~~~~~~~~ should not be emitted twice. + .stderr(""); +} + +#[test] +fn ignored_suffix_arg() { + bat() + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("test.json~") + .assert() + .success() + .stdout("\u{1b}[38;5;231m{\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;208mtest\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;231m:\u{1b}[0m\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;186m\"\u{1b}[0m\u{1b}[38;5;186mvalue\u{1b}[0m\u{1b}[38;5;186m\"\u{1b}[0m\u{1b}[38;5;231m}\u{1b}[0m") + .stderr(""); + + bat() + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("--ignored-suffix=.suffix") + .arg("test.json.suffix") + .assert() + .success() + .stdout("\u{1b}[38;5;231m{\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;208mtest\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;231m:\u{1b}[0m\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;186m\"\u{1b}[0m\u{1b}[38;5;186mvalue\u{1b}[0m\u{1b}[38;5;186m\"\u{1b}[0m\u{1b}[38;5;231m}\u{1b}[0m") + .stderr(""); + + bat() + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("test.json.suffix") + .assert() + .success() + .stdout("\u{1b}[38;5;231m{\"test\": \"value\"}\u{1b}[0m") + .stderr(""); +} + +fn wrapping_test(wrap_flag: &str, expect_wrap: bool) { + let expected = match expect_wrap { + true => + "abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcde\nfghigklmnopqrstuvxyz\n", + false => + "abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n", + }; + + bat() + .arg(wrap_flag) + .arg("--style=rule") + .arg("--color=never") + .arg("--decorations=always") + .arg("--terminal-width=80") + .arg("long-single-line.txt") + .assert() + .success() + .stdout(expected.to_owned()) + .stderr(""); +} + +#[test] +fn no_line_wrapping_when_set_to_never() { + wrapping_test("--wrap=never", false); +} + +#[test] +fn line_wrapping_when_auto() { + wrapping_test("--wrap=auto", true); +} + +#[test] +fn no_line_wrapping_with_s_flag() { + wrapping_test("-S", false); +} + +#[test] +fn no_wrapping_with_chop_long_lines() { + wrapping_test("--chop-long-lines", false); +} + +#[test] +#[serial] +fn wrap_never_flag_respected_with_paging_always() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .arg("--pager=cat") + .arg("--paging=always") + .arg("--wrap=never") + .arg("--color=never") + .arg("--decorations=never") + .arg("--style=plain") + .write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n") + .assert() + .success() + .stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize()) + .stderr(""); + }); +} + +#[test] +#[serial] +fn s_flag_respected_with_paging_always() { + mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| { + bat() + .arg("--pager=cat") + .arg("--paging=always") + .arg("-S") + .arg("--color=never") + .arg("--decorations=never") + .arg("--style=plain") + .write_stdin("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz\n") + .assert() + .success() + .stdout(predicate::str::contains("abcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyzabcdefghigklmnopqrstuvxyz").normalize()) + .stderr(""); + }); +} + +#[test] +fn theme_arg_overrides_env() { + bat() + .env("BAT_THEME", "TwoDark") + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("--theme=ansi") + .arg("--style=plain") + .arg("--highlight-line=1") + .write_stdin("Ansi Underscore Test\nAnother Line") + .assert() + .success() + .stdout("\x1B[4mAnsi Underscore Test\n\x1B[24mAnother Line") + .stderr(""); +} + +#[test] +fn theme_arg_overrides_env_withconfig() { + bat_with_config() + .env("BAT_CONFIG_PATH", "bat-theme.conf") + .env("BAT_THEME", "TwoDark") + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("--theme=ansi") + .arg("--style=plain") + .arg("--highlight-line=1") + .write_stdin("Ansi Underscore Test\nAnother Line") + .assert() + .success() + .stdout("\x1B[4mAnsi Underscore Test\n\x1B[24mAnother Line") + .stderr(""); +} + +#[test] +fn theme_light_env_var_is_respected() { + bat() + .env("BAT_THEME_LIGHT", "Coldark-Cold") + .env("COLORTERM", "truecolor") + .arg("--theme=light") + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("--style=plain") + .arg("--highlight-line=1") + .write_stdin("Lorem Ipsum") + .assert() + .success() + .stdout("\x1B[48;2;208;218;231mLorem Ipsum\x1B[0m") + .stderr(""); +} + +#[test] +fn theme_dark_env_var_is_respected() { + bat() + .env("BAT_THEME_DARK", "Coldark-Dark") + .env("COLORTERM", "truecolor") + .arg("--theme=dark") + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("--style=plain") + .arg("--highlight-line=1") + .write_stdin("Lorem Ipsum") + .assert() + .success() + .stdout("\x1B[48;2;33;48;67mLorem Ipsum\x1B[0m") + .stderr(""); +} + +#[test] +fn theme_env_overrides_config() { + bat_with_config() + .env("BAT_CONFIG_PATH", "bat-theme.conf") + .env("BAT_THEME", "ansi") + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=80") + .arg("--wrap=never") + .arg("--decorations=always") + .arg("--style=plain") + .arg("--highlight-line=1") + .write_stdin("Ansi Underscore Test\nAnother Line") + .assert() + .success() + .stdout("\x1B[4mAnsi Underscore Test\n\x1B[24mAnother Line") + .stderr(""); +} + +#[test] +fn highlighting_is_skipped_on_long_lines() { + let expected = "\u{1b}[38;5;231m{\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;208mapi\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;231m:\u{1b}[0m\n".to_owned() + + "\u{1b}" + + r#"[38;5;231m {"ANGLE_instanced_arrays":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/ANGLE_instanced_arrays","spec_url":"https://www.khronos.org/registry/webgl/extensions/ANGLE_instanced_arrays/","support":{"chrome":{"version_added":"32"},"chrome_android":{"version_added":"32"},"edge":{"version_added":"12"},"firefox":{"version_added":"47"},"firefox_android":{"version_added":true},"ie":{"version_added":"11"},"opera":{"version_added":"19"},"opera_android":{"version_added":"19"},"safari":{"version_added":"8"},"safari_ios":{"version_added":"8"},"samsunginternet_android":{"version_added":"2.0"},"webview_android":{"version_added":"4.4"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}},"drawArraysInstancedANGLE":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/ANGLE_instanced_arrays/drawArraysInstancedANGLE","spec_url":"https://www.khronos.org/registry/webgl/extensions/ANGLE_instanced_arrays/","support":{"chrome":{"version_added":"32"},"chrome_android":{"version_added":"32"},"edge":{"version_added":"12"},"firefox":{"version_added":"47"},"firefox_android":{"version_added":true},"ie":{"version_added":"11"},"opera":{"version_added":"19"},"opera_android":{"version_added":"19"},"safari":{"version_added":"8"},"safari_ios":{"version_added":"8"},"samsunginternet_android":{"version_added":"2.0"},"webview_android":{"version_added":"4.4"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}}},"drawElementsInstancedANGLE":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/ANGLE_instanced_arrays/drawElementsInstancedANGLE","spec_url":"https://www.khronos.org/registry/webgl/extensions/ANGLE_instanced_arrays/","support":{"chrome":{"version_added":"32"},"chrome_android":{"version_added":"32"},"edge":{"version_added":"12"},"firefox":{"version_added":"47"},"firefox_android":{"version_added":true},"ie":{"version_added":"11"},"opera":{"version_added":"19"},"opera_android":{"version_added":"19"},"safari":{"version_added":"8"},"safari_ios":{"version_added":"8"},"samsunginternet_android":{"version_added":"2.0"},"webview_android":{"version_added":"4.4"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}}},"vertexAttribDivisorANGLE":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/ANGLE_instanced_arrays/vertexAttribDivisorANGLE","spec_url":"https://www.khronos.org/registry/webgl/extensions/ANGLE_instanced_arrays/","support":{"chrome":{"version_added":"32"},"chrome_android":{"version_added":"32"},"edge":{"version_added":"12"},"firefox":{"version_added":"47"},"firefox_android":{"version_added":true},"ie":{"version_added":"11"},"opera":{"version_added":"19"},"opera_android":{"version_added":"19"},"safari":{"version_added":"8"},"safari_ios":{"version_added":"8"},"samsunginternet_android":{"version_added":"2.0"},"webview_android":{"version_added":"4.4"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}}}},"AbortController":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortController","spec_url":"https://dom.spec.whatwg.org/#interface-abortcontroller","support":{"chrome":{"version_added":"66"},"chrome_android":{"version_added":"66"},"edge":{"version_added":"16"},"firefox":{"version_added":"57"},"firefox_android":{"version_added":"57"},"ie":{"version_added":false},"nodejs":{"version_added":"15.0.0"},"opera":{"version_added":"53"},"opera_android":{"version_added":"47"},"safari":[{"version_added":"12.1"},{"version_added":"11.1","partial_implementation":true,"notes":"Even though window.AbortController is defined, it doesn't really abort fetch requests. See bug 174980."}],"safari_ios":[{"version_added":"12.2"},{"version_added":"11.3","partial_implementation":true,"notes":"Even though window.AbortController is defined, it doesn't really abort fetch requests. See bug 174980."}],"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"66"}},"status":{"experimental":true,"standard_track":true,"deprecated":false}},"AbortController":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortController/AbortController","spec_url":"https://dom.spec.whatwg.org/#ref-for-dom-abortcontroller-abortcontroller①","description":"AbortController() constructor","support":{"chrome":{"version_added":"66"},"chrome_android":{"version_added":"66"},"edge":{"version_added":"16"},"firefox":{"version_added":"57"},"firefox_android":{"version_added":"57"},"ie":{"version_added":false},"nodejs":{"version_added":"15.0.0"},"opera":{"version_added":"53"},"opera_android":{"version_added":"47"},"safari":[{"version_added":"12.1"},{"version_added":"11.1","partial_implementation":true,"notes":"Even though window.AbortController is defined, it doesn't really abort fetch requests. See bug 174980."}],"safari_ios":[{"version_added":"12.2"},{"version_added":"11.3","partial_implementation":true,"notes":"Even though window.AbortController is defined, it doesn't really abort fetch requests. See bug 174980."}],"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"66"}},"status":{"experimental":true,"standard_track":true,"deprecated":false}}},"abort":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortController/abort","spec_url":"https://dom.spec.whatwg.org/#ref-for-dom-abortcontroller-abortcontroller①","support":{"chrome":{"version_added":"66"},"chrome_android":{"version_added":"66"},"edge":{"version_added":"16"},"firefox":{"version_added":"57"},"firefox_android":{"version_added":"57"},"ie":{"version_added":false},"nodejs":{"version_added":"15.0.0"},"opera":{"version_added":"53"},"opera_android":{"version_added":"47"},"safari":[{"version_added":"12.1"},{"version_added":"11.1","partial_implementation":true,"notes":"Even though window.AbortController is defined, it doesn't really abort fetch requests. See bug 174980."}],"safari_ios":[{"version_added":"12.2"},{"version_added":"11.3","partial_implementation":true,"notes":"Even though window.AbortController is defined, it doesn't really abort fetch requests. See bug 174980."}],"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"66"}},"status":{"experimental":true,"standard_track":true,"deprecated":false}}},"signal":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortController/signal","spec_url":"https://dom.spec.whatwg.org/#ref-for-dom-abortcontroller-signal②","support":{"chrome":{"version_added":"66"},"chrome_android":{"version_added":"66"},"edge":{"version_added":"16"},"firefox":{"version_added":"57"},"firefox_android":{"version_added":"57"},"ie":{"version_added":false},"nodejs":{"version_added":"15.0.0"},"opera":{"version_added":"53"},"opera_android":{"version_added":"47"},"safari":[{"version_added":"12.1"},{"version_added":"11.1","partial_implementation":true,"notes":"Even though window.AbortController is defined, it doesn't really abort fetch requests. See bug 174980."}],"safari_ios":[{"version_added":"12.2"},{"version_added":"11.3","partial_implementation":true,"notes":"Even though window.AbortController is defined, it doesn't really abort fetch requests. See bug 174980."}],"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"66"}},"status":{"experimental":true,"standard_track":true,"deprecated":false}}}},"AbortPaymentEvent":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortPaymentEvent","support":{"chrome":{"version_added":"70"},"chrome_android":{"version_added":"70"},"edge":{"version_added":"79"},"firefox":{"version_added":false},"firefox_android":{"version_added":false},"ie":{"version_added":false},"opera":{"version_added":"57"},"opera_android":{"version_added":"49"},"safari":{"version_added":false},"safari_ios":{"version_added":false},"samsunginternet_android":{"version_added":"10.0"},"webview_android":{"version_added":false}},"status":{"experimental":true,"standard_track":false,"deprecated":false}},"AbortPaymentEvent":{"__compat":{"description":"AbortPaymentEvent() constructor","mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortPaymentEvent/AbortPaymentEvent","support":{"chrome":{"version_added":"70"},"chrome_android":{"version_added":"70"},"edge":{"version_added":"79"},"firefox":{"version_added":false},"firefox_android":{"version_added":false},"ie":{"version_added":false},"opera":{"version_added":"57"},"opera_android":{"version_added":"49"},"safari":{"version_added":false},"safari_ios":{"version_added":false},"samsunginternet_android":{"version_added":"10.0"},"webview_android":{"version_added":false}},"status":{"experimental":true,"standard_track":false,"deprecated":false}}},"respondWith":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortPaymentEvent/respondWith","support":{"chrome":{"version_added":"70"},"chrome_android":{"version_added":"70"},"edge":{"version_added":"79"},"firefox":{"version_added":false},"firefox_android":{"version_added":false},"ie":{"version_added":false},"opera":{"version_added":"57"},"opera_android":{"version_added":"49"},"safari":{"version_added":false},"safari_ios":{"version_added":false},"samsunginternet_android":{"version_added":"10.0"},"webview_android":{"version_added":false}},"status":{"experimental":true,"standard_track":false,"deprecated":false}}}},"AbortSignal":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortSignal","spec_url":"https://dom.spec.whatwg.org/#interface-AbortSignal","support":{"chrome":{"version_added":"66"},"chrome_android":{"version_added":"66"},"edge":{"version_added":"16"},"firefox":{"version_added":"57"},"firefox_android":{"version_added":"57"},"ie":{"version_added":false},"nodejs":{"version_added":"15.0.0"},"opera":{"version_added":"53"},"opera_android":{"version_added":"47"},"safari":{"version_added":"11.1"},"safari_ios":{"version_added":"11.3"},"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"66"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}},"abort":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortSignal/abort","spec_url":"https://dom.spec.whatwg.org/#ref-for-dom-abortsignal-abort①","support":{"chrome":{"version_added":false},"chrome_android":{"version_added":false},"edge":{"version_added":false},"firefox":{"version_added":"88"},"firefox_android":{"version_added":"88"},"ie":{"version_added":false},"nodejs":{"version_added":false},"opera":{"version_added":false},"opera_android":{"version_added":false},"safari":{"version_added":false},"safari_ios":{"version_added":false},"samsunginternet_android":{"version_added":false},"webview_android":{"version_added":false}},"status":{"experimental":false,"standard_track":true,"deprecated":false}}},"abort_event":{"__compat":{"description":"abort event","mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event","spec_url":"https://dom.spec.whatwg.org/#eventdef-abortsignal-abort","support":{"chrome":{"version_added":"66"},"chrome_android":{"version_added":"66"},"edge":{"version_added":"16"},"firefox":{"version_added":"57"},"firefox_android":{"version_added":"57"},"ie":{"version_added":false},"nodejs":{"version_added":"15.0.0"},"opera":{"version_added":"53"},"opera_android":{"version_added":"47"},"safari":{"version_added":"11.1"},"safari_ios":{"version_added":"11.3"},"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"66"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}}},"aborted":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted","spec_url":"https://dom.spec.whatwg.org/#ref-for-dom-abortsignal-aborted①","support":{"chrome":{"version_added":"66"},"chrome_android":{"version_added":"66"},"edge":{"version_added":"16"},"firefox":{"version_added":"57"},"firefox_android":{"version_added":"57"},"ie":{"version_added":false},"nodejs":{"version_added":"15.0.0"},"opera":{"version_added":"53"},"opera_android":{"version_added":"47"},"safari":{"version_added":"11.1"},"safari_ios":{"version_added":"11.3"},"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"66"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}}},"onabort":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbortSignal/onabort","spec_url":"https://dom.spec.whatwg.org/#abortsignal-onabort","support":{"chrome":{"version_added":"66"},"chrome_android":{"version_added":"66"},"edge":{"version_added":"16"},"firefox":{"version_added":"57"},"firefox_android":{"version_added":"57"},"ie":{"version_added":false},"nodejs":{"version_added":"15.0.0"},"opera":{"version_added":"53"},"opera_android":{"version_added":"47"},"safari":{"version_added":"11.1"},"safari_ios":{"version_added":"11.3"},"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"66"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}}}},"AbsoluteOrientationSensor":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbsoluteOrientationSensor","spec_url":"https://w3c.github.io/orientation-sensor/#absoluteorientationsensor-interface","support":{"chrome":{"version_added":"67"},"chrome_android":{"version_added":"67"},"edge":{"version_added":"79"},"firefox":{"version_added":false},"firefox_android":{"version_added":false},"ie":{"version_added":false},"opera":{"version_added":"54"},"opera_android":{"version_added":"48"},"safari":{"version_added":false},"safari_ios":{"version_added":false},"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"67"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}},"AbsoluteOrientationSensor":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbsoluteOrientationSensor/AbsoluteOrientationSensor","spec_url":"https://w3c.github.io/orientation-sensor/#dom-absoluteorientationsensor-absoluteorientationsensor","description":"AbsoluteOrientationSensor() constructor","support":{"chrome":{"version_added":"67"},"chrome_android":{"version_added":"67"},"edge":{"version_added":"79"},"firefox":{"version_added":false},"firefox_android":{"version_added":false},"ie":{"version_added":false},"opera":{"version_added":"54"},"opera_android":{"version_added":"48"},"safari":{"version_added":false},"safari_ios":{"version_added":false},"samsunginternet_android":{"version_added":"9.0"},"webview_android":{"version_added":"67"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}}}},"AbstractRange":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbstractRange","spec_url":"https://dom.spec.whatwg.org/#interface-abstractrange","support":{"chrome":{"version_added":"90"},"chrome_android":{"version_added":"90"},"edge":[{"version_added":"90"},{"version_added":"18","version_removed":"79"}],"firefox":{"version_added":"69"},"firefox_android":{"version_added":false},"ie":{"version_added":false},"opera":{"version_added":false},"opera_android":{"version_added":false},"safari":{"version_added":"14.1"},"safari_ios":{"version_added":"14.5"},"samsunginternet_android":{"version_added":false},"webview_android":{"version_added":"90"}},"status":{"experimental":false,"standard_track":true,"deprecated":false}},"collapsed":{"__compat":{"mdn_url":"https://developer.mozilla.org/docs/Web/API/AbstractRange/collapsed","spec_url":"https://dom.spec.whatwg.org/#ref-for-dom-range-collapsed①","support":{"chrome":{"version_added":"90"},"chrome_android":{"version_added":"90"},"edge":[{"version_added":"90"},{"version_added":"18","version_removed":"79"}],"firefox":{"version_added":"69"},"firefox_android":{"version_added":false},"ie":{"version_added":false},"opera":{"version_added":false},"opera_android":"# + + "\u{1b}[0m\n\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;231m{\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;208mversion_added\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;231m:\u{1b}[0m\u{1b}[38;5;141mfalse\u{1b}[0m\u{1b}[38;5;231m}\u{1b}[0m\n"; + + bat() + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("longline.json") + .assert() + .success() + .stdout(expected) + .stderr(""); +} + +#[test] +fn all_global_git_config_locations_syntax_mapping_work() { + let fake_home = Path::new(EXAMPLES_DIR).join("git").canonicalize().unwrap(); + let expected = "\u{1b}[38;5;231m[\u{1b}[0m\u{1b}[38;5;149muser\u{1b}[0m\u{1b}[38;5;231m]\u{1b}[0m +\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;231memail\u{1b}[0m\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;203m=\u{1b}[0m\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;186mfoo@bar.net\u{1b}[0m +\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;231mname\u{1b}[0m\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;203m=\u{1b}[0m\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;186mfoobar\u{1b}[0m +"; + + bat() + .env("XDG_CONFIG_HOME", fake_home.join(".config").as_os_str()) + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("git/.config/git/config") + .assert() + .success() + .stdout(expected) + .stderr(""); + + bat() + .env("HOME", fake_home.as_os_str()) + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("git/.config/git/config") + .assert() + .success() + .stdout(expected) + .stderr(""); + + bat() + .env("HOME", fake_home.as_os_str()) + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("git/.gitconfig") + .assert() + .success() + .stdout(expected) + .stderr(""); +} + +#[test] +fn map_syntax_and_ignored_suffix_work_together() { + bat() + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("--ignored-suffix=.suffix") + .arg("--map-syntax=*.demo:JSON") + .arg("test.demo.suffix") + .assert() + .success() + .stdout("\u{1b}[38;5;231m{\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;208mtest\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;231m:\u{1b}[0m\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;186m\"\u{1b}[0m\u{1b}[38;5;186mvalue\u{1b}[0m\u{1b}[38;5;186m\"\u{1b}[0m\u{1b}[38;5;231m}\u{1b}[0m") + .stderr(""); + + bat() + .arg("-f") + .arg("--theme") + .arg("Monokai Extended") + .arg("-p") + .arg("--ignored-suffix=.suffix") + .arg("--ignored-suffix=.foo") + .arg("--map-syntax=*.demo:JSON") + .arg("test.demo.foo.suffix") + .assert() + .success() + .stdout("\u{1b}[38;5;231m{\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;208mtest\u{1b}[0m\u{1b}[38;5;208m\"\u{1b}[0m\u{1b}[38;5;231m:\u{1b}[0m\u{1b}[38;5;231m \u{1b}[0m\u{1b}[38;5;186m\"\u{1b}[0m\u{1b}[38;5;186mvalue\u{1b}[0m\u{1b}[38;5;186m\"\u{1b}[0m\u{1b}[38;5;231m}\u{1b}[0m") + .stderr(""); +} + +#[test] +fn acknowledgements() { + bat() + .arg("--acknowledgements") + .assert() + .success() + .stdout( + // Just some sanity checking that avoids names of persons, except our own Keith Hall :) + predicate::str::contains( + "Copyright (c) 2018-2021 bat-developers (https://github.com/sharkdp/bat).", + ) + .and(predicate::str::contains( + "Copyright (c) 2012-2020 The Sublime CMake authors", + )) + .and(predicate::str::contains( + "Copyright 2014-2015 SaltStack Team", + )) + .and(predicate::str::contains( + "Copyright (c) 2013-present Dracula Theme", + )) + .and(predicate::str::contains( + "## syntaxes/01_Packages/Rust/LICENSE.txt", + )) + .and(predicate::str::contains( + "## syntaxes/02_Extra/http-request-response/LICENSE", + )) + .and(predicate::str::contains( + "## themes/dracula-sublime/LICENSE", + )) + .and(predicate::str::contains("Copyright (c) 2017 b123400")) + .and(predicate::str::contains("Copyright (c) 2021 Keith Hall")) + .and(predicate::str::contains("Copyright 2014 Clams")), + ) + .stderr(""); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_file_piped() { + bat() + .env("LESSOPEN", "|echo File is %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("File is test.txt\n"); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_stdin_piped() { + bat() + .env("LESSOPEN", "|cat") + .arg("--lessopen") + .write_stdin("hello world\n") + .assert() + .success() + .stdout("hello world\n"); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_and_lessclose_file_temp() { + // This is mainly to test that $LESSCLOSE gets passed the correct file paths + // In this case, the original file and the temporary file returned by $LESSOPEN + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "echo empty.txt && echo %s >/dev/null") + .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("lessclose: test.txt empty.txt\n"); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_and_lessclose_file_piped() { + // This is mainly to test that $LESSCLOSE gets passed the correct file paths + // In these cases, the original file and a dash + bat() + // This test will not work properly if $LESSOPEN does not output anything + .env("LESSOPEN", "|cat %s") + .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\nlessclose: test.txt -\n"); + + bat() + .env("LESSOPEN", "||cat %s") + .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") + .arg("empty.txt") + .assert() + .success() + .stdout("lessclose: empty.txt -\n"); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_and_lessclose_stdin_temp() { + // This is mainly to test that $LESSCLOSE gets passed the correct file paths + // In this case, a dash and the temporary file returned by $LESSOPEN + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "-echo empty.txt && echo %s >/dev/null") + .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") + .write_stdin("test.txt") + .assert() + .success() + .stdout("lessclose: - empty.txt\n"); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_and_lessclose_stdin_piped() { + // This is mainly to test that $LESSCLOSE gets passed the correct file paths + // In these cases, two dashes + bat() + // This test will not work properly if $LESSOPEN does not output anything + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|-cat test.txt && echo %s >/dev/null") + .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") + .write_stdin("empty.txt") + .assert() + .success() + .stdout("hello world\nlessclose: - -\n"); + + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||-cat empty.txt && echo %s >/dev/null") + .env("LESSCLOSE", "echo lessclose: %s %s") + .arg("--lessopen") + .write_stdin("empty.txt") + .assert() + .success() + .stdout("lessclose: - -\n"); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_handling_empty_output_file() { + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|cat empty.txt && echo %s >/dev/null") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); + + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|cat nonexistent.txt && echo %s >/dev/null") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); + + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||cat empty.txt && echo %s >/dev/null") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout(""); + + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||cat nonexistent.txt && echo %s >/dev/null") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +// FIXME +fn lessopen_handling_empty_output_stdin() { + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|-cat empty.txt && echo %s >/dev/null") + .arg("--lessopen") + .write_stdin("hello world\n") + .assert() + .success() + .stdout("hello world\n"); + + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "|-cat nonexistent.txt && echo %s >/dev/null") + .arg("--lessopen") + .write_stdin("hello world\n") + .assert() + .success() + .stdout("hello world\n"); + + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||-cat empty.txt && echo %s >/dev/null") + .arg("--lessopen") + .write_stdin("hello world\n") + .assert() + .success() + .stdout(""); + + bat() + // Need a %s for $LESSOPEN to be valid + .env("LESSOPEN", "||-cat nonexistent.txt && echo %s >/dev/null") + .arg("--lessopen") + .write_stdin("hello world\n") + .assert() + .success() + .stdout("hello world\n"); +} + +#[cfg(unix)] // Expected output assumed that tests are run on a Unix-like system +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_uses_shell() { + bat() + .env("LESSOPEN", "|cat < %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[cfg(unix)] +#[cfg(feature = "lessopen")] +#[test] +fn do_not_use_lessopen_by_default() { + bat() + .env("LESSOPEN", "|echo File is %s") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[cfg(unix)] +#[cfg(feature = "lessopen")] +#[test] +fn do_not_use_lessopen_if_overridden() { + bat() + .env("LESSOPEN", "|echo File is %s") + .arg("--lessopen") + .arg("--no-lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[cfg(unix)] +#[cfg(feature = "lessopen")] +#[test] +fn lessopen_validity() { + bat() + .env("LESSOPEN", "|echo File is test.txt") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n") + .stderr( + "\u{1b}[33m[bat warning]\u{1b}[0m: LESSOPEN ignored: must contain exactly one %s\n", + ); + + bat() + .env("LESSOPEN", "|echo File is %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("File is test.txt\n") + .stderr(""); + + bat() + .env("LESSOPEN", "|echo %s is %s") + .arg("--lessopen") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n") + .stderr( + "\u{1b}[33m[bat warning]\u{1b}[0m: LESSOPEN ignored: must contain exactly one %s\n", + ); +} + +// Regression test for issue #2520 and PR #2650 +// Syntax highlighting should be the same regardless of +// --map-syntax' case or file extension's case +#[test] +fn highlighting_independant_from_map_syntax_case() { + let expected = bat() + .arg("-f") + .arg("--map-syntax=*.config:JSON") + .arg("map-syntax_case.Config") + .assert() + .get_output() + .stdout + .clone(); + + bat() + .arg("-f") + .arg("--map-syntax=*.Config:JSON") + .arg("map-syntax_case.Config") + .assert() + .success() + .stdout(expected) + .stderr(""); +} + +#[test] +fn map_syntax_target_syntax_case_insensitive() { + let expected = bat() + .arg("-f") + .arg("--map-syntax=*.config:json") + .arg("map-syntax_case.Config") + .assert() + .get_output() + .stdout + .clone(); + + bat() + .arg("-f") + .arg("--map-syntax=*.config:json") + .arg("map-syntax_case.Config") + .assert() + .success() + .stdout(expected) + .stderr(""); +} + +#[test] +fn strip_ansi_always_strips_ansi() { + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=always") + .write_stdin("\x1B[33mYellow\x1B[m") + .assert() + .success() + .stdout("Yellow"); +} + +#[test] +fn strip_ansi_never_does_not_strip_ansi() { + let output = String::from_utf8( + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=never") + .write_stdin("\x1B[33mYellow\x1B[m") + .assert() + .success() + .get_output() + .stdout + .clone(), + ) + .expect("valid utf8"); + + assert!(output.contains("\x1B[33mYellow")) +} + +#[test] +fn strip_ansi_does_not_affect_simple_printer() { + let output = String::from_utf8( + bat() + .arg("--style=plain") + .arg("--decorations=never") + .arg("--color=never") + .arg("--strip-ansi=always") + .write_stdin("\x1B[33mYellow\x1B[m") + .assert() + .success() + .get_output() + .stdout + .clone(), + ) + .expect("valid utf8"); + + assert!(output.contains("\x1B[33mYellow")) +} + +#[test] +fn strip_ansi_does_not_strip_when_show_nonprintable() { + let output = String::from_utf8( + bat() + .arg("--style=plain") + .arg("--decorations=never") + .arg("--color=always") + .arg("--strip-ansi=always") + .arg("--show-nonprintable") + .write_stdin("\x1B[33mY") + .assert() + .success() + .get_output() + .stdout + .clone(), + ) + .expect("valid utf8"); + + assert!(output.contains("␛")) +} + +#[test] +fn strip_ansi_auto_strips_ansi_when_detected_syntax_by_filename() { + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=auto") + .arg("--file-name=test.rs") + .write_stdin("fn \x1B[33mYellow\x1B[m() -> () {}") + .assert() + .success() + .stdout("fn Yellow() -> () {}"); +} + +#[test] +fn strip_ansi_auto_strips_ansi_when_provided_syntax_by_option() { + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=auto") + .arg("--language=rust") + .write_stdin("fn \x1B[33mYellow\x1B[m() -> () {}") + .assert() + .success() + .stdout("fn Yellow() -> () {}"); +} + +#[test] +fn strip_ansi_auto_does_not_strip_when_plain_text_by_filename() { + let output = String::from_utf8( + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=auto") + .arg("--file-name=ansi.txt") + .write_stdin("\x1B[33mYellow\x1B[m") + .assert() + .success() + .get_output() + .stdout + .clone(), + ) + .expect("valid utf8"); + + assert!(output.contains("\x1B[33mYellow")) +} + +#[test] +fn strip_ansi_auto_does_not_strip_ansi_when_plain_text_by_option() { + let output = String::from_utf8( + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=auto") + .arg("--language=txt") + .write_stdin("\x1B[33mYellow\x1B[m") + .assert() + .success() + .get_output() + .stdout + .clone(), + ) + .expect("valid utf8"); + + assert!(output.contains("\x1B[33mYellow")) +} + +#[test] +fn sanitize_implies_strip_ansi() { + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("\x1B[33mYellow\x1B[m") + .assert() + .success() + .stdout("Yellow"); +} + +#[test] +fn sanitize_strips_osc_clipboard_hijack() { + // OSC 52 sets the system clipboard. A file containing this would silently + // overwrite the user's clipboard if displayed unfiltered. + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("safe\x1B]52;c;cm0=\x07payload") + .assert() + .success() + .stdout("safepayload"); +} + +#[test] +fn sanitize_strips_osc_8_hyperlink_spoof() { + // OSC 8 hyperlinks let displayed text point to an arbitrary URL. + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("\x1B]8;;https://evil.example\x07click here\x1B]8;;\x07") + .assert() + .success() + .stdout("click here"); +} + +#[test] +fn sanitize_strips_window_title_injection() { + // OSC 0/1/2 set the terminal window title. + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("hello\x1B]0;evil-title\x07world") + .assert() + .success() + .stdout("helloworld"); +} + +#[test] +fn sanitize_strips_8bit_csi() { + // 8-bit CSI introducer (U+009B) is the single-codepoint equivalent of ESC [. + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("a\u{9B}31mRED\u{9B}0mb") + .assert() + .success() + .stdout("aREDb"); +} + +#[test] +fn sanitize_substitutes_bare_cr() { + // Bare CR (not part of CRLF) is the line-overwrite forgery vector. + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("safe\rEVIL") + .assert() + .success() + .stdout("safe\u{FFFD}EVIL"); +} + +#[test] +fn sanitize_preserves_crlf() { + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("line1\r\nline2\r\n") + .assert() + .success() + .stdout("line1\r\nline2\r\n"); +} + +#[test] +fn sanitize_substitutes_bidi_controls() { + // Trojan-Source attack (CVE-2021-42574): U+202E (RLO) reorders display. + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("admin\u{202E}check") + .assert() + .success() + .stdout("admin\u{FFFD}check"); +} + +#[test] +fn sanitize_substitutes_zero_width() { + // Zero-width chars allow invisible content / identifier confusion. + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("ad\u{200B}min") + .assert() + .success() + .stdout("ad\u{FFFD}min"); +} + +#[test] +fn sanitize_preserves_form_feed_in_source() { + // FF (U+000C) is used as a section separator in C source and Emacs Lisp. + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("section1\x0Csection2") + .assert() + .success() + .stdout("section1\x0Csection2"); +} + +#[test] +fn sanitize_preserves_unicode_text() { + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--sanitize=always") + .write_stdin("snowman ☃ CJK 漢字 emoji 🦀") + .assert() + .success() + .stdout("snowman ☃ CJK 漢字 emoji 🦀"); +} + +// Tests that style components can be removed with `-component`. +#[test] +fn style_components_can_be_removed() { + bat() + .arg({ + #[cfg(not(feature = "git"))] + { + "--style=full,-grid" + } + #[cfg(feature = "git")] + { + "--style=full,-grid,-changes" + } + }) + .arg("--decorations=always") + .arg("--color=never") + .write_stdin("test") + .assert() + .success() + .stdout(" STDIN\n Size: -\n 1 test\n") + .stderr(""); +} + +// Tests that style components are chosen based on the rightmost `--style` argument. +#[test] +fn style_components_can_be_overidden() { + bat() + .arg("--style=full") + .arg("--style=header,numbers") + .arg("--decorations=always") + .arg("--color=never") + .write_stdin("test") + .assert() + .success() + .stdout(" STDIN\n 1 test\n") + .stderr(""); +} + +// Tests that style components can be merged across multiple `--style` arguments. +#[test] +fn style_components_will_merge() { + bat() + .arg("--style=header,grid") + .arg("--style=-grid,+numbers") + .arg("--decorations=always") + .arg("--color=never") + .write_stdin("test") + .assert() + .success() + .stdout(" STDIN\n 1 test\n") + .stderr(""); +} + +// Tests that style components can be merged with the `BAT_STYLE` environment variable. +#[test] +fn style_components_will_merge_with_env_var() { + bat() + .env("BAT_STYLE", "header,grid") + .arg("--style=-grid,+numbers") + .arg("--decorations=always") + .arg("--color=never") + .write_stdin("test") + .assert() + .success() + .stdout(" STDIN\n 1 test\n") + .stderr(""); +} + +// Test for https://github.com/sharkdp/bat/issues/3526 +#[test] +fn plain_with_sized_terminal_width() { + bat() + .arg("--plain") + .arg("--terminal-width=6") + .arg("--decorations=always") + .arg("test.txt") + .assert() + .success() + .stdout("hello \nworld\n") + .stderr(""); +} + +#[test] +fn quiet_empty_suppresses_output_on_empty_stdin() { + bat() + .arg("--quiet-empty") + .write_stdin("") + .assert() + .success() + .stdout(""); +} + +#[test] +fn quiet_empty_does_not_affect_non_empty_input() { + bat() + .arg("--quiet-empty") + .write_stdin("hello\n") + .assert() + .success() + .stdout("hello\n"); +} + +#[test] +fn quiet_empty_suppresses_output_on_empty_file() { + bat() + .arg("--quiet-empty") + .arg("empty.txt") + .assert() + .success() + .stdout(""); +} + +#[test] +fn cache_help_shows_help_message() { + // Test that `bat cache --help` works (fixes #3560) + // Run in cache_source directory which doesn't have a file named "cache" + bat_with_config() + .current_dir(Path::new(EXAMPLES_DIR).join("cache_source")) + .arg("cache") + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains( + "Modify the syntax-definition and theme cache", + )) + .stdout(predicate::str::contains("--build")) + .stdout(predicate::str::contains("--clear")); +} + +#[test] +fn unbuffered_flag_is_accepted() { + bat() + .arg("--unbuffered") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[test] +fn unbuffered_mode_disables_line_numbers() { + // When --unbuffered is used, line numbers should be auto-disabled even if requested + bat() + .arg("--unbuffered") + .arg("--style=numbers") + .arg("--decorations=always") + .arg("--color=never") + .arg("test.txt") + .assert() + .success() + .stdout(predicate::str::starts_with(" 1").not()); +} + +#[test] +fn unbuffered_mode_plain_output() { + bat() + .arg("--unbuffered") + .arg("--color=never") + .arg("--decorations=never") + .arg("test.txt") + .assert() + .success() + .stdout("hello world\n"); +} + +#[test] +fn word_wrap_breaks_at_word_boundaries() { + bat() + .arg("word-wrap.txt") + .arg("--wrap=word") + .arg("--terminal-width=40") + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .assert() + .success() + .stdout( + "\ +The quick brown fox jumps over the lazy +dog and then runs away +superlongwordthatdefinitelyexceedstheter +minalwidthandshouldfallbacktocharacterwr +apping +short words here +", + ); +} + +#[test] +fn word_wrap_with_line_numbers() { + bat() + .arg("word-wrap.txt") + .arg("--wrap=word") + .arg("--terminal-width=40") + .arg("--style=numbers") + .arg("--decorations=always") + .arg("--color=never") + .assert() + .success() + .stdout( + " 1 The quick brown fox jumps over the + lazy dog and then runs away + 2 superlongwordthatdefinitelyexceedst + heterminalwidthandshouldfallbacktoc + haracterwrapping + 3 short words here +", + ); +} + +#[test] +fn word_wrap_short_line_no_wrap() { + bat() + .arg("--wrap=word") + .arg("--terminal-width=80") + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("single-line.txt") + .assert() + .success() + .stdout("Single Line\n"); +} + +#[cfg(unix)] +#[cfg(feature = "git")] +fn setup_diff_test_repo() -> tempfile::TempDir { + use std::process::Command; + + let dir = tempfile::tempdir().expect("can create temporary directory"); + let repo = dir.path(); + + // Initialize a git repo and commit a file + Command::new("git") + .args(["init"]) + .current_dir(repo) + .output() + .expect("git init"); + + Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(repo) + .output() + .expect("git config email"); + + Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(repo) + .output() + .expect("git config name"); + + std::fs::write(repo.join("test.txt"), "line 1\nline 2\nline 3\n").expect("can write test file"); + + Command::new("git") + .args(["add", "test.txt"]) + .current_dir(repo) + .output() + .expect("git add"); + + Command::new("git") + .args(["commit", "-m", "initial"]) + .current_dir(repo) + .output() + .expect("git commit"); + + // Modify the file so --diff has something to show + std::fs::write( + repo.join("test.txt"), + "line 1\nline 2 modified\nline 3\nline 4 added\n", + ) + .expect("can write modified test file"); + + dir +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn diff_plain_preserves_change_markers() { + let repo = setup_diff_test_repo(); + + // With --diff --plain, output should contain the change marker column + // but not other decorations like line numbers or grid + let output = bat() + .current_dir(repo.path()) + .arg("--diff") + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("test.txt") + .assert() + .success() + .get_output() + .stdout + .clone(); + + let stdout = std::str::from_utf8(&output).expect("valid utf-8"); + + // The output should contain the modified and added lines + assert!( + stdout.contains("line 2 modified"), + "diff plain output should contain modified line, got: {stdout}" + ); + assert!( + stdout.contains("line 4 added"), + "diff plain output should contain added line, got: {stdout}" + ); + + // Should NOT contain line numbers (a decoration that --plain disables) + assert!( + !stdout.contains(" 1"), + "diff plain output should not contain line numbers, got: {stdout}" + ); +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn diff_plain_does_not_show_grid_or_header() { + let repo = setup_diff_test_repo(); + + let output = bat() + .current_dir(repo.path()) + .arg("--diff") + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("--terminal-width=80") + .arg("test.txt") + .assert() + .success() + .get_output() + .stdout + .clone(); + + let stdout = std::str::from_utf8(&output).expect("valid utf-8"); + + // Grid lines use box-drawing characters + assert!( + !stdout.contains('─'), + "diff plain output should not contain grid lines, got: {stdout}" + ); + assert!( + !stdout.contains('│'), + "diff plain output should not contain grid separators, got: {stdout}" + ); + + // Header shows "File: " + assert!( + !stdout.contains("File:"), + "diff plain output should not contain file header, got: {stdout}" + ); +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn plain_without_diff_still_works() { + let repo = setup_diff_test_repo(); + + // --plain without --diff should output file content with no decorations at all + bat() + .current_dir(repo.path()) + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("test.txt") + .assert() + .success() + .stdout("line 1\nline 2 modified\nline 3\nline 4 added\n"); +} + +#[test] +fn tcl_shebang_detection_tclsh() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_tclsh") + .assert() + .success(); +} + +#[test] +fn tcl_shebang_detection_wish() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_wish") + .assert() + .success(); +} + +#[test] +fn tcl_shebang_detection_expect() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_expect") + .assert() + .success(); +} + +#[test] +fn ignored_suffix_enables_first_line_detection() { + // A shebang shell script saved with a `.txt` extension is Plain Text by + // default (the extension wins). With `--ignored-suffix .txt` the suffix is + // stripped before detection, so it falls back to the first line and is + // highlighted exactly as if its language were forced to bash. See #2745. + let fixture = "regression_tests/issue_2745.txt"; + let common = ["--color=always", "--decorations=never", "--style=plain"]; + + let stdout = |args: &[&str]| -> Vec { + let assert = bat() + .args(common) + .args(args) + .arg(fixture) + .assert() + .success(); + assert.get_output().stdout.clone() + }; + + let forced_bash = stdout(&["--language", "bash"]); + let with_ignored_suffix = stdout(&["--ignored-suffix", ".txt"]); + let default = stdout(&[]); + + // The fixture really is being highlighted (forcing bash is not a no-op). + assert!( + forced_bash.windows(2).any(|w| w == b"\x1b["), + "forced-bash output should contain ANSI color codes" + ); + // With the ignored suffix, detection matches forced bash highlighting... + assert_eq!(with_ignored_suffix, forced_bash); + // ...while the default (extension wins) stays plain and differs. + assert_ne!(default, forced_bash); +}