pexels vovaflame 3673513

fzf: Fuzzy Finding Everything in Your Shell

Most of the time I spend in a shell is not typing commands, it is finding things: the branch I was on yesterday, the file three directories down, the command I ran last Tuesday. fzf is a general-purpose fuzzy finder that turns any list into an interactive, type-to-narrow picker, and once it is wired into your shell you stop navigating by memory altogether.

Installing fzf

macOS

brew install fzf

Linux

sudo apt update && sudo apt install fzf

Installing the binary does not give you the shell key bindings — that is the step people skip, and then they wonder what the fuss is about. On fzf 0.48 and later, add one line to your shell config:

# ~/.zshrc
source <(fzf --zsh)

# ~/.bashrc
eval "$(fzf --bash)"

Older versions ship an install script instead — $(brew --prefix)/opt/fzf/install on macOS, or /usr/share/doc/fzf/examples/ on Debian and Ubuntu. Because the apt package is usually a long way behind, cloning the repo to ~/.fzf and running its installer is a reasonable choice on Linux.

The three keybindings that matter

  • Ctrl-R — fuzzy search your shell history. This alone justifies the install. No more pressing up forty times.
  • Ctrl-T — pick a file or directory and paste its path onto the current command line. Type git add , hit Ctrl-T, select, done.
  • Alt-C — fuzzy cd into any subdirectory.

Inside the picker there are four more worth knowing: Tab multi-selects when the list allows it, Ctrl-/ toggles the preview window, and typing 'exact with a leading quote switches that term to a literal match. A leading ! negates a term, so config !test means “matches config, does not match test”.

Real workflows

fzf reads a list on stdin and writes your choice to stdout. Every useful thing you build with it is that one sentence, repeated. Branch switching is the obvious first one:

gb() {
  git branch --all --sort=-committerdate \
    | sed 's#remotes/origin/##' | grep -v HEAD | awk '{print $NF}' | sort -u \
    | fzf --preview 'git log --oneline --color=always -20 {}' \
    | xargs git switch
}

Most-recently-committed first, a preview of what each branch actually contains, and one keystroke to check it out. If you juggle several branches at once, the same picker works even better over worktrees — see git worktrees versus branches for why that is usually the better model.

The second workflow is killing a process without hunting for a PID:

fkill() {
  ps -eo pid,pcpu,comm,args --sort=-pcpu \
    | fzf --header-lines=1 --multi | awk '{print $1}' | xargs kill "${1:--15}"
}

And the third is live grep: results that re-run the search on every keystroke, with the matching file previewed beside them. --bind 'change:reload' is the feature that makes fzf a front end for other tools rather than just a list picker.

rgf() {
  rg --line-number --no-heading --color=always "${1:-}" \
    | fzf --ansi --delimiter : \
          --bind "change:reload:rg --line-number --no-heading --color=always {q} || true" \
          --preview 'bat --color=always --highlight-line {2} {1}' \
          --preview-window '+{2}/2'
}

Shell integration worth setting

Two environment variables change how everything behaves, so set them once and forget them:

export FZF_DEFAULT_COMMAND='fd --type f --hidden --exclude .git'
export FZF_DEFAULT_OPTS='--height 60% --layout=reverse --border --info=inline --cycle'
export FZF_CTRL_T_OPTS="--preview 'bat --color=always --line-range :200 {}'"
export FZF_ALT_C_COMMAND='fd --type d --hidden --exclude .git'

--height 60% is the important one: it keeps fzf in a pane at the bottom of the terminal instead of taking the whole screen, so you can still see what you were doing. Using fd as the default command means the file picker respects .gitignore and skips node_modules without you asking.

When fzf is not the answer

  • Scripts. Anything interactive has no place in CI or a cron job. Keep fzf in your interactive shell config, guarded by a check that stdin is a terminal if you share dotfiles.
  • Exact, known targets. If you know the path, type the path. Fuzzy matching earns its keep on recall, not on typing speed.
  • Enormous lists. It handles millions of lines, but if you are piping the whole filesystem in, narrow it first with fd and let fzf sort the rest.
  • Shared or ephemeral boxes. Plain Ctrl-R and history | grep still work everywhere.

The verdict

Install it, enable the key bindings, set FZF_DEFAULT_OPTS, and use Ctrl-R for a week. Then start writing small functions that pipe a list into fzf — branches, containers, processes, hosts from your SSH config. That pattern is the real payoff, and it takes about three lines each time.


Leave a Reply