A timesaver: find a file, an old command or a folder from a few letters, and pick from the output of any command, with every key run for real on a sample project.
▶Prefer to watch? This short video explains what fzf is for, then runs it on a real Ubuntu 24.04 machine with a small sample project, with on-screen captions and no voiceover. Click to play, or watch it on YouTube.
fzf: a fuzzy finder. Give it a list, type a few letters, pick a line.
fzf list the files below this folder and narrow them as you type
cmd | fzf pick one line from the output of any command
Ctrl-R Ctrl-T Alt-C search your history, paste a path, jump to a folder
fzf -f WORD no screen: print the matches and exit, for scripts
fzf is a program that reads a list of lines, shows them, and narrows them as you type. The matching is "fuzzy": your letters only have to appear in order, with gaps, so bnote finds docs/billing-notes.md. It prints the line you pick, which makes it a building block for the shell. With one line added to ~/.bashrc it also gives you three key bindings: Ctrl-R searches your command history, Ctrl-T pastes a file path into the command line, and Alt-C changes into a folder you pick.
This page is the written version of the Timesavers video. Every command and key below was run for real, in an Ubuntu 24.04 container with fzf 0.74.5 and bash, on a small made-up project (18 files) and a made-up history of 20 commands. The screens shown are captures of the terminal at one moment, and a page cannot show the list changing as you type, so watch the video for that. The section "What was not run" lists what was not tried.
What fzf is for
Use fzf when you know a little of what you want and the list is long:
a file in a big project a few letters of the name or the folders on its path
a command you ran last week Ctrl-R, and a word of it
a folder deep in a tree Alt-C, and a word of it
one line out of any list git branches, running processes, containers, lines of a file
It does not replace find or grep: it filters a list that you already have, and shows the choice for you to make. The matching is by characters, not by meaning. Its power is that it is generic: anything that prints lines can be piped into it, and what you pick comes out on standard output.
Install
Ubuntu packages fzf, but the version in 24.04 is old (0.44.1 in the archive the lab used) and it has no --bash option for the shell integration. The project's GitHub releases have the current one. The lab downloaded the Linux (x86_64) archive and the checksums file, checked the checksum, and unpacked the program into ~/.local/bin:
$ B=https://github.com/junegunn/fzf/releases/download/v0.74.5
$ curl -fsSLO $B/fzf-0.74.5-linux_amd64.tar.gz
$ curl -fsSLO $B/fzf_0.74.5_checksums.txt
$ grep linux_amd64 fzf_0.74.5_checksums.txt | sha256sum -c -
fzf-0.74.5-linux_amd64.tar.gz: OK
$ tar xzf fzf-0.74.5-linux_amd64.tar.gz -C ~/.local/bin
$ fzf --version
0.74.5 (c0a193e0)
The Ubuntu package route is sudo apt install fzf. What the lab found in that package (version 0.44.1): it has no --bash option (the lab ran it and got "unknown option: --bash"), and it ships the key bindings as example files instead, in /usr/share/doc/fzf/examples:
$ ls /usr/share/doc/fzf/examples
completion.zsh fzf.vim key-bindings.bash key-bindings.fish key-bindings.zsh plugin
With that package you source key-bindings.bash from ~/.bashrc yourself (the package was not installed in the lab, only unpacked, so that line was not tried). The rest of this page uses 0.74.5.
Shell integration: the three key bindings
One line in ~/.bashrc turns on Ctrl-R, Ctrl-T, Alt-C and the ** completion. The lab's ~/.bashrc ended with these three lines (the middle one is an option used for the video and not needed):
$ tail -3 ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
export FZF_DEFAULT_OPTS="--height=60% --reverse"
eval "$(fzf --bash)"
FZF_DEFAULT_OPTS holds options that fzf uses every time. --height=60% makes it draw in the lower 60 percent of the terminal instead of taking the whole screen, so what you ran stays visible, and --reverse puts the prompt at the top. For zsh and fish the project's README has the equivalent lines (not run). Open a new shell for the line to take effect.
The sample project
A small project called work: 18 files in a few folders, built for this page. This is what fzf sees:
$ cd ~/work
$ find . -type f | sort | sed 's|^\./||'
.gitignore
README.md
config/logging.toml
config/settings.toml
docs/auth-design.md
docs/billing-notes.md
docs/setup.md
scripts/backup.sh
scripts/deploy.sh
src/auth/login.py
src/auth/token.py
src/billing/invoice.py
src/billing/tax.py
src/main.py
src/utils.py
tests/test_invoice.py
tests/test_login.py
tests/test_tax.py
Run on its own with no input, fzf lists the files below the current folder. The lab's screen included .gitignore, so hidden files are listed. What else it leaves out by default was not tested.
Type a few letters
fzf by itself opens the list. This is the screen before typing (the first rows):
$ fzf
ubuntu@timesaver:~/work$ fzf
>
18/18 ───────────────────────────────────────────────────────────────────────────────────────────
▌ .gitignore │
▌ README.md │
▌ src/main.py │
▌ src/utils.py │
▌ src/billing/invoice.py │
▌ src/billing/tax.py │
▌ scripts/backup.sh │
▌ scripts/deploy.sh │
▌ docs/auth-design.md
The line with 18/18 says how many lines match out of how many there are. Typing narrows the list at once. The letters must appear in order, but not next to each other. invo leaves the two invoice files, and bnote, which skips most of the name, finds the notes about billing:
keys: invo
ubuntu@timesaver:~/work$ fzf
> invo
2/18 ────────────────────────────────────────────────────────────────────────────────────────────
▌ src/billing/invoice.py
▌ tests/test_invoice.py
keys: bnote
ubuntu@timesaver:~/work$ fzf
> bnote
1/18 ────────────────────────────────────────────────────────────────────────────────────────────
▌ docs/billing-notes.md
Enter prints the highlighted line and leaves fzf, and Esc or Ctrl-C leaves without printing. The up and down arrows (or Ctrl-P and Ctrl-N) move the highlight. The list is sorted by how well each line matches, so the best match is first.
Search words
A query can have several words, separated by spaces, and every word must match. Each word can carry a symbol (this is called extended search, and it is on by default):
^src .py$ ^ means the line starts with, $ means it ends with
billing !test ! leaves out lines that match the word
'main a leading quote means an exact match, not fuzzy
a | b either one (an OR, with spaces around the bar)
The lab ran three of them (the screens below are captures at one moment). ^src .py$ is a file under src that ends in .py:
keys: ^src .py$
ubuntu@timesaver:~/work$ fzf
> ^src .py$
6/18 ────────────────────────────────────────────────────────────────────────────────────────────
▌ src/main.py
▌ src/utils.py
▌ src/auth/login.py
▌ src/auth/token.py
▌ src/billing/tax.py
▌ src/billing/invoice.py
billing !test is billing without test (the test files are gone):
keys: billing !test
ubuntu@timesaver:~/work$ fzf
> billing !test
3/18 ────────────────────────────────────────────────────────────────────────────────────────────
▌ docs/billing-notes.md
▌ src/billing/tax.py
▌ src/billing/invoice.py
'main is the exact text main, and finds one file:
keys: 'main then Enter
ubuntu@timesaver:~/work$ fzf
> 'main
1/18 ────────────────────────────────────────────────────────────────────────────────────────────
▌ src/main.py
ubuntu@timesaver:~/work$ fzf
src/main.py
ubuntu@timesaver:~/work$
The OR form with a bar was not run. -e (--exact) makes every word exact. By default fzf uses smart case: a query in lowercase ignores case, and a query with a capital letter is case sensitive. In the lab fzf -f TAX found nothing and exited with status 1, while fzf -i -f TAX (the -i turns it off) found the two tax files:
$ fzf -f TAX; echo "exit status: $?"
exit status: 1
$ fzf -i -f TAX
src/billing/tax.py
tests/test_tax.py
No screen: filter mode and exit codes
-f (--filter) does the matching without the screen. It prints the matching lines, best first, and exits. It is meant for scripts and for checking a query before you use it:
$ fzf -f tax
src/billing/tax.py
tests/test_tax.py
$ fzf -f '^docs'
docs/setup.md
docs/auth-design.md
docs/billing-notes.md
fzf's exit status says what happened: 0 for a match, 1 for no match (the lab saw 1 for fzf -f zzz and for fzf -e -f tx), and 130 when you leave with Esc or Ctrl-C (the lab ran both and printed 130 each time). Two more options help in scripts. -1 (--select-1) takes the single match without asking, and -0 (--exit-0) leaves at once when there is nothing to pick. -q gives a starting query:
$ fzf -q billing/inv -1; echo "exit status: $?"
src/billing/invoice.py
exit status: 0
$ fzf -q zzz -0; echo "exit status: $?"
exit status: 1
The first printed src/billing/invoice.py with no screen, because one line matched, and the second left with status 1.
Ctrl-R: search your history
With the shell integration, Ctrl-R opens fzf on your command history, newest first. Type a word, move to the line you want, and Enter puts it on the command line (it does not run it, so you can edit it first). The lab's history was a made-up list of 20 commands:
$ cat ~/.bash_history
cd ~/work
git status
python3 src/main.py
pytest tests/test_login.py
docker ps
docker logs web --tail 50
ssh deploy@web1.example.com
git log --oneline | head
sudo systemctl status nginx
sudo systemctl restart nginx
journalctl -u nginx --since "10 min ago"
tail -f /var/log/nginx/error.log
grep -rn "TODO" src/
df -h
du -sh ~/work
rsync -av docs/ backup:/srv/docs/
kubectl get pods -n shop
kubectl logs deploy/api -n shop
curl -s localhost:8080/health
ls -la
keys: Ctrl-R, then nginx
> nginx
4/28 (0) +S ─────────────────────────────────────────────────────────────────────────────────────
▌ 12 tail -f /var/log/nginx/error.log
▌ 11 journalctl -u nginx --since "10 min ago"
▌ 10 sudo systemctl restart nginx
▌ 9 sudo systemctl status nginx
keys: Down, Enter
ubuntu@timesaver:~/work$ journalctl -u nginx --since "10 min ago"
Four nginx commands matched (the 4/28 counts the 20 old lines and the commands run in that session), and the second one down was put on the prompt. Press Enter once more to run it, or edit it first. This is a far better history search than the plain Ctrl-R of the shell, because the whole list is visible and the words can be in any order.
Ctrl-T: paste a path
Ctrl-T opens a file picker and pastes what you choose onto the command line, wherever the cursor is. Type the start of a command, press Ctrl-T, find the file, and Enter:
keys: cat (then Ctrl-T) tax
> tax
2/25 (0) ────────────────────────────────────────────────────────────────────────────────────────
▌ src/billing/tax.py
▌ tests/test_tax.py
keys: Enter
ubuntu@timesaver:~/work$ cat src/billing/tax.py
keys: Enter (to run it)
ubuntu@timesaver:~/work$ cat src/billing/tax.py
RATE = 0.19
def with_tax(amount):
return round(amount * (1 + RATE), 2)
ubuntu@timesaver:~/work$
Marking several with Tab pastes all of them (not run). A related feature is the ** completion: type a command and a path ending in two stars, press Tab, and fzf opens on that folder:
keys: cat src/** then Tab
ubuntu@timesaver:~/work$ cat src/**
>
9/9 (0) ───────────────────────────────────────────────────────────────────────────────────────
▌ src/
▌ src/main.py
▌ src/utils.py
▌ src/billing/
▌ src/auth/
▌ src/billing/invoice.py
▌ src/billing/tax.py
▌ src/auth/login.py
▌ src/auth/token.py
(The status line of that screen, 9/9 (0), counts the entries below src.)
Alt-C: jump to a folder
Alt-C lists the folders below the current one, and changes into the one you pick. Type a few letters and Enter:
keys: Alt-C bill Enter
ubuntu@timesaver:~/work$ `__fzf_cd__`
> bill
1/7 ─────────────────────────────────────────────────────────────────────────────────────────────
▌ src/billing/
ubuntu@timesaver:~/work$ builtin cd -- /home/ubuntu/work/src/billing
ubuntu@timesaver:~/work/src/billing$
The shell showed the commands it ran for you (the line starting with builtin cd), and the prompt is now in ~/work/src/billing. Alt-C reaches the shell as Esc followed by c, and that is how the lab sent it. If your terminal uses Alt for something else, Esc then c should do the same (not run on a desktop terminal).
Options at a glance
A selection of options of this version, from fzf --help:
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--ansi Enable processing of ANSI color codes
--height=[~][-]HEIGHT[%] Display fzf window below the cursor with the given
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border[=STYLE] Draw border around the finder
--border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
--cycle Enable cyclic scroll
--tac Reverse the order of the input
--prompt=STR Input prompt (default: '> ')
--preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%)
--preview-border[=STYLE] Short for --preview-window=border-STYLE
--preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos,
--preview-wrap-sign=STR Indicator for wrapped lines in the preview window
--header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line
--header-border[=STYLE] Draw border around the header section
--header-lines-border[=STYLE]
--header-label=LABEL Label to print on the header border
--header-label-pos=COL Position of the header label
-q, --query=STR Start the finder with the given query
--expect=KEYS Comma-separated list of keys to complete fzf
--bind=BINDINGS Custom key/event bindings
Most of these were not run. The ones that were are in the sections above: -f, -e, -i, -m, -q, -1, -0, --preview, --height, --reverse and --print-query (the last, with -f, printed the query first and then the matches).
What was not run in the lab
Only the keys and commands above were run, in bash, on a sample project of 18 files and a history of 20 commands. These were not: big inputs (the speed on a million lines), zsh and fish, the OR form with a bar, -e on the interactive screen, --bind (your own keys), --expect, --header, --border and --layout other than reverse, the window layout of --preview-window, FZF_DEFAULT_COMMAND (a custom file lister such as fd), using fzf from a script or with vim, tmux integration (fzf-tmux), Windows, macOS, and installing the Ubuntu package (it was only unpacked to look inside). The checksum of the release archive was checked. Everything ran on Ubuntu 24.04 with fzf 0.74.5.
Common problems and tips
Ctrl-R, Ctrl-T or Alt-C do nothing. The line eval "$(fzf --bash)" is missing from ~/.bashrc, or you have not opened a new shell, or fzf is not on the PATH when that line runs. With the Ubuntu package source key-bindings.bash instead.
fzf: unknown option: --bash. The version is older than the one that has it. The Ubuntu 24.04 package is one of those.
My lowercase search works but a search with a capital finds nothing. That is smart case. Use -i, or type the query in lowercase.
Alt-C does nothing in my terminal. Some terminals use Alt for something else. Try Esc then c, or change the terminal setting.
I see a list that is not my files. With no input fzf walks the current folder. Piping a command into it, as in ps -ef | fzf, replaces the list.
Tips. Put FZF_DEFAULT_OPTS in ~/.bashrc with --height and --reverse so fzf does not take over the screen. Use --preview when the name does not tell you enough. Use -m when you want more than one. And try fzf -f on a query first when you plan to use fzf in a script.