Hi everyone, welcome back. Today, I will be going over fzf — a fuzzy finder that turns any list of text into something you search interactively.

The project lives here: github.com/junegunn/fzf

I installed it and verified every matching example below, so the outputs are real.

I'll write this for someone comfortable in a terminal who hasn't used fzf.

None
Photo by Carlos Perez on Unsplash

The Problem

You know a command exists in your history. You typed it three weeks ago. It involved ffmpeg and some flags you'll never remember.

So you do this:

history | grep ffmpeg
# 40 lines scroll past
history | grep ffmpeg | grep scale
# 12 lines
# squint, find the right one, copy it

Or you're looking for a file and you know part of the name:

find . -name "*user*"
# 60 results
find . -name "*user*" -path "*handler*"
# still not right

The pattern is the same both times: you have a list, you know roughly what you want, and you're refining a search from the outside — running the command again with a better filter each time.

That's backwards. You should be able to type into the list and watch it narrow.

What fzf Is

fzf is a general-purpose fuzzy finder. You pipe a list of lines into it, it shows them with a search box, you type, the list filters as you type, you pick one, and it prints your selection.

That's it. And that description is deliberately abstract, because fzf doesn't know or care what the lines are. Filenames, git branches, running processes, shell history, Docker containers, Kubernetes pods, lines from a log file — it's all just text.

The key architectural fact: fzf reads from standard input and writes to standard output. It's a Unix filter, which means it composes with everything.

some_command_producing_a_list | fzf

That's the entire interface. Everything else is a variation on it.

Fuzzy Matching, Demonstrated

"Fuzzy" means your typed characters must appear in order, but not next to each other.

printf "apple pie\nbanana split\ncherry tart\napricot jam\n" | fzf --filter="ap"
apple pie
apricot jam
banana split

All three matched ap. "banana split" made the list because it contains an a and, later, a p.

That flexibility is the point when searching paths:

printf "src/handlers/user.go\nsrc/models/user.go\ntest/user_test.go\n" | fzf --filter="shu"
src/handlers/user.go

Three characters — s, h, u — picked one file out of three by matching across directory boundaries. That's the everyday use: type a few characters from the path and the folder and get there.

The Search Syntax

When fuzzy is too loose, fzf has operators:

Exact substring with a single quote prefix:

printf "apple\npineapple\ngrape\n" | fzf --filter="'apple"
apple
pineapple

Exclusion with !:

printf "main.go\nmain_test.go\nutil.go\nutil_test.go\n" | fzf --filter=".go !test"
main.go
util.go

Two terms, space-separated, and they combine — match .go, exclude anything containing test.

The full set:

  • sbtrkt — fuzzy match
  • 'word — exact match
  • ^prefix — starts with
  • suffix$ — ends with
  • !word — does not contain
  • word1 | word2 — either

Why it works: you start with fuzzy, and when it returns too much you add one operator. You're never forced to learn regex to narrow a list.

Installing It

macOS:

brew install fzf

Windows:

winget install fzf
scoop install fzf

Linux:

# Arch
sudo pacman -S fzf

# Debian/Ubuntu
sudo apt install fzf

One caution about distribution packages. The version in Ubuntu's repositories is often well behind — the one I got from apt was 0.44.1 while the current release is in the 0.6x line. Several features and default behaviors changed in between, so tutorials may not match. If something in the docs doesn't work, check your version first.

For the current release, use the official installer:

git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install

That script also sets up the shell integration, which is the next section and the most important part.

Verify it worked:

fzf --version

The Shell Keybindings — Start Here

Installing fzf gives you three keyboard shortcuts. These alone justify the tool.

Add this to your ~/.bashrc or ~/.zshrc if the installer didn't:

eval "$(fzf --bash)"    # or --zsh

Ctrl+R — search your shell history.

This replaces bash's default reverse-search, which only steps backward through exact matches. Now you press Ctrl+R, type ffmpeg scale, and see every matching command interactively.

This is the one that changes your habits. Most people's shell history is thousands of commands they can't reach.

Ctrl+T — insert a file path.

You're mid-command and need a path. Press Ctrl+T, fuzzy-find the file, and it's inserted at your cursor. No ls, no tab-completing through five directories.

vim <Ctrl+T>        # find the file, it appears in the command

Alt+C — cd into a directory.

Fuzzy-find a subdirectory and change into it.

Why it works: these three cover the actions you perform most often in a shell, and each one replaces a multi-step process with typing a few characters.

The Preview Window

fzf can run a command against whatever line is currently highlighted and show the output beside the list.

fzf --preview 'cat {}'

{} is replaced with the current selection. With syntax highlighting:

fzf --preview 'bat --color=always {}'

Why it matters: this turns fzf from a picker into a browser. You're not just choosing a filename from a list — you're reading each file's contents as you move through candidates.

Where It Gets Genuinely Useful

The keybindings are the on-ramp. Composing fzf with other commands is where it pays off.

Check out a git branch:

git branch | fzf | xargs git checkout

Kill a process:

ps aux | fzf | awk '{print $2}' | xargs kill

Search file contents, preview the match, open in your editor:

rg --line-number "" . | fzf --delimiter : \
  --preview 'bat --color=always --highlight-line {2} {1}' \
  | awk -F: '{print $1}' | xargs $EDITOR

Check out a git commit:

git log --oneline | fzf | awk '{print $1}' | xargs git checkout

Pick a Docker container to shell into:

docker ps --format '{{.Names}}' | fzf | xargs -I {} docker exec -it {} sh

Search your environment variables:

env | fzf

Notice the shape. Every one is: produce a list, pipe to fzf, do something with the choice. Once you see that pattern, you write your own without looking anything up.

Turn the useful ones into functions:

fbr() {
  git branch --all | grep -v HEAD | fzf | sed 's/.* //;s#remotes/[^/]*/##' \
    | xargs git checkout
}

Options Worth Knowing

  • --height 40% --layout reverse — a compact window below your prompt rather than fullscreen
  • -m / --multi — select several with Tab, then act on all of them
  • --preview — the preview command
  • --query — start with a search term already typed
  • -1 — auto-select if exactly one match
  • -0 — exit immediately if nothing matches
  • --filter — non-interactive mode, used for every example above

Set your defaults once:

export FZF_DEFAULT_OPTS="--height 40% --layout reverse --border"
export FZF_DEFAULT_COMMAND="fd --type f --hidden --exclude .git"

That second one makes fzf use fd instead of find, which is faster and respects your .gitignore.

When It Isn't the Right Tool

In scripts that run unattended. fzf is interactive. --filter gives you non-interactive matching, but at that point grep or rg is usually clearer.

For exact structured queries. If you know precisely what you want and it's a well-defined pattern, grep is more direct. fzf is for when you know roughly what you want.

On enormous lists without a fast source. fzf handles millions of lines, but if generating the list is slow, that's your bottleneck. Use fd or rg to produce it.

Alternatives worth knowing: skim (sk) is a Rust implementation with similar syntax. peco is a simpler Go alternative. fzf has by far the largest ecosystem of integrations.

Conclusion

This concludes my article on fzf. The short version: install it, enable the shell integration, and start with Ctrl+R for history, Ctrl+T for file paths, and Alt+C for directories. When you want more, remember the shape — produce a list, pipe it to fzf, act on the result.

What makes fzf unusual among the tools I've written about is that it isn't really an application. lazygit is for Git, btop is for system monitoring, k9s is for Kubernetes. fzf is for lists, which means it shows up everywhere — inside other tools, inside your shell, inside scripts you write this afternoon.

That generality comes from a design decision that looks almost too simple: read lines from standard input, write the chosen line to standard output. Nothing about files, nothing about git, nothing about any specific domain. Because it refused to know what your data is, it works with all of it.

That's the Unix philosophy actually paying off, decades later, in a tool nobody could have anticipated. Worth remembering when you're tempted to build something that knows too much about its input.

I hope this helps. If you have any questions or comments, please let me know. Thanks for reading!