Skip to content

The extended search syntax

Level: 201 · you want fzf to find exactly this, starting with that, and never the other

One line: fzf splits its query at spaces into terms, and a line must match every term. A term is fuzzy unless its first or last character says otherwise. 'wild is an exact substring, ^core must start the line, .go$ must end it, and !test removes the lines that contain test. A | standing alone between two terms means either one. The shells claim some of those characters first. At an interactive bash or zsh prompt, ! inside double quotes is history expansion. ^ is a glob under zsh's extendedglob, and fish rejects "go$". Single quotes are safe in all three.

Measured

demo/files.txt holds fourteen paths. Each query is in quotes because it holds characters the shell would otherwise read:

Verified output of fzfext_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ fzf -f wldf < files.txt
music/sbtrkt-wildfire.mp3
music/sbtrkt-wildfire-rmx.mp3

$ fzf -f "'wldf" < files.txt; echo "status $?"
status 1

$ fzf -f "'wild" < files.txt
music/wild-horses.mp3
music/sbtrkt-wildfire.mp3
music/sbtrkt-wildfire-rmx.mp3

$ fzf -f '^core' < files.txt
core/app.go
core/app.py
core/app.rb
core/app_test.go

$ fzf -f '.go$' < files.txt
core/app.go
src/core.go
core/app_test.go

$ fzf -f '.go$ !test' < files.txt
core/app.go
src/core.go

$ fzf -f 'core go' < files.txt
core/app.go
core/app_test.go
src/core.go

$ fzf -f 'to do' < files.txt
notes/to do.txt
notes/todo.txt

$ fzf -f 'to\ do' < files.txt
notes/to do.txt

$ fzf -f '^core go$ | rb$ | py$' < files.txt
core/app.go
core/app.py
core/app.rb
core/app_test.go

$ fzf -f 'py$|rb$' < files.txt; echo "status $?"
status 1

$ fzf -f "'foo'" < files.txt
foo bar.txt
foo_bar.txt

$ fzf -f "'foo" < files.txt
foo_bar.txt
foo bar.txt
xfooy.txt

$ fzf -f "'wild ^music .mp3$ sbtrkt !rmx" < files.txt
music/sbtrkt-wildfire.mp3

$ fzf -f wld --exact < files.txt; echo "status $?"
status 1

$ fzf -f "'wld" --exact < files.txt
music/wild-horses.mp3
music/sbtrkt-wildfire.mp3
music/sbtrkt-wildfire-rmx.mp3

$ fzf -f '^core' +x < files.txt; echo "status $?"
status 1

$ fzf -f core go < files.txt; echo "status $?"
unknown option: go
status 2

$ printf '%s\n' 'fzf -f ".go$ !test" < files.txt' | HISTFILE= bash --norc --noprofile -i 2>&1 | grep -o 'event not found'
event not found

$ printf '%s\n' "fzf -f '.go$ !test' < files.txt" | HISTFILE= bash --norc --noprofile -i 2>/dev/null
core/app.go
src/core.go
  • Fuzzy by default; a leading ' makes a term exact. wldf found both wildfire files. 'wldf found nothing, because no line contains those four letters together. 'wild found all three lines that contain wild.
  • ^ and $ anchor the term. ^core kept the four lines that begin with core, and .go$ kept the three that end in .go. The manual adds that an anchored term is also an exact one.
  • ! removes. .go$ !test is two terms: the line must end in .go, and must not contain test. core/app_test.go dropped out.
  • A space means "and". core go found src/core.go as well as the core/ files, because each term is matched on its own. to do is two terms as well, so todo.txt matched it. to\ do is one term with a space in it, and only to do.txt matched.
  • | has to stand alone. ^core go$ | rb$ | py$ means a line that starts with core and ends in go, rb or py. It found four files, because app_test.go also ends in go. Written py$|rb$, without the spaces, the query is a single term, and it matched nothing.
  • ' at both ends asks for a whole word. 'foo' found foo bar.txt and foo_bar.txt, whose foo has a word boundary on each side. It did not find xfooy.txt, which 'foo alone did find. The manual counts an underscore as a boundary that ranks lower, and the space line came first.
  • The manual's own example, 'wild ^music .mp3$ sbtrkt !rmx, left one line.
  • --exact reverses the default. Under it, wld found nothing, and a leading ' makes a term fuzzy again, so 'wld found all three wild lines. +x turns the syntax off altogether: ^core became a literal ^, and it matched nothing.
  • Quote the whole query. Unquoted, core go reaches fzf as two arguments, and fzf calls the second one an unknown option.
  • At an interactive bash prompt, ! inside double quotes is history expansion. The last two commands feed one line each to an interactive bash. With the query in double quotes, bash answered event not found, which is all grep -o keeps of its stderr. In single quotes the same query ran. A script is not interactive, which is why 'wild ^music .mp3$ sbtrkt !rmx could sit inside double quotes above. History is the chapter about that expansion.

On a Mac

Nothing changes: the key is shared, and /bin/bash 3.2 also answered event not found to the double-quoted !.

In zsh and fish

zsh reads fzf's syntax the same way, and adds two readings of its own:

Verified output of fzfext_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ fzf -f '.go$ !test' < files.txt
core/app.go
src/core.go

$ fzf -f ".go$ !test" < files.txt
core/app.go
src/core.go

$ print -r -- 'fzf -f ".go$ !test" < files.txt' | zsh -f -i 2>&1 | grep -o 'event not found'
event not found

$ print -r -- "fzf -f '.go\$ !test' < files.txt" | zsh -f -i 2>/dev/null
core/app.go
src/core.go

$ fzf -f ^core < files.txt; echo "status $?"
core/app.go
core/app.py
core/app.rb
core/app_test.go
status 0

$ setopt extendedglob; print -r -- ^core; fzf -f ^core < files.txt; echo "status $?"; unsetopt extendedglob
files.txt
status 1

$ setopt extendedglob; fzf -f '^core' < files.txt; unsetopt extendedglob
core/app.go
core/app.py
core/app.rb
core/app_test.go

In a zsh script, ".go$ !test" in double quotes ran just like the single-quoted query: a $ followed by a space is a plain $, and a script does not expand !. Fed to an interactive zsh, the double-quoted query got event not found again, and the single-quoted one ran. An unquoted ^core is fine in plain zsh. With setopt extendedglob, as some .zshrc files have, ^core becomes a glob meaning "every file except core". In the example's folder that is files.txt, so fzf searched for files.txt, matched nothing and returned 1, with no error printed anywhere. Quoted, '^core' belongs to fzf again.

fish has no ! history expansion, and puts its trap on $ instead:

Verified output of fzfext_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ fzf -f '.go$ !test' < files.txt
core/app.go
src/core.go

$ fzf -f ".go\$ !test" < files.txt
core/app.go
src/core.go

$ fzf -f '\'wild' < files.txt
music/wild-horses.mp3
music/sbtrkt-wildfire.mp3
music/sbtrkt-wildfire-rmx.mp3

$ fzf -f ^core < files.txt
core/app.go
core/app.py
core/app.rb
core/app_test.go

$ fzf -f ".go$" < files.txt
fish: Expected a variable name after this $.
fzf -f ".go$" < files.txt
           ^
(exit status 127)

$ printf '%s\n' 'fzf -f ".go\$ !test" < files.txt' | interactive_fish 2>/dev/null
core/app.go
src/core.go

Inside double quotes, fish expects a variable name after $, so ".go$" is a parse error, reported before anything runs. \$ escapes the $, and single quotes avoid it. fish also accepts \' inside single quotes, so '\'wild' spells the exact term 'wild without switching to double quotes. The last command fed the double-quoted query to an interactive fish, and it ran: fish did not treat the ! as history.

bash zsh fish
! inside double quotes, in a script literal literal literal
! inside double quotes, at a prompt event not found event not found literal
$ inside double quotes a plain $ before a space a plain $ before a space write \$; ".go$" is a parse error
the whole query in single quotes reaches fzf as typed reaches fzf as typed reaches fzf as typed

If you are coming from another library

  • Rust. Fuzzy finding ↗ sets up the key bindings where you will type these terms without any shell quoting at all, because the query goes to fzf directly.

See also