Skip to content

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. rdme matched README.md and docs/README.md because r, d, m and e occur 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.md came first. --no-sort printed the same two lines in the order of the file, which turns fzf --filter into a fuzzy grep. 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=nope printed 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-1 and --exit-0 are not --filter. They belong to the interactive finder. --query makef --select-1 printed its only match and exited 0 without drawing anything, and --query zzz --exit-0 exited 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_OPTS reaches --filter too. mngo found two files, then found nothing once --exact arrived through the variable. A bad option in the variable fails the run with status 2, and the message names $FZF_DEFAULT_OPTS rather than the command line. FZF_DEFAULT_OPTS_FILE does the same from a file. Setting both to empty for the one command, as the export line does, restores fzf's built-in defaults whatever the shell has exported.
  • An unquoted $q is two arguments in bash. q="main go"; fzf --filter $q hands fzf --filter main go, and fzf rejects the stray go with unknown option: go and 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

See also