cheat sheet

fzf

Interactive fuzzy finder for the terminal. Covers key bindings, shell integration, previews with bat and tree, multi-select, popup/tmux mode, --listen IPC, and pipelines with git, kill, and vim.

fzf — Fuzzy Finder

What it is

fzf is a general-purpose interactive fuzzy finder for the command line, written in Go by Junegunn Choi and released in 2013. It reads any newline-separated list on stdin — files, branches, processes, history lines — and lets you narrow it down with fuzzy matching while you type, returning the chosen item(s) on stdout. Reach for fzf whenever you'd otherwise pipe to grep | head | awk '{print $1}' and copy-paste the result; for a tighter, file-tree-only experience, fd plus bat's --preview complement it well.

Install

fzf ships as a single static binary, available from every major package manager. The official installer additionally wires up shell key bindings and completion in one step.

bash
# Debian/Ubuntu
sudo apt install fzf

# Fedora
sudo dnf install fzf

# macOS
brew install fzf
$(brew --prefix)/opt/fzf/install   # opt-in: key bindings + completion

# Arch
sudo pacman -S fzf

# From source (latest)
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install

Output: (none — exits 0 on success)

Syntax

fzf reads candidates from stdin (or, when stdin is a TTY, from the default FZF_DEFAULT_COMMAND, typically a file walker like fd or find). It writes the selected line(s) to stdout, making it composable in command substitutions.

bash
<command-producing-lines> | fzf [OPTIONS]
fzf [OPTIONS] < file-of-candidates

Output: (none — exits 0 on success)

Essential options

OptionMeaning
-m, --multiMulti-select (Tab toggles, Shift-Tab deselects)
-q QUERYStart with QUERY pre-filled in the prompt
-1, --select-1Auto-select if only one match remains
-0, --exit-0Exit immediately if no matches
--preview CMDRender CMD (with {} placeholder) in a side pane
--preview-windowPosition/size of preview (e.g. right:60%, up:40%)
--height HEIGHTDisplay in N lines or percent (e.g. 40%) instead of fullscreen
--layout=reversePrompt at top instead of bottom
--ansiHonor ANSI color codes in input
--bind KEY:ACTIONCustom key bindings (see man fzf)
-i / +iForce case-insensitive / case-sensitive matching
--no-sortPreserve input order (useful for history)
--print-queryEcho the typed query on its own line before the result

Fuzzy matching syntax

fzf matches by default using extended fuzzy mode: characters in the query must appear in the candidate in order, but not adjacently. Special prefix and suffix characters tighten or invert the match, letting one query mix fuzzy, exact, and negative terms.

TokenMatch
sbtrktFuzzy: each char appears in order
'wildExact substring wild
^musicPrefix anchor — line starts with music
.mp3$Suffix anchor — line ends with .mp3
!fireNegation — line must NOT contain fire
!^testNegation + prefix — line must NOT start with test
a | b | cOR — line contains any of a, b, c
bash
# Pick a Markdown file but not under node_modules
fd . | fzf -q "'.md !node_modules "

Output:

text
src/content/sections/linux/grep.md

Default key bindings

Inside the picker, the following keys drive selection without leaving the prompt. The most-used are Ctrl-J/K (navigate), Tab (multi-select), and Ctrl-/ (toggle preview pane).

KeyAction
Ctrl-J / Ctrl-N / Move down
Ctrl-K / Ctrl-P / Move up
TabSelect (multi-mode) and move down
Shift-TabDeselect and move up
EnterConfirm
Esc / Ctrl-C / Ctrl-GAbort
Ctrl-/Toggle preview window
Ctrl-A / Ctrl-EBeginning / end of prompt
Ctrl-UClear query
Alt-EnterPrint the typed query (without choosing)
?Show help (custom; not on by default)
bash
fzf

Output:

text
  src/content/sections/linux/grep.md
  src/content/sections/linux/sed.md
> src/content/sections/linux/fzf.md
  3/247 ──────────────
> fzf|

Shell integration (Ctrl-R, Ctrl-T, Alt-C)

The install script (or the matching distro package) drops three key bindings into your shell init: Ctrl-R for fuzzy history search, Ctrl-T to insert a file path at the cursor, and Alt-C to cd into a fuzzy-picked directory. These three alone justify installing fzf.

bash
# Bash
[ -f ~/.fzf.bash ] && source ~/.fzf.bash

# Zsh
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh

# Fish
fzf --fish | source

# After sourcing:
#   Ctrl-R   →  fuzzy history
#   Ctrl-T   →  insert file path
#   Alt-C    →  cd into picked directory

Output: (none — exits 0 on success)

Fuzzy completion (**<Tab>)

After shell integration is loaded, typing ** followed by Tab rewrites the partial argument under the cursor into an fzf picker. It works for files, directories, host names (for ssh), environment variables (for unset / export), processes (for kill), and git branches.

bash
vim **<Tab>            # pick a file
cd **<Tab>             # pick a directory
ssh **<Tab>            # pick a known host
kill -9 **<Tab>        # pick a process (by PID, multi-select)
git checkout **<Tab>   # pick a branch / tag / commit
unset **<Tab>          # pick an env var to unset

Output: (none — exits 0 on success)

--tmux (fzf 0.53+) renders the picker in a tmux popup instead of taking over the current pane, leaving the rest of your tmux layout visible underneath. In fzf 0.71+ the canonical name is --popup and --tmux is kept as an alias; --popup also works inside Zellij 0.44+ as a floating pane. The flag accepts a size spec — center,80%, bottom,40%, 100%,50% — so you can shape the popup without leaving the command line.

bash
# Center popup at 80% width, 60% height (requires tmux 3.3+)
fzf --tmux center,80%,60%

# Same thing, newer spelling (fzf 0.71+)
fd -t f | fzf --popup center,80%,60% \
  --preview 'bat --color=always --line-range :100 {}'

# Bottom-anchored popup with reverse layout
git log --oneline --color=always | \
  fzf --ansi --popup bottom,90%,40% --preview 'git show --color=always {1}'

Output: (none — exits 0 on success)

--listen mode and HTTP/Unix-socket IPC

--listen (fzf 0.42+, expanded in 0.66+) starts an HTTP server alongside the picker so another process can drive it: change the query, reload candidates, accept the highlight, even read back the current selection. Pass a TCP port (--listen 6266) or a Unix-socket path ending in / (fzf 0.66+) for in-host IPC without a network. Pair it with editor plugins, tmux scripts, or status bars that want to push results into a live picker.

bash
# Start fzf as a TCP server on port 6266
fd -t f | fzf --listen 6266 &

# Drive it from anywhere on the same host
curl -X POST localhost:6266 -d 'reload(rg --files)+change-query(test)'
curl localhost:6266                              # GET → current state JSON
curl -X POST localhost:6266 -d 'accept'          # confirm the highlighted item

# Same idea over a Unix socket (fzf 0.66+, trailing slash matters)
fd -t f | fzf --listen /tmp/fzf.sock/ &
curl --unix-socket /tmp/fzf.sock/ http://x/ -d 'change-query(grep)'

Output (curl localhost:6266, abridged):

json
{ "query": "test", "position": 0, "totalCount": 247, "matchCount": 5,
  "current": { "text": "src/content/sections/linux/grep.md" } }

Previews with bat and tree

--preview runs an arbitrary shell command for each highlighted candidate, with {} expanding to the candidate string. Pairing fzf with bat (syntax-highlighted file preview) and tree (directory preview) gives a Midnight-Commander-style file navigator in two lines.

bash
# Preview source files with syntax highlighting + line numbers
fd -t f | fzf \
  --preview 'bat --style=numbers --color=always --line-range :200 {}' \
  --preview-window=right:60%

# Preview a directory listing
fd -t d | fzf --preview 'tree -C {} | head -200'

# Center-line preview with bat
fzf --preview 'bat --color=always --line-range :100 {}' \
    --preview-window 'right:50%:wrap'

Output:

text
src/content/sections/linux/fzf.md

Multi-select (-m)

-m (or --multi) turns fzf into a multi-picker: Tab toggles each highlighted line and Enter returns all selected lines on stdout, one per line. Combine with xargs to apply the same command to every chosen item.

bash
# Open multiple files in vim
vim $(fd -t f | fzf -m)

# Delete several chosen files
fd -t f | fzf -m | xargs -I{} rm -v {}

# Stage multiple files in one shot
git status -s | awk '{print $2}' | fzf -m | xargs git add

Output (fd -t f | fzf -m, two files chosen):

text
src/content/sections/linux/grep.md
src/content/sections/linux/fzf.md

fzf + git

Many of the most common git commands take a ref, file, or stash name as an argument — exactly the shape fzf consumes. Wrapping git log, git branch, and git stash list in fzf eliminates the copy-paste step that usually separates "I see what I want" from "I run the command".

bash
# Pick a branch to check out
git branch --all | fzf | sed 's|^[* ]*remotes/[^/]*/||' | xargs git checkout

# Browse log; press Enter to see the diff
git log --oneline --color=always | \
  fzf --ansi --preview 'git show --color=always {1}'

# Pick a stash to pop
git stash list | fzf | cut -d: -f1 | xargs git stash pop

# Add modified files interactively
git diff --name-only | fzf -m | xargs git add

Output (git branch --all | fzf):

text
  main
  origin/develop
> feature/fuzzy-search

fzf + kill / process selection

Killing a process by name is fragile (pkill matches loosely) and by PID is annoying (you have to look it up first). Piping ps through fzf gives you a searchable picker and returns just the PID for kill to consume.

bash
# Pick a process to kill
ps -ef | sed 1d | fzf -m --header='Pick processes to kill' | \
  awk '{print $2}' | xargs -r kill -9

# Show only your processes
ps -u "$USER" -o pid,pcpu,pmem,comm | sed 1d | \
  fzf --header='PID  %CPU  %MEM  CMD' | awk '{print $1}' | xargs kill

Output (after killing PID 9123):

text
9123

fzf.vim and editor integration

fzf.vim is a companion Vim/Neovim plugin that turns fzf into the editor's file picker, buffer switcher, and :grep UI. :Files, :Buffers, :Rg, and :Lines are the four most-used commands.

vim
" ~/.vimrc — minimal fzf.vim setup
set rtp+=~/.fzf
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
Plug 'junegunn/fzf.vim'

" Key maps
nnoremap <C-p> :Files<CR>
nnoremap <leader>b :Buffers<CR>
nnoremap <leader>g :Rg<CR>
nnoremap <leader>l :Lines<CR>

Output: (none — exits 0 on success)

Custom commands via FZF_DEFAULT_COMMAND

When stdin is a TTY, fzf runs FZF_DEFAULT_COMMAND to populate the candidate list. Setting it to fd (or rg --files) is much faster than fzf's built-in file walker and respects .gitignore by default.

bash
# In ~/.bashrc or ~/.zshrc
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border --info=inline'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
export FZF_ALT_C_COMMAND='fd --type d --hidden --follow --exclude .git'

# Default preview for Ctrl-T
export FZF_CTRL_T_OPTS="--preview 'bat --color=always --line-range :100 {}'"

Output: (none — exits 0 on success)

Common pitfalls

  1. Ctrl-R not working — shell integration is not sourced. Re-run $(brew --prefix)/opt/fzf/install or check ~/.fzf.bash / ~/.fzf.zsh is sourced from your init file.
  2. Slow startup in big repos — the default file walker scans everything. Set FZF_DEFAULT_COMMAND='fd --type f' to use fd and honor .gitignore.
  3. Preview pane blank — the preview command failed silently. Add 2>&1 (e.g. bat ... 2>&1) to surface errors in the pane.
  4. Selected line has trailing whitespace — pipe through awk '{$1=$1};1' or use fzf --tac and slice with awk '{print $N}' to pick a specific column.
  5. fzf -m returns nothing — you forgot to press Tab before Enter. Without a selection, multi-mode confirms only the highlighted item.
  6. fzf chokes on large input — pipe stdin through head -n 100000 or use --no-sort to skip ranking, which is the slow part on huge lists.

Real-world recipes

Persistent dev-session jump-list

A two-line function that jumps to any directory under ~/code with j plus a fuzzy query — a faster, no-database alternative to zoxide when all your projects share one root.

bash
# ~/.bashrc
j() {
  local dir
  dir=$(fd -t d -d 4 . "$HOME/code" | fzf -q "$*" -1) && cd "$dir"
}

Output: (none — exits 0 on success)

Fuzzy man page browser

Pipe the list of installed man pages through fzf with a preview showing the rendered page — useful when you remember roughly what a tool does but not its exact name.

bash
man -k . | fzf --preview 'echo {} | sed "s/ .*//" | xargs -I{} man {} | col -bx' \
              --preview-window=right:70%:wrap \
  | awk '{print $1}' | xargs man

Output: (renders the chosen man page in $PAGER)

text
GREP(1)              General Commands Manual              GREP(1)

NAME
       grep, egrep, fgrep - print lines matching a pattern
...

Interactive apt/brew package picker

Combine the package manager's search output with a preview that runs apt show (or brew info) on the highlighted line, then install everything you Tab-select.

bash
# Debian/Ubuntu
apt list 2>/dev/null | sed '1d;s|/.*||' | \
  fzf -m --preview 'apt show {} 2>/dev/null | head -30' | \
  xargs -r sudo apt install -y

# macOS
brew search '' | fzf -m --preview 'brew info {}' | xargs -r brew install

Output: (none — exits 0 on success)

cd into a recently used git project

Combine git for-each-ref over your ~/code worktrees with fzf to jump to the project root by typing part of its name.

bash
gcd() {
  local repo
  repo=$(fd -H -t d -d 5 '^\.git$' "$HOME/code" | sed 's|/\.git$||' | \
         fzf -q "$*" --preview 'git -C {} log --oneline -n 10') && cd "$repo"
}

Output:

text
/home/alice/code/jockey

Replace history | grep

Ctrl-R already does this once shell integration is loaded, but the manual equivalent is useful in scripts or non-interactive shells where you want the last matching command stamped onto stdout.

bash
HISTTIMEFORMAT='' history | tac | awk '{$1=""; print}' | fzf --no-sort

Output:

text
 git rebase -i HEAD~3

Combine fzf --bind 'enter:become(vim {})' (fzf 0.38+) to make Enter directly launch a command on the chosen item — no $(...) wrapping, no escaping. Run fd -t f | fzf --bind 'enter:become(vim {})'. Other actions like execute(...) keep the picker open after the command finishes.

--tmux (fzf 0.53+) was renamed to --popup in fzf 0.71 — the old name still works as an alias. --popup additionally supports Zellij 0.44+ floating panes, so the same flag opens a popup in either multiplexer.

What's new in recent releases

A condensed survey of features added since fzf 0.50, useful when you skim a CHANGELOG and want to know which release introduced what. Use fzf --version to check your installed build before relying on any of these.

VersionFeatureWhy it matters
0.53--tmux flagNative tmux popup support without the legacy fzf-tmux script
0.55--gap N and --gap-lineVisually separate groups of candidates in the result list
0.55+--style full[:STYLE]One flag swaps in a richer default UI (borders, separators, scroll markers)
0.62--info=inline-rightMatch counts pushed to the right edge of the prompt
0.66Raw mode (--raw, toggle-raw)Show non-matching lines dimmed in original order — context without losing the filter
0.66--listen over Unix socketIPC via --listen /tmp/sock/ (trailing slash) for in-host control
0.66up-match / down-match actionsNew defaults for Ctrl-N/Ctrl-P — jump only through matches
0.67--freeze-left=N, --freeze-right=NPin leading/trailing columns while horizontally scrolling wide rows
0.68--wrap=word, extra underline stylesWord-level wrapping; underline-curly/dotted/dashed/double for richer colors
0.701.3–1.9× faster filteringAlgorithmic speedups, especially noticeable on large candidate sets
0.71--popup (renames --tmux)Same popup flag now works in Zellij 0.44+ as well as tmux 3.3+
0.71Ctrl-R multi-select + shift-deletePick several history lines at once or bulk-delete entries in bash
0.71Linear thread scalingSearch latency scales with available CPU cores via dynamic load balancing
0.72--border=dashed, --border=inlineEmbedded-in-list border style and dashed corner variant
0.73every(N) timer + $FZF_IDLE_TIMEAuto-accept or auto-quit on user inactivity for kiosk/dashboard use
bash
# Quick check: which features your fzf supports
fzf --version

# Try the new richer default look (fzf 0.55+)
fd -t f | fzf --style full --preview 'bat --color=always {}'

# Raw mode keeps un-matched lines visible while dimmed (fzf 0.66+)
fd -t f | fzf --raw -q "grep"

# Pin the first column while scrolling wide rows (fzf 0.67+)
ps -ef | fzf --freeze-left=1 --no-sort

Output:

text
0.71.0 (brew)

Sources