You grep -R a codebase and wait. The results dump 400 lines you scroll past. You cat a config file and get a wall of plain text. You hit Ctrl-R and cycle through shell history hoping the command you need is somewhere in the last ten entries. That is the default terminal experience, and none of it is mandatory.
Three tools replace those workflows: ripgrep for searching, fzf for picking, bat for reading. Everything in this article was tested on Ubuntu 24.04 and cross-checked against the official docs of each project. Install, the commands that matter, and the gotchas the READMEs warn about but most tutorials skip.
Prerequisites
- Linux, macOS, or WSL (Windows works too, via winget or scoop)
- A shell you actually use, ideally bash or zsh
- A project with more than a handful of files, because that is where these tools earn their keep
Part 1: ripgrep, search that respects your project
ripgrep (rg) is a recursive search tool written in Rust. Its defaults are what make it different from grep -R: it skips hidden files, skips binary files, and ignores anything your .gitignore (inside a git repo), .ignore, or .rgignore files exclude. You search the code, not the build artifacts.
Install
# Debian/Ubuntu
sudo apt-get install ripgrep
# macOS
brew install ripgrep
# Windows
winget install BurntSushi.ripgrep.MSVC
The commands that matter
rg "TODO" -n # line numbers
rg "def " -l # only file names
rg "return" -c # match count per file
rg -tpy "def " # Python files only
rg -Tjs "TODO" # exclude JavaScript
rg "handle" -A 3 -B 1 # context lines
rg -w "add" # whole word only
rg --smart-case "todo" # case-sensitive when you type caps
rg -P "(?<=def )\w+" # PCRE2: look-around, backreferences
All of the above are real runs from a test project. Type filtering is worth learning on its own: -tpy is shorthand for --type py, and rg --type-list prints every type the tool knows.
By default rg does not search hidden files, and -uuu disables every automatic filter. That flag is how you search inside node_modules or .git when you genuinely need to, which is almost never.
Why it is fast
From the project README, searching the Linux kernel source tree for [A-Z]+_SUSPEND (Intel i9-12900K): ripgrep took 0.082s, The Silver Searcher 0.443s, ack 2.935s. The speed comes from the Rust regex engine with SIMD and literal optimizations, memory-mapped files for single files, and a parallel directory walker. It is not magic, it is engineering choices, and the benchmarks are public.
Use rg over grep unless you need POSIX portability on machines you do not control. grep is everywhere. rg is not.
Part 2: fzf, a fuzzy finder for everything
fzf reads a list from stdin, filters it as you type, and prints your selection to stdout. That description undersells it. It is the piece that turns file picking, history search, and even live grep into an interactive UI.
Install and shell integration
# Debian/Ubuntu (19.10+)
sudo apt install fzf
# macOS
brew install fzf
Installing the binary is not enough. The key bindings are a separate step. For bash, add this line to .bashrc:
eval "$(fzf --bash)"
For zsh: source <(fzf --zsh). Then open a new shell.
The three key bindings
Ctrl-T: paste a file or directory path into your command lineCtrl-R: fuzzy search your shell history, instead of arrow-key archaeologyAlt-C: cd into a subdirectory
Ctrl-R alone is worth the install. You type part of a command you remember, and the fuzzy match handles the rest.
Search syntax
fzf matches fuzzily by default: sbtrkt matches any item containing those letters in order. The extended syntax handles the rest:
^music: starts with music.mp3$: ends with .mp3!fire: does not contain fire'wild: exact match, no fuzzinggo$ | rb$ | py$: OR operator
Verified: printf 'core.go\ncore.rb\ncore.py\nother.txt\n' | fzf --filter="^core go$ | rb$ | py$" returns the three core files.
Preview with bat
The preview window is what makes fzf feel like a real tool rather than a toy:
fzf --preview 'bat --color=always {}' --preview-window '~3'
For big files, limit what bat loads: --line-range=:500.
Live grep with fzf and rg
The fzf README ships a recipe that turns fzf into an interactive rg launcher. Every keystroke restarts the search, and Enter opens the match in vim at the right line:
: | rg_prefix='rg --column --line-number --no-heading --color=always --smart-case' \
fzf --bind 'start:reload:$rg_prefix ""' \
--bind 'change:reload:$rg_prefix {q} || true' \
--bind 'enter:become(vim {1} +{2})' \
--ansi --disabled \
--height=50% --layout=reverse
--disabled tells fzf not to filter the list itself; rg does the filtering. {q} is the current query, {1} and {2} are the first two columns of the matched line, file and line number.
Part 3: bat, cat with eyes
bat is a cat clone with syntax highlighting and git integration. It highlights source files, shows a gutter with line numbers and git modification markers, and pages long output automatically.
Install (and the batcat gotcha)
sudo apt install bat
On Debian and some Ubuntu releases the binary is installed as batcat, because the name bat clashes with another package. I hit this on Ubuntu 24.04, where the README implies it only affects older releases. If bat --version says command not found, run batcat --version and fix it once:
mkdir -p ~/.local/bin
ln -s /usr/bin/batcat ~/.local/bin/bat
On macOS: brew install bat, no drama.
Everyday usage
bat -n src/app.py # line numbers
bat -A /etc/hosts # show non-printable characters
echo '{"a": 1}' | bat -l json # explicit language from stdin
bat --paging=never # behave like cat when you need it
bat auto-detects syntax from file extensions, and from shebangs when reading stdin. When output is piped into another command, it falls back to plain cat behavior, so scripts that use cat keep working if you alias it:
alias cat='bat --paging=never'
Git integration
bat --diff highlights lines relative to the git index. The README wraps it in a function that diffs every changed file at once:
batdiff() {
git diff --name-only --relative --diff-filter=d -z | xargs -0 bat --diff
}
And git show output keeps its highlighting: git show HEAD~1:src/app.py | bat -l py.
bat as a pager
Use bat to render man pages:
export MANPAGER="bat -plman"
man 2 select
Same trick for --help output. The bat README suggests a helper that colorizes help text for any command:
alias bathelp='bat --plain --language=help'
help() {
"$@" --help 2>&1 | bathelp
}
Then help git commit reads like documentation instead of a wall of text.
When to use vs alternatives
- rg vs grep: grep is the right call when you need a tool that exists on every Unix machine, including ones you do not administer. rg is the right call everywhere else.
- bat vs cat: cat stays the standard for scripting, because output must stay plain. bat is for your eyes.
- bat vs delta: delta is specialized for git diffs and does that better. bat covers reading, man pages, help text, and diffs with one binary.
- fzf vs plain history: if your workflow is five commands, Ctrl-R is fine. fzf wins when the list is long enough that scrolling stops working.
Next steps
- Put the aliases and the fzf eval line in your dotfiles, and commit them.
- Try
fzf --bind 'enter:become(vim {})'so selection opens the file directly. - Read the fzf ADVANCED.md for reload patterns and multi-select workflows.
- Add fd (
sudo apt install fd-find) and pointFZF_DEFAULT_COMMANDat it for a faster file list. - Check out bat-extras (batgrep, batdiff, batman) once the core three feel boring.