fzf is a filter¶
Level: 101 · you have piped a list into fzf and picked a line, and now want the same answer inside a script
One line: fzf --filter QUERY is fzf's matcher without the screen. It reads lines on stdin and writes the ones that match to stdout, best match first. It exits 0 when something matched, 1 when nothing did, and 2 on an error, so it can sit in an if the way grep does. It still reads FZF_DEFAULT_OPTS, so a --exact exported in someone's shell changes what a script's --filter finds.
Measured¶
demo/files.txt holds seven paths. rdme is not a substring of any of them, and it still matches two:
Verified output of fzfilter_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ cat files.txt
src/main.go
src/main_test.go
docs/README.md
core/app.py
core/app.rb
Makefile
README.md
$ fzf --filter rdme < files.txt; echo "status $?"
README.md
docs/README.md
status 0
$ fzf --filter rdme --no-sort < files.txt
docs/README.md
README.md
$ fzf --filter zzz < files.txt; echo "status $?"
status 1
$ fzf --filter "" < files.txt
src/main.go
src/main_test.go
docs/README.md
core/app.py
core/app.rb
Makefile
README.md
$ fzf --filter rdme --tiebreak=nope < files.txt; echo "status $?"
invalid sort criterion: nope
status 2
$ fzf --query makef --select-1 --exit-0 < files.txt; echo "status $?"
Makefile
status 0
$ fzf --query zzz --select-1 --exit-0 < files.txt; echo "status $?"
status 1
$ fzf --filter mngo < files.txt; echo "status $?"
src/main.go
src/main_test.go
status 0
$ FZF_DEFAULT_OPTS=--exact fzf --filter mngo < files.txt; echo "status $?"
status 1
$ FZF_DEFAULT_OPTS=--bogus fzf --filter mngo < files.txt; echo "status $?"
$FZF_DEFAULT_OPTS: unknown option: --bogus
status 2
$ cat fzfrc; FZF_DEFAULT_OPTS_FILE=fzfrc fzf --filter mngo < files.txt; echo "status $?"
--exact
status 1
$ export FZF_DEFAULT_OPTS=--exact; FZF_DEFAULT_OPTS= FZF_DEFAULT_OPTS_FILE= fzf --filter mngo < files.txt; echo "status $?"; unset FZF_DEFAULT_OPTS
src/main.go
src/main_test.go
status 0
$ q="main go"; fzf --filter $q < files.txt; echo "status $?"
unknown option: go
status 2
$ q="main go"; fzf --filter "$q" < files.txt; echo "status $?"
src/main.go
src/main_test.go
status 0
- Lines in, lines out, nothing else.
rdmematchedREADME.mdanddocs/README.mdbecauser,d,mandeoccur in them in that order; Fuzzy means in order measures that rule. fzf printed no prompt, no count and no colour, so its output can go straight into the next command. - Best first, unless you ask for the file's order. The shorter
README.mdcame first.--no-sortprinted the same two lines in the order of the file, which turnsfzf --filterinto a fuzzygrep. An empty query matched all seven lines and kept their order. - The exit status is the answer. It is 0 for a match and 1 for none. It is 2 when fzf rejects its own arguments:
--tiebreak=nopeprinted a message and exited 2. The manual lists one more status, 130 for Ctrl-C or Esc. That can only happen on the screen, so it is not measured here. --select-1and--exit-0are not--filter. They belong to the interactive finder.--query makef --select-1printed its only match and exited 0 without drawing anything, and--query zzz --exit-0exited 1. The manual promises that only for exactly one match or for none, so a script that must never wait for a keypress uses--filter.FZF_DEFAULT_OPTSreaches--filtertoo.mngofound two files, then found nothing once--exactarrived through the variable. A bad option in the variable fails the run with status 2, and the message names$FZF_DEFAULT_OPTSrather than the command line.FZF_DEFAULT_OPTS_FILEdoes the same from a file. Setting both to empty for the one command, as theexportline does, restores fzf's built-in defaults whatever the shell has exported.- An unquoted
$qis two arguments in bash.q="main go"; fzf --filter $qhands fzf--filter main go, and fzf rejects the straygowithunknown option: goand status 2."$q"is one argument, and inside the query the space means "and" (the extended search syntax).
On a Mac¶
Nothing changes. The key above is shared: /bin/bash 3.2 printed the same bytes as bash 5.2 in the Linux image. fzf is the same 0.67.0 release on both machines, so the ranking and every error message agree. The one line that does differ is fzf --version, which prints 0.67.0 (Homebrew) on this Mac and 0.67.0 (2ab923f3) in the Linux image. That is why no example prints it.
In zsh and fish¶
zsh runs the same commands and gets the same answers, except when the query is in a variable:
Verified output of fzfilter_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ fzf --filter rdme < files.txt; echo "status $?"
README.md
docs/README.md
status 0
$ fzf --filter zzz < files.txt || echo "no match, status $?"
no match, status 1
$ FZF_DEFAULT_OPTS=--exact fzf --filter mngo < files.txt; echo "status $?"
status 1
$ q="main go"; fzf --filter $q < files.txt; echo "status $?"
src/main.go
src/main_test.go
status 0
$ q="main go"; fzf --filter ${=q} < files.txt; echo "status $?"
unknown option: go
status 2
zsh does not split an unquoted parameter into words, because its SH_WORD_SPLIT option is off. So $q holding main go reached fzf as one argument and matched. ${=q} asks for the split, and it reproduces bash's unknown option: go.
fish spells the status $status, and still accepts a VAR=value prefix on one command:
Verified output of fzfilter_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ fzf --filter rdme < files.txt; echo "status $status"
README.md
docs/README.md
status 0
$ fzf --filter zzz < files.txt; or echo "no match, status $status"
no match, status 1
$ FZF_DEFAULT_OPTS=--exact fzf --filter mngo < files.txt; echo "status $status"
status 1
$ set -gx FZF_DEFAULT_OPTS --exact; fzf --filter mngo < files.txt; echo "status $status"; set -e FZF_DEFAULT_OPTS
status 1
$ set q "main go"; fzf --filter $q < files.txt; echo "status $status"
src/main.go
src/main_test.go
status 0
$ set q main go; fzf --filter $q < files.txt; echo "status $status"
unknown option: go
status 2
fish never splits a variable on spaces either: set q "main go" is one element, so it is one argument, and it matched. What fish splits is a list. set q main go makes two elements, $q expands to two arguments, and fzf rejects the second just as it did in bash. ; or runs its command only after a failure, and the status 1 it printed is fzf's own. set -gx exports the variable until set -e erases it.
| bash | zsh | fish | |
|---|---|---|---|
| fzf's exit status | $? |
$? |
$status |
| options for one command | FZF_DEFAULT_OPTS=--exact fzf … |
the same | the same |
| a query in a variable | write "$q"; unquoted, it splits |
$q is one word; ${=q} splits |
one argument per list element |
If you are coming from another library¶
- Rust. Fuzzy finding: three key bindings, and the setup that is not the install ↗ installs fzf, turns on Ctrl-T, Ctrl-R and Alt-C with
fzf --bash,fzf --zshandfzf --fish, and points fzf atfd. This chapter does not repeat that setup. It measures the matcher those keys open.
See also¶
- Fuzzy means in order — why
rdmematched, and whyREADME.mdcame first - The extended search syntax — what a space,
',^,$and!do inside a query - Ctrl-R is a pipeline —
fzf --filterat the end of the history key binding's own pipeline - grep — the other filter in this library, and its exit status
- fzf(1), 0.67.0 ↗ —
--filter,--select-1,--exit-0,FZF_DEFAULT_OPTSand EXIT STATUS - zsh manual, Options ↗ —
SH_WORD_SPLIT