Fuzzy means in order¶
Level: 201 · you have typed four letters into fzf and wondered why that line came first
One line: A fuzzy query matches a line when its characters occur in that line in the same order, however far apart: abc finds a_b_c and xaxbxcx, but not acb. The order of the matches is a score. A character that starts a word scores more, and how much more depends on the character before it: in the default scheme a space beats : and /, those beat ., _ and -, and all of them beat a camelCase capital. A tie goes to the shorter line. --scheme=path and --scheme=history move those bonuses, --exact turns the fuzziness off, and * is not a wildcard.
Measured¶
The inputs are four small files: letters.txt, bonus.txt, bar.txt and paths.txt.
Verified output of fzfuzzy_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ cat letters.txt
xaxbxcx
a_b_c
cab
a b c
bac
acb
abc
$ fzf --filter abc < letters.txt
abc
a b c
a_b_c
xaxbxcx
$ fzf --filter abc --exact < letters.txt
abc
$ cat bonus.txt
fooxbar
fooxBar
foo.bar
foo_bar
foo-bar
foo:bar
foo/bar
foo bar
$ fzf --filter fb < bonus.txt
foo bar
foo:bar
foo/bar
foo.bar
foo_bar
foo-bar
fooxBar
fooxbar
$ sort -r bonus.txt | fzf --filter fb
foo bar
foo:bar
foo/bar
foo_bar
foo.bar
foo-bar
fooxBar
fooxbar
$ fzf --filter bar < bar.txt
bar
barbecue
bxxaxxr
xbarx
$ fzf --filter bar --tiebreak=index < bar.txt
barbecue
bar
bxxaxxr
xbarx
$ fzf --filter bar --tiebreak=index,length < bar.txt; echo "status $?"
index should be the last criterion
status 2
$ fzf --filter doc < paths.txt
docs/report.txt
My Documents/report.txt
old docs/report final.txt
src/doc_parser.go
notes/reports/doc.md
$ fzf --filter doc --scheme=path < paths.txt
src/doc_parser.go
notes/reports/doc.md
docs/report.txt
My Documents/report.txt
old docs/report final.txt
$ fzf --filter fb --scheme=path < bonus.txt
foo/bar
foo.bar
foo_bar
foo-bar
foo:bar
foo bar
fooxBar
fooxbar
$ fzf --filter fb --scheme=history < bonus.txt
foo.bar
foo_bar
foo-bar
foo:bar
foo/bar
foo bar
fooxBar
fooxbar
$ fzf --filter "a*c" < letters.txt; echo "status $?"
status 1
$ fzf --filter a*c < letters.txt; echo "status $?"
status 1
- In order, not adjacent.
abcmatched the four lines wherea,bandcoccur from left to right. It matched none ofcab,bacandacb, which hold the same letters in another order.--exactwants the three letters side by side, and leaves onlyabc. - Starting a word scores. The eight lines of
bonus.txtare all seven characters long, with thebfour characters after thef, so only the character in front of thebdiffers. The space ranked first. Next came:and/, then.,_and-, then the camelCaseB. Last camefooxbar, whosebsits inside a word and still matched. - Equal scores keep the file's order.
sort -rputsfoo_barbeforefoo.bar, and fzf did the same: those two scored equally, and the tie went to the line that came first. The:and/lines are in the same order in both files, so this measurement cannot tell whether they tie. - Where a match starts can outweigh adjacent letters. In
bar.txt,bxxaxxrranked abovexbarx. Thebthat starts the line, with theaandrfar behind it, beat three adjacent letters that start no word. - A tie goes to the shorter line.
barandbarbecueboth start withbar. The shorter one came first even though it comes later in the file.--tiebreak=indexputs the file's order first instead.indexis allowed only at the end of the list, and fzf says so with status 2. - A scheme changes the bonuses. With
--scheme=path,foo/barcame first and the other separators, the space included, fell into the file's order.paths.txtshows what that scheme is for. Withdoc, the default scheme rankeddocs/report.txtfirst, while--scheme=pathranked first the two lines whose file name, after the last/, containsdoc. The manual gives that scheme the tiebreakpathname,length.--scheme=historyput all six separator lines in the file's order, with the camelCase and mid-word lines still last. That is the scheme fzf's Ctrl-R uses, and its input is in newest-first order (Ctrl-R is a pipeline). *is a character. fzf has no wildcards."a*c"looks for ana, then a*, then ac, and no line contains a*. Unquoted, bash found no file indemo/to matcha*c, so it passed the word on as typed and fzf saw the same query.
On a Mac¶
Nothing changes: the key is shared. fzf computes the ranking, and fzf is 0.67.0 on both machines. The only other tool is sort -r, and under the runner's LC_ALL=C the Mac's sort and GNU sort put these lines in the same order.
In zsh and fish¶
zsh prints the same lines in the same order for all three schemes. Its difference is the unquoted *:
Verified output of fzfuzzy_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ for s in default path history; do echo "-- $s"; fzf --filter fb --scheme=$s < bonus.txt; done
-- default
foo bar
foo:bar
foo/bar
foo.bar
foo_bar
foo-bar
fooxBar
fooxbar
-- path
foo/bar
foo.bar
foo_bar
foo-bar
foo:bar
foo bar
fooxBar
fooxbar
-- history
foo.bar
foo_bar
foo-bar
foo:bar
foo/bar
foo bar
fooxBar
fooxbar
$ fzf --filter "a*c" < letters.txt; echo "status $?"
status 1
$ fzf --filter a*c < letters.txt; echo "status $?"
(eval):1: no matches found: a*c
status 1
With no file to match, zsh refused the word a*c with no matches found instead of passing it on. The (eval):1: in front comes from the example's eval, not from fzf.
fish prints the same rankings too, and refuses the same word in its own way:
Verified output of fzfuzzy_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ for s in default path history; echo "-- $s"; fzf --filter fb --scheme=$s < bonus.txt; end
-- default
foo bar
foo:bar
foo/bar
foo.bar
foo_bar
foo-bar
fooxBar
fooxbar
-- path
foo/bar
foo.bar
foo_bar
foo-bar
foo:bar
foo bar
fooxBar
fooxbar
-- history
foo.bar
foo_bar
foo-bar
foo:bar
foo/bar
foo bar
fooxBar
fooxbar
$ fzf --filter "a*c" < letters.txt; echo "status $status"
status 1
$ fzf --filter a*c < letters.txt
fish: No matches for wildcard 'a*c'. See `help language#wildcards-globbing`.
fzf --filter a*c < letters.txt
^~^
(exit status 124)
fish stopped the command with No matches for wildcard 'a*c' and exit status 124, and pointed at the word.
unquoted a*c, no file matches |
bash | zsh | fish |
|---|---|---|---|
| what happens | passed to fzf as typed | no matches found; fzf does not run |
No matches for wildcard, status 124; fzf does not run |
An unquoted [:lower:] is a glob is the same three-way split, measured for tr. Quoting the query settles it in all three shells.
If you are coming from another library¶
- Rust. Fuzzy finding ↗ walks through Ctrl-T, where typing
optvsfindsoption_vs_result.rsdeep in a tree. This page is the rule that makes that work, measured with--filter.
See also¶
- fzf is a filter —
--filter, its exit status, and--no-sort - The extended search syntax —
'for one exact term, instead of--exactfor the whole query - Smart case, and
cafefindingCafé— which letters count as the same letter before any of this ranking happens - fzf(1), 0.67.0 ↗ —
--scheme,--tiebreak,--exactand--no-sort