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.wldffound bothwildfirefiles.'wldffound nothing, because no line contains those four letters together.'wildfound all three lines that containwild. ^and$anchor the term.^corekept the four lines that begin withcore, and.go$kept the three that end in.go. The manual adds that an anchored term is also an exact one.!removes..go$ !testis two terms: the line must end in.go, and must not containtest.core/app_test.godropped out.- A space means "and".
core gofoundsrc/core.goas well as thecore/files, because each term is matched on its own.to dois two terms as well, sotodo.txtmatched it.to\ dois one term with a space in it, and onlyto do.txtmatched. |has to stand alone.^core go$ | rb$ | py$means a line that starts withcoreand ends ingo,rborpy. It found four files, becauseapp_test.goalso ends ingo. Writtenpy$|rb$, without the spaces, the query is a single term, and it matched nothing.'at both ends asks for a whole word.'foo'foundfoo bar.txtandfoo_bar.txt, whosefoohas a word boundary on each side. It did not findxfooy.txt, which'fooalone 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. --exactreverses the default. Under it,wldfound nothing, and a leading'makes a term fuzzy again, so'wldfound all threewildlines.+xturns the syntax off altogether:^corebecame a literal^, and it matched nothing.- Quote the whole query. Unquoted,
core goreaches 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 answeredevent not found, which is allgrep -okeeps of its stderr. In single quotes the same query ran. A script is not interactive, which is why'wild ^music .mp3$ sbtrkt !rmxcould 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¶
- Fuzzy means in order — what a term without
',^,$or!matches, and how it ranks - fzf is a filter — the exit status that
'wldfandpy$|rb$returned - History —
!expansion at a bash or zsh prompt - fzf(1), 0.67.0 ↗ — EXTENDED SEARCH MODE
- zsh manual, Filename Generation ↗ —
^underEXTENDED_GLOB