Skip to main content
Settings
Search
Appearance
Theme Mode
About
Jekyll v3.10.0
Environment Production
Last Build
2026-06-28 19:46 UTC
Current Environment Production
Build Time Jun 28, 19:46
Jekyll v3.10.0
Build env (JEKYLL_ENV) production
Quick Links
Page Location
Page Info
Layout article
Collection tools
Path _tools/bat-honest-review.md
URL /tools/bat-honest-review/
Date 2026-06-28
Theme Skin
SVG Backgrounds
Layer Opacity
0.6
0.04
0.08

bat: the honest review

Verdict: install it, alias it to the thing you read files with, and learn two surprises before you trust it — but do not rip cat out of your scripts. bat is cat with syntax highlighting, line numbers, a git change gutter, and a built-in pager. For reading a file at the terminal it’s a genuine upgrade. For the other half of what cat does — being a dumb pipe in a shell script — it’s smarter than you’d fear and less of a drop-in than the README implies. We use it daily. We also tripped over it twice while writing this, and both trips are in the box.

bat is free and open source (Apache-2.0 / MIT). We have no relationship with the project and nothing to sell. Like its siblings ripgrep and fd, the dealbreaker here isn’t price or telemetry — it’s a couple of defaults that surprise anyone arriving from coreutils. We’ll show you exactly where, with output we actually captured on a fresh Ubuntu box.

Install — and the first surprise is the name (again)

brew install bat        # macOS
sudo apt install bat    # Debian/Ubuntu

If you’ve read our fd review you already know the punchline. On macOS and most distros the command is bat. On Debian and Ubuntu it is not — the name bat was already claimed by another package, so apt ships the binary as batcat. Copy the bat … line from any tutorial and:

$ bat --version
bash: line 1: bat: command not found
$ dpkg -L bat | grep bin
/usr/bin
/usr/bin/batcat
$ batcat --version
bat 0.24.0

The command on your PATH is batcat. The fix is one line — a symlink into your ~/.local/bin, or an alias in ~/.bashrc:

ln -s "$(which batcat)" ~/.local/bin/bat   # bulletproof, works in scripts too
# or, lighter, in ~/.bashrc:  alias bat=batcat

Same caveat as fd: an alias only exists in interactive shells, so a script that calls bat still hits command not found. (More on why your scripts shouldn’t call bat at all in a minute.) For the rest of this review we ran the real binary, so the prompts say batcat.

Why you’d switch from cat

Everything below ran against a throwaway demo.py. Here’s the whole pitch in one screen — syntax highlighting, line numbers, a filename header, and a grid, none of which cat gives you:

$ batcat demo.py
─────┬──────────────────────────────────────────
     │ File: demo.py
─────┼──────────────────────────────────────────
   1 │ import sys
   2 │
   3 │ def greet(name):
   4 │     # say hello
   5 │     return f"hello, {name}"
   6 │
   7 │ if __name__ == "__main__":
   8 │     print(greet(sys.argv[1]))
─────┴──────────────────────────────────────────

(The colors don’t survive a copy-paste into a Markdown block, but in a real terminal import, def, the string and the comment are all highlighted.) A few more dailies, all real:

$ batcat -r 3:5 demo.py        # only lines 3–5, numbers preserved
   3 def greet(name):
   4     # say hello
   5     return f"hello, {name}"

$ batcat -A weird.txt          # reveal invisible characters
   1   │ tab↹here␊
   2   │ trailing···␊

That -A is cat -A with a better alphabet: the tab is , the newline is , and trailing spaces show as ·. When you’re hunting a “why won’t this Makefile run” tabs-vs-spaces bug, this is the fastest way to see it. (Speaking of which: the Makefile hack ends on exactly that missing separator error.)

And because bat shells out to git, it draws a change gutter when you point it at a tracked file with uncommitted edits:

$ batcat --style=numbers,changes file.txt
   1 ~ line one CHANGED
   2   line two
   3   line three
   4 + line four added

~ for a modified line, + for an added one — a tiny git diff you didn’t have to ask for.

Surprise 1: the pager that ambushes your muscle memory

In a terminal, bat doesn’t just print and exit like cat. It pipes its output into less. For a 2,000-line file that’s a feature. For the muscle memory of someone who types cat config.yml to glance at twelve lines and get their prompt back, it’s an ambush: now you’re inside a pager, and you have to press q to escape a file you could already see.

You can make it behave. --paging=never disables the pager for one run; setting it in your config (~/.config/bat/config) or export BAT_PAGER= makes it permanent. The honest framing: bat optimizes for reading, cat optimizes for dumping. If your hands expect dumping, retrain them or turn the pager off.

Surprise 2: it does NOT break your pipes (the good surprise)

Here’s the fear everyone has, and it’s wrong. “If I alias cat=bat, won’t all those decorations and color codes poison every pipe?” Watch what actually happens when bat’s output isn’t a terminal:

$ batcat demo.py | grep greet
def greet(name):
    print(greet(sys.argv[1]))

No line numbers. No grid. No header. No color codes. The moment bat detects its stdout isn’t a TTY, it silently switches to plain, uncolored, undecorated output and skips the pager — i.e. it behaves exactly like cat. That’s why batcat file | grep, batcat file | wc -l, and friends all keep working. The auto-detection is the single best-designed thing about this tool.

So why keep cat in scripts? Two reasons, both real. First, the batcat-vs-bat name problem: a script that hardcodes bat breaks on Debian, and one that hardcodes batcat breaks on macOS — cat is on every machine under one name. Second, that auto-plain behavior is a default, not a contract; if you actually need cat-identical bytes you ask for them explicitly with -pp (plain, no pager):

$ batcat -pp demo.py
import sys

def greet(name):
    # say hello
    return f"hello, {name}"

if __name__ == "__main__":
    print(greet(sys.argv[1]))

For interactive reading, alias away. For a script that another machine will run, write cat. The tool that’s a delight to read with is the wrong dependency to bake into automation.

Where plain cat still wins

bat is for humans looking at files. cat is for plumbing. Plain cat wins whenever:

  • You’re scripting. One name, every machine, zero surprises, no syntax-highlighting CPU you don’t need.
  • You’re concatenating. cat a b c > out is cat’s literal job; bat a b c will try to help (headers between files), which is the opposite of what you want feeding a redirect.
  • The file is huge or binary. bat is happy to launch a pager and attempt to highlight; cat just streams bytes. And when bat can’t find your file it’s louder about it — which is friendly interactively and noise in a pipeline:
$ batcat nope.txt
[bat error]: 'nope.txt': No such file or directory (os error 2)

What it costs and the free alternative

It costs nothing — open source, no account, no telemetry, no paid tier. The free alternative is the one already on your machine: cat. The honest trade is reading comfort (highlighting, numbers, git gutter, paging) versus plumbing simplicity (one name, dumb pipe, everywhere). They’re not really competitors; they’re a division of labor. Let bat be the thing you read with and cat be the thing you script with, and you never have to choose.

(bat also makes a tidy colorizing pager for other tools — export MANPAGER="sh -c 'col -bx | batcat -l man -p'" gives you syntax-highlighted man pages — but that’s a config rabbit hole for another day.)

What made us close the tab

Nothing — bat is staying on every machine, aliased to the file-reading half of our brain. The honest caveats, in the order they’ll bite you:

  • The name isn’t bat on Debian/Ubuntu. It’s batcat. Symlink or alias it on day one or every tutorial lies to you.
  • It pages by default. cat-muscle-memory lands you inside less. --paging=never (or BAT_PAGER=) turns it off; press q until then.
  • Don’t put it in scripts. Not because it breaks pipes — it doesn’t, it auto-plains when piped — but because the name isn’t portable and the plain behavior is a default, not a promise. Script with cat; read with bat.

When it goes wrong: if bat is behaving weirdly inside a pipeline or a script, the fastest sanity check is to force the cat-compatible mode explicitly: batcat -pp <file> (plain, no pager, no color, no decorations). If that gives you what you wanted, your problem was a decoration or the pager, not the tool. And if a tutorial command “does nothing,” check the name — you almost certainly typed bat on a box that only knows batcat.