Smart case, and cafe finding Café¶
Level: 201 · your lists hold names with capitals and accents: people, cities, file names
One line: A query in lower case ignores case, and a single capital makes the whole query case-sensitive; -i and +i force one or the other. fzf also folds Latin letters with accents, so cafe finds Café and lodz finds Łódź. In every query measured here, though, the folding stopped as soon as the query itself held an accented letter: lodź finds nothing. With a capital it is patchier still, since Cafe finds Café but Lodz does not find Łódź. --literal turns folding off, and the locale changes nothing.
Measured¶
demo/words.txt holds French café and Polish żółć and Łódź, each spelled with and without capitals and accents:
Verified output of fzfcase_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ cat words.txt
Café
cafe
CAFÉ
café
żółć
Żółć
zolc
Łódź
lodz
ŁÓDŹ
$ fzf -f cafe < words.txt
Café
cafe
CAFÉ
café
$ fzf -f Cafe < words.txt
Café
$ fzf -f CAFE < words.txt
CAFÉ
$ fzf -f Cafe -i < words.txt
Café
cafe
CAFÉ
café
$ fzf -f cafe +i < words.txt
cafe
café
$ fzf -f café < words.txt
Café
CAFÉ
café
$ fzf -f cafe --literal < words.txt
cafe
$ fzf -f zolc < words.txt
żółć
Żółć
zolc
$ fzf -f żółć < words.txt
żółć
Żółć
$ fzf -f Zolc < words.txt; echo "status $?"
status 1
$ fzf -f lodz < words.txt
Łódź
lodz
ŁÓDŹ
$ fzf -f łódź < words.txt
Łódź
ŁÓDŹ
$ fzf -f Łódź < words.txt
Łódź
$ fzf -f Lodz < words.txt; echo "status $?"
status 1
$ fzf -f lodź < words.txt; echo "status $?"
status 1
$ fzf -f lodz --literal < words.txt; echo "status $?"
lodz
status 0
$ LC_ALL=C.UTF-8 fzf -f lodz < words.txt
Łódź
lodz
ŁÓDŹ
$ printf 'caf\303\251 is NFC\ncafe\314\201 is NFD\n' > forms.txt; fzf -f cafe < forms.txt
café is NFC
café is NFD
$ fzf -f café < forms.txt
café is NFC
$ q=ŁÓDŹ; echo "length ${#q}, lowered $(printf '%s' "$q" | tr '[:upper:]' '[:lower:]')"
length 7, lowered ŁÓdŹ
- Smart case.
cafefound all four spellings.Cafefound onlyCafé, andCAFEonlyCAFÉ: once the query has a capital, every letter's case has to match.-igaveCafeall four back, and+iheldcafeto the two lower-case lines. - Accents fold one way.
cafefoundCafé, so fzf compared thatéas ane.cafédid not findcafe: the accented query matched only lines that have the accent.--literalturned the folding off, andcafethen found onlycafe. - Polish letters fold in a lower-case query.
zolcfoundżółć,Żółćandzolc, andlodzfoundŁódź,lodzandŁÓDŹ. With the accents typed,żółćfound the two accented spellings andłódźthe two accentedŁódź, still ignoring case. - One accent turns the folding off.
lodźislodzwith one accent added, and it found nothing at all, wherelodzfound three lines. - A capital does not fold every letter.
Cafestill foundCafé, butLodzdid not findŁódź, andZolcdid not findŻółć. Without-i, the only query here with a capital in it that foundŁódźisŁódźitself. - The locale changed nothing.
LC_ALL=C.UTF-8printed the same three lines as the runner'sLC_ALL=C. écan be written two ways.forms.txtholdscafétwice. The first is the single characteré(NFC). The second is anefollowed by a combining acute accent (NFD).cafefound both, because the decomposed line simply has one extra character after thee.café, typed with the single character, found only the NFC line.- Do not lower-case a query yourself. Under
LC_ALL=C, bash countedŁÓDŹas 7 characters, which is its byte count.tr '[:upper:]' '[:lower:]'lowered only the ASCIID. Lowering a query in the shell depends on the locale;-idoes not.
On a Mac¶
Nothing changes: the key is shared. The Mac's tr and GNU tr in the Linux image both left Ł, Ó and Ź alone under LC_ALL=C, and bash 3.2 and bash 5.2 both counted 7.
In zsh and fish¶
zsh hands fzf the same bytes and gets the same lines. Lowering the query is where it differs:
Verified output of fzfcase_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ fzf -f lodz < words.txt
Łódź
lodz
ŁÓDŹ
$ q=ŁÓDŹ; print -r -- "length ${#q}, lowered ${(L)q}"
length 7, lowered ŁÓdŹ
$ LC_ALL=C.UTF-8 zsh -fc 'q=ŁÓDŹ; print -r -- "length ${#q}, lowered ${(L)q}"'
length 4, lowered łódź
$ fzf -f ŁÓDŹ -i < words.txt
Łódź
ŁÓDŹ
Under LC_ALL=C, zsh also counted bytes, and ${(L)q} lowered only the D. The same line in a zsh started with LC_ALL=C.UTF-8 counted 4 characters and lowered all of them. fzf -i found both Łódź lines from the query exactly as typed.
fish works in characters even under LC_ALL=C:
Verified output of fzfcase_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ fzf -f lodz < words.txt
Łódź
lodz
ŁÓDŹ
$ set q ŁÓDŹ; echo "length "(string length -- $q)", lowered "(string lower -- $q)
length 4, lowered łódź
$ fzf -f (string lower -- ŁÓDŹ) < words.txt
Łódź
ŁÓDŹ
$ fzf -f ŁÓDŹ -i < words.txt
Łódź
ŁÓDŹ
string length counted 4 and string lower gave łódź under the same LC_ALL=C that made bash and zsh count bytes. So in fish, fzf -f (string lower -- $q) finds what -i finds. -i works in all three shells without depending on the locale.
lowering ŁÓDŹ under LC_ALL=C |
bash | zsh | fish |
|---|---|---|---|
| length | 7 (${#q}) |
7 (${#q}) |
4 (string length) |
| lowered | ŁÓdŹ (tr) |
ŁÓdŹ (${(L)q}) |
łódź (string lower) |
If you are coming from another library¶
- Encodings. Normalization ↗ explains why
éhas two spellings, and what NFC and NFD are.forms.txtabove is that difference reaching a fuzzy finder. - Encodings. Case is not a per-character operation ↗ shows why lower-casing is more than a table of letters. That is one more reason to leave case to
-i. - Rust. Fuzzy finding ↗ has a Polish section on fzf's folding. It also covers the macOS Option key, which types
ćwhen you meant Alt-C.
See also¶
- Fuzzy means in order — how the letters that do match are ranked
- The extended search syntax —
'and^on top of case and folding - fzf(1), 0.67.0 ↗ —
--smart-case,-i,+iand--literal - fish,
string↗ —string lowerandstring length