TechEarl

fzf: Fuzzy-Find Anything in Your Shell

fzf is a fuzzy finder that turns any line-based list into an interactive picker. Install it, pipe a list into it, and select with type-as-you-go matching.

Ishan Karunaratne⏱️ 9 min readUpdated
Share thisCopied
fzf is a command-line fuzzy finder for the shell. Pipe any list into fzf for interactive type-to-filter selection, with Ctrl-T, Ctrl-R, and Alt-C key bindings, ripgrep and fd integration, and a preview window.

fzf is a general-purpose command-line fuzzy finder: pipe any line-based list into it, type a few characters, and it interactively filters the list down to the lines that match. Pick one with the arrow keys and Enter, and fzf writes your selection to standard output. That is the whole idea. It does not know what your lines mean, so it works on filenames, git branches, process IDs, log lines, anything that comes one-per-line.

The simplest possible use is fzf on its own, which finds files under the current directory and prints the one you pick:

bash
fzf

The more useful pattern is putting fzf in the middle of a pipe. Feed it a list on stdin, filter interactively, and pipe the chosen line onward:

bash
git branch | fzf

Everything else on this page is variations on that: the shell key bindings it installs (Ctrl-T, Ctrl-R, Alt-C), the preview window, and wiring it up with fd and ripgrep so the file search skips junk.

Install fzf

The fastest path is your package manager.

bash
# Debian / Ubuntu
sudo apt install fzf

# Fedora
sudo dnf install fzf

# Arch
sudo pacman -S fzf

# macOS (Homebrew)
brew install fzf

Or clone it from GitHub, which is what I do on servers where the packaged version lags behind:

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

The install script offers to set up the shell key bindings and completion for bash, zsh, and fish. Say yes; the bindings are most of what makes fzf worth having. On a Homebrew install run $(brew --prefix)/opt/fzf/install to get the same setup. Check the version with fzf --version.

The core idea: it filters stdin to stdout

fzf reads lines from standard input, lets you filter them interactively, and writes the selected line(s) to standard output. So it slots into a pipe between "produce a list" and "do something with the chosen item".

bash
# pick a running process and kill it
ps -ef | fzf | awk '{print $2}' | xargs kill

# cd into any subdirectory, fuzzy-selected
cd "$(find . -type d | fzf)"

# check out a git branch without typing the name
git branch | fzf | tr -d ' *' | xargs git checkout

The matching is "fuzzy": the characters you type have to appear in order, but not adjacently. Typing srctmain matches src/components/Main.tsx. That is why it feels fast: you type the distinctive characters and skip the rest. A leading ' makes a term an exact match ('main matches the literal substring), ^ anchors to the start, $ to the end, and a leading ! negates. Space-separated terms are ANDed together.

Inside the picker, move the highlight with the arrow keys or Ctrl-J / Ctrl-K (down/up), press Enter to take the highlighted line, and Esc or Ctrl-G to abort without selecting anything. Those work the same whether you launched fzf yourself or it popped up from a key binding.

The shell key bindings

If you let the installer add the key bindings, you get three that fire inside any command line you are typing.

BindingWhat it does
Ctrl-TFuzzy-find files and directories, paste the selection onto the command line
Ctrl-RFuzzy-search your shell command history, paste the chosen command
Alt-CFuzzy-find a directory and cd into it

Ctrl-R is the one that changes how you use the shell. The default history search (Ctrl-R in plain bash/readline) walks backward one match at a time; fzf's version shows you all matches at once and filters as you type. I reach for it constantly to dig out a long command I ran last week without remembering the exact flags.

Ctrl-T is for building a command incrementally: type vim , hit Ctrl-T, pick the file, and the path lands on the line. Alt-C is the quick "jump somewhere deep in the tree" without typing the path. On macOS the Alt key is Option, and you may need to enable "Use Option as Meta key" in your terminal for Alt-C to register.

Preview window

fzf can run a command for the highlighted line and show the output in a side panel. This is what turns it from a picker into something you can actually inspect before committing.

bash
fzf --preview 'cat {}'

The {} is a placeholder fzf replaces with the current line. For files, cat is the obvious preview, but bat (syntax-highlighted) is nicer if you have it:

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

You can preview anything. For a git branch picker, preview the recent commits on each branch:

bash
git branch | tr -d ' *' | fzf --preview 'git log --oneline -20 {}'

Resize and reposition the panel with --preview-window, for example --preview-window=right:60% or --preview-window=up:40%.

Better default source: fd and ripgrep

By default Ctrl-T and bare fzf use find to build the file list, which means they crawl .git, node_modules, and every other heavy directory. The fix is to point fzf at fd (or ripgrep), which honor .gitignore automatically. Set the default command in your shell rc:

bash
export FZF_DEFAULT_COMMAND='fd --type f --hidden --exclude .git'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
export FZF_ALT_C_COMMAND='fd --type d --hidden --exclude .git'

Now the file pickers skip ignored files for free, which on a real project is the difference between a fast list and one drowning in build artifacts. ripgrep works as the source too (rg --files) if you already have it and not fd. This is the same gitignore-aware behavior that makes rg worth using over grep -r; the grep cheat sheet covers that side of it.

fzf vs the tools it replaces

fzf does not replace find, grep, or readline's history search outright. It sits in front of them as the interactive selection layer.

ToolWhat it does wellWhere fzf fits
find / fdBuild the file list by criteria (name, type, time)fzf filters that list interactively; find still does the selection logic
grep -rnSearch file contents non-interactively, scriptablyfzf can drive an interactive grep, but scripts want grep directly
readline Ctrl-RIncremental reverse history search, one match at a timefzf's Ctrl-R shows all matches and filters live
select (bash builtin)Menu from a fixed short list in a scriptfzf scales to thousands of lines and adds typing

The rule I use: if a human is choosing from a list, fzf. If a script is deciding from a list, the underlying tool (find, grep, awk) directly, because fzf is interactive and needs a TTY.

Caveats and gotchas

It needs a terminal. fzf is interactive, so it expects a TTY on stdout. Inside a cron job, a CI step, or a non-interactive pipe with no terminal, it will not behave the way you want. For non-interactive filtering use fzf --filter='query' (or the short -f), which prints all matches sorted by score and exits without prompting.

Multi-select needs a flag. A plain fzf returns exactly one line. Add -m (--multi) and you can mark several with Tab, then Enter to return all of them. Easy to forget when you pipe the output into xargs expecting more than one item.

The Alt key on macOS. Alt-C and the Alt- bindings depend on your terminal sending a Meta key. iTerm2 and Terminal.app both need "Option as Meta" turned on, or the binding silently does nothing.

Old packaged versions. Distro packages can trail the GitHub release by a lot, and some --preview-window and binding syntax is newer. If a documented option errors, check fzf --version against the README before assuming you typed it wrong.

Quoting in --preview. The {} placeholder is shell-quoted by fzf, so it survives spaces in filenames, but anything else in the preview command is your responsibility to quote correctly.

See also

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsfzfFuzzy FinderCLILinuxShell ScriptingDevOpsripgrep

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Find Duplicate Rows in MySQL

Find duplicate rows in MySQL with GROUP BY HAVING, a ROW_NUMBER window function, or a self-join. Includes NULL behaviour, soft duplicates, and the right index.