fzf — fuzzy finder, który zmienia skrypty powłoki w interfejsy
fzf czyta linie ze standardowego wejścia i wypisuje wybraną na wyjście, ale jego drugą, mniej znaną połową jest silnik zdarzeń: reload, become, transform i podgląd pozwalają zbudować z jednego polecenia pełny interfejs terminalowy. Pokazujemy instalację, składnię wyszukiwania rozszerzonego, kompletną konfigurację przez FZF_DEFAULT_OPTS, sześć gotowych przepisów dla Laravela, Sail i gita oraz pułapki: kod wyjścia 130, koszt podglądu przy każdym ruchu kursora i różnicę między --listen a --listen-unsafe.
fzf to fuzzy finder działający w terminalu: czyta linie ze standardowego wejścia, pozwala je interaktywnie przefiltrować i wypisuje wybraną linię na standardowe wyjście. Opisany tak brzmi jak drobne narzędzie pomocnicze. Repozytorium junegunn/fzf zebrało jednak ponad 82 tysiące gwiazdek, 2,8 tysiąca forków i 313 współautorów, a autor, Junegunn Choi, prowadzi je od października 2013 roku, czyli prawie trzynaście lat. Napisane w Go, licencja MIT, wydanie 0.74.3 z 17 sierpnia 2026, ostatni commit z 26 sierpnia 2026.
Rozjazd między prostotą opisu a skalą projektu ma konkretne wyjaśnienie. README nazywa fzf „ogólnego przeznaczenia fuzzy finderem i interaktywnym zestawem narzędzi terminalowych”, i to drugie określenie jest tu ważniejsze. Poza filtrowaniem list fzf ma silnik zdarzeń: potrafi przeładować własne wejście przy każdym naciśnięciu klawisza, uruchomić polecenie w podglądzie, podmienić się na inny proces po zatwierdzeniu wyboru i wystawić serwer HTTP, przez który steruje nim proces z zewnątrz. Z tych klocków składa się interfejs, którego napisanie w Pythonie z curses zajęłoby tydzień, a w fzf zajmuje jedną funkcję powłoki na dziesięć linii.
Piszemy o tym z perspektywy zespołu, który spędza w terminalu większość dnia: Sail, artisan, git, docker, logi. Połowa tego czasu schodzi na przypominanie sobie nazw — jak dokładnie nazywa się ta komenda artisana, jak nazywa się gałąź, którą ktoś wypchnął w piątek, który kontener trzyma kolejkę. fzf zamienia „przypomnij sobie i wpisz” na „napisz trzy litery i wybierz”. To brzmi na drobiazg, dopóki nie policzy się, ile razy dziennie się to robi.
Model działania: jeden filtr uniksowy
Najkrótszy możliwy przykład pokazuje wszystko, co fzf robi domyślnie:
find * -type f | fzf > selectedNa wejściu lista plików, na ekranie interaktywna lista z polem zapytania, na wyjściu jedna linia zapisana do pliku. Żadnej konfiguracji, żadnego pliku ustawień, żadnej integracji z powłoką. Z tego wynika najbardziej użyteczna cecha fzf: da się go wstawić w dowolne miejsce, w którym już masz listę tekstową.
vim $(fzf)
git switch "$(git branch --format='%(refname:short)' | fzf)"
kill -9 "$(ps -ef | fzf | awk '{print $2}')"Dopasowanie jest rozmyte: wpisanie srcmdl znajdzie src/Models/User.php, bo litery zapytania występują w tej kolejności, choć nie po kolei. Ranking premiuje dopasowania zwarte, na początku wyrazu i po separatorze ścieżki. README obiecuje przetwarzanie milionów pozycji w milisekundach i to nie jest okrągłe zdanie marketingowe — dwa ostatnie wydania z sierpnia 2026 zajmowały się właśnie tym, o czym niżej.
fzf nie jest programem, który się uruchamia. Jest filtrem, który się wstawia — do potoku, do podstawienia polecenia, do skryptu. Cała reszta jego funkcji to nadbudowa nad tym jednym zdaniem.
Instalacja
fzf jest jednym plikiem wykonywalnym bez zależności, więc każda ścieżka instalacji sprowadza się do położenia binarki w PATH.
Menedżer pakietów
brew install fzf # macOS, Linuxbrew
mise use -g fzf@latest # mise, zawsze najnowsza
apt install fzf # Debian, Ubuntu
dnf install fzf # Fedora
pacman -S fzf # Arch
winget install fzf # Windows
scoop install fzf # Windows, ScoopWariant z apt ma haczyk, o którym trzeba wiedzieć zawczasu: repozytoria stabilnych dystrybucji trzymają wersję z dnia wydania dystrybucji, a fzf wypuszcza nowe wydanie co dwa–trzy tygodnie. Debian stable potrafi mieć wersję o rok starszą od bieżącej, a większość przykładów krążących po internecie używa opcji dodanych w ostatnich kilkunastu wydaniach. Pierwsze polecenie po instalacji to fzf --version, a nie kopiowanie konfiguracji z czyjegoś repozytorium dotfiles.
Skrypt instalacyjny z gita
Ten sposób daje bieżącą wersję niezależnie od dystrybucji i przy okazji konfiguruje integrację z powłoką:
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/installSkrypt pyta o trzy rzeczy: czy dopisać skróty klawiszowe, czy włączyć uzupełnianie i czy zmodyfikować pliki konfiguracyjne powłoki. Aktualizacja to cd ~/.fzf && git pull && ./install. W środowisku, w którym pliki dotfiles są zarządzane przez repozytorium, warto puścić ~/.fzf/install --bin, który pobiera samą binarkę i niczego nie modyfikuje.
Binarka i pakiet .deb
Wydania na GitHubie zawierają skompilowane binarki dla Linuksa, macOS, Windows, FreeBSD i OpenBSD, a od wersji 0.74.1 również pakiety .deb. To najsensowniejsza opcja dla obrazu Dockera i dla maszyny CI: jedna binarka o znanej wersji, bez menedżera pakietów w środku.
# w Dockerfile, wersja przypięta świadomie
ARG FZF_VERSION=0.74.3
RUN curl -fsSL "https://github.com/junegunn/fzf/releases/download/v${FZF_VERSION}/fzf-${FZF_VERSION}-linux_amd64.tar.gz" \
| tar -xz -C /usr/local/bin fzfPrzypięcie wersji nie jest tu przesadną ostrożnością. fzf siedzi w wersjonowaniu 0.x i nie deklaruje stabilności API między wydaniami minor — opcje bywają przemianowywane, a domyślne zachowania zmieniane. W wydaniu 0.74.0 --popup zaczął otwierać w tmuksie 3.7 pływający panel zamiast popupu, co jest zmianą zachowania dla każdego, kto miał to w konfiguracji. Skrypt, który ma działać w CI za pół roku, potrzebuje numeru wersji.
Integracja z powłoką: trzy skróty
Integracja to jedna linia w pliku konfiguracyjnym powłoki. fzf sam generuje potrzebny kod:
# ~/.bashrc
eval "$(fzf --bash)"
# ~/.zshrc
source <(fzf --zsh)
# ~/.config/fish/config.fish
fzf --fish | sourceNushell wymaga zapisania wygenerowanego kodu do katalogu automatycznego ładowania:
mkdir ($nu.default-config-dir | path join "autoload")
fzf --nushell | save -f ($nu.default-config-dir | path join "autoload" "_fzf_integration.nu")Po tym dostajesz trzy skróty klawiszowe i jeden mechanizm uzupełniania.
CTRL-T — wklejenie ścieżki do linii poleceń
Wstawia wybrany plik lub katalog w miejsce kursora. Domyślnie przechodzi drzewo wbudowanym walkerem z opcjami file,dir,follow,hidden, czyli widzi pliki ukryte i podąża za symlinkami, ale respektuje .gitignore tylko wtedy, gdy podstawisz własne polecenie. Sterują tym FZF_CTRL_T_COMMAND i FZF_CTRL_T_OPTS:
export FZF_CTRL_T_COMMAND='fd --type f --hidden --exclude .git'
export FZF_CTRL_T_OPTS="
--preview 'bat --color=always --style=numbers --line-range=:200 {}'
--preview-window 'right,60%,border-left'
--bind 'ctrl-/:change-preview-window(down,70%|hidden|)'"Ostatnia linia to przykład tego, jak fzf skleja się w całość: ctrl-/ przełącza podgląd między trzema stanami rozdzielonymi pionową kreską, a puste trzecie pole oznacza powrót do ustawienia domyślnego.
CTRL-R — historia poleceń
Zastępuje wbudowane reverse-i-search i jest tym skrótem, który najszybciej się przyzwyczaja. Kilka rzeczy, których nie widać na pierwszy rzut oka:
- TAB zaznacza wiele wpisów, więc da się wkleić sekwencję poleceń z historii,
- powtórne CTRL-R przełącza sortowanie między trafnością a kolejnością chronologiczną,
- ALT-R włącza tryb „raw”, który pokazuje polecenie razem z sąsiednimi wpisami z historii — przydatne, gdy pamiętasz, że interesujące polecenie było zaraz po
cddo jakiegoś katalogu, - zapytanie działa w trybie wyszukiwania rozszerzonego, więc
sail !testznajdzie polecenia zsail, pomijając te ztest.
ALT-C — wejście do katalogu
Wybiera katalog i wykonuje do niego cd. Domyślny walker to dir,follow,hidden. Konfiguracja przez FZF_ALT_C_COMMAND i FZF_ALT_C_OPTS:
export FZF_ALT_C_COMMAND='fd --type d --hidden --exclude .git --exclude node_modules --exclude vendor'
export FZF_ALT_C_OPTS="--preview 'eza --tree --level=2 --color=always {} 2>/dev/null || ls -la {}'"Każdy z trzech skrótów wyłącza się przez ustawienie odpowiedniej zmiennej *_COMMAND na pusty łańcuch. Przydaje się, gdy ALT-C zderza się ze skrótem terminala albo menedżera okien.
Uzupełnianie przez **
Sekwencja ** przed TAB uruchamia fzf jako mechanizm uzupełniania. Wyzwalacz zmienia FZF_COMPLETION_TRIGGER:
vim **<TAB> # pliki w katalogu bieżącym
vim ../**<TAB> # w katalogu nadrzędnym
cd ~/projekty/hype**<TAB> # z częściowym wzorcem
kill -9 **<TAB> # identyfikatory procesów, wielokrotny wybór przez TAB
ssh **<TAB> # hosty z /etc/hosts i ~/.ssh/config
export **<TAB> # zmienne środowiskowe
unalias **<TAB> # aliasyUzupełnianie identyfikatorów procesów przy kill jest tym przypadkiem, w którym fzf zarabia na siebie w pierwszym dniu: zamiast ps aux | grep, przepisywania numeru i pomyłki o jedną cyfrę wybierasz proces z listy.
Składnia wyszukiwania rozszerzonego
Domyślnie fzf pracuje w trybie rozszerzonym, w którym zapytanie rozbija się na tokeny po spacji, a każdy token może mieć własny typ dopasowania. Warto to znać, bo bez tego rozmyte dopasowanie na dużych listach zwraca zbyt wiele wyników.
sbtrkt— dopasowanie rozmyte, litery w podanej kolejności,'wild— dopasowanie dokładne, linia musi zawierać dosłowniewild,'wild'— dopasowanie dokładne do granicy wyrazu, czyliwildjako osobne słowo,^music— linia zaczyna się odmusic,.mp3$— linia kończy się na.mp3,!fire— linia nie zawierafire,!^music,!.mp3$— zaprzeczone dopasowanie prefiksu i sufiksu,wzorzec | wzorzec— alternatywa;^core go$ | rb$ | py$znajdzie linie zaczynające się odcorei kończące nago,rbalbopy.
Tokeny rozdzielone spacją łączą się koniunkcją, kreska pionowa wiąże mocniej niż spacja. Flagi -e i --exact odwracają domyślne zachowanie: dopasowanie staje się dokładne, a prefiks ' włącza rozmyte.
Ranking: --scheme i --tiebreak
Kolejność wyników przy równym dopasowaniu rozstrzyga --tiebreak z kryteriami length, chunk, pathname, begin, end i index. Zamiast składać to ręcznie, wygodniej sięgnąć po gotowy schemat:
--scheme=default— ogólny, dla dowolnego wejścia,--scheme=path— premiuje znaki po separatorze ścieżki i ustawia--tiebreak=pathname,length; właściwy wybór dla list plików,--scheme=history— dla danych chronologicznych, ustawia--tiebreak=index, czyli zachowuje kolejność wejścia.
Przy liście plików w monorepo różnica między default a path jest widoczna od razu: wpisanie usercontroller z --scheme=path wyrzuca na górę app/Http/Controllers/UserController.php, a nie plik testowy o dłuższej ścieżce, w którym te same litery przypadkiem układają się gęściej.
Konfiguracja globalna
Dwie zmienne środowiskowe ustawiają zachowanie domyślne dla wszystkich wywołań:
export FZF_DEFAULT_COMMAND='fd --type f --hidden --strip-cwd-prefix --exclude .git'
export FZF_DEFAULT_OPTS="
--height 60%
--layout reverse
--border rounded
--info inline-right
--highlight-line
--scheme path
--cycle
--bind 'ctrl-u:preview-page-up,ctrl-d:preview-page-down'
--bind 'ctrl-y:execute-silent(printf %s {} | pbcopy)+abort'"FZF_DEFAULT_COMMAND działa tylko wtedy, gdy fzf dostaje wejście z terminala, a nie z potoku. Podmiana find na fd albo rg --files daje przy okazji respektowanie .gitignore, co w projekcie z node_modules i vendor jest różnicą między listą kilkuset a kilkuset tysięcy plików.
Jeśli lista opcji rozrasta się do kilkunastu linii, można ją wynieść do pliku:
export FZF_DEFAULT_OPTS_FILE=~/.fzfrcWygląd steruje presetem --style z wartościami default, minimal i full[:STYL_RAMKI], a osobno --border z kilkunastoma wariantami od rounded i sharp po thinblock i none. Układ ekranu: --layout=reverse rysuje listę od góry z polem zapytania nad nią, --height 40% rezerwuje część okna zamiast trybu pełnoekranowego, --height ~100% dopasowuje wysokość do liczby wyników, a --height -3 zostawia trzy wolne linie.
Pływające okno w tmuksie
Opcja --popup (dawniej --tmux, które nadal działa jako alias) otwiera fzf w osobnym oknie tmuksa od wersji 3.3 albo Zellija od 0.44:
fzf --popup center # środek, 50% szerokości i wysokości
fzf --popup left,40%,90% # lewa strona, 40% szerokości, 90% wysokości
fzf --popup top,40% # góra, 40% wysokościOd tmuksa 3.7 i fzf 0.74.0 jest to pływający panel, nie popup. Różnica jest praktyczna: panel nie blokuje reszty sesji, więc w trakcie wyszukiwania da się przejść do innego okna, przesunąć panel myszą, rozwinąć go na pełny ekran albo wejść w tryb kopiowania. Popup wraca, gdy jawnie podasz styl ramki przez --border.
Podgląd i wyrażenia podstawiane
Podgląd zamienia listę w przeglądarkę zawartości. Polecenie z --preview uruchamia się przy każdym przesunięciu kursora, a wynik trafia do okna podglądu:
fzf --preview 'bat --color=always --style=numbers {}' \
--preview-window 'right,60%,wrap,~3'W szablonach polecenia fzf podstawia wartości. Ta lista jest sercem całej „programowalnej” części narzędzia:
{}— linia pod kursorem, w apostrofach,{+}— wszystkie zaznaczone pozycje,{*}— wszystkie dopasowane pozycje, także niezaznaczone,{q}— bieżące zapytanie,{n}— indeks pozycji, liczony od zera,{1},{2},{2..},{-1}— wybrane pola linii, z separatorem z--delimiter,{f},{+f}— ścieżka do pliku tymczasowego z pozycją albo z wszystkimi zaznaczonymi; ratunek przy zaznaczeniu kilkuset elementów, gdy lista argumentów nie zmieściłaby się w wywołaniu procesu.
Opcja --preview-window przyjmuje pozycję (up, down, left, right, next), rozmiar, styl ramki i zestaw przełączników: wrap i wrap-word zawijają linie, follow przewija za nowymi liniami jak tail -f, cycle zawija przewijanie, hidden startuje ze schowanym podglądem, +SCROLL ustawia pozycję startową, a ~N przykleja pierwsze N linii jako nagłówek. Zapis <80(up) zmienia układ na węższych terminalach: przy szerokości poniżej 80 kolumn podgląd wskakuje nad listę.
Do wyświetlania pól służą osobne opcje. --nth ogranicza obszar wyszukiwania do wybranych pól, --with-nth zmienia to, co widzi użytkownik, a --accept-nth to, co fzf wypisze po zatwierdzeniu. Ta trójka pozwala przekazywać przez fzf linie z metadanymi, wyszukiwać po jednej kolumnie, pokazywać drugą i zwracać trzecią, bez owijania wszystkiego w awk.
Od filtra do interfejsu: --bind
Tu przestaje być narzędziem do wybierania plików. --bind przypisuje akcje do klawiszy i do zdarzeń, a lista jednych i drugich jest długa.
Zdarzenia
start— po uruchomieniu, przed wczytaniem wejścia,load— po zamknięciu strumienia wejściowego,change— przy każdej zmianie zapytania,focus— przy przesunięciu kursora,result— po zakończeniu filtrowania,result-final— jakresult, ale bez wyzwalania w trakcie wczytywania wejścia (dodane w 0.74.0),resize— przy zmianie rozmiaru terminala,every(N)— co N sekund.
Rozdzielenie result i result-final rozwiązuje konkretny problem: przy wejściu, które napływa przez kilka sekund, result odpala się dla każdego pośredniego stanu listy, więc licznik w nagłówku migocze i akcja jednorazowa wykonuje się kilkanaście razy. result-final czeka na zamknięcie strumienia.
Akcje
reload,reload-sync— podmiana wejścia przez ponowne uruchomienie polecenia,become— zastąpienie procesu fzf innym poleceniem, czyliexecbez powrotu,execute,execute-silent— uruchomienie polecenia z powrotem do fzf; pierwsze oddaje terminal, drugie nie,transform,transform-query,transform-header,transform-prompt,transform-border-label— podmiana elementu interfejsu wynikiem polecenia,change-query,change-prompt,clear-query— zmiana stanu wprost,put,print,accept,accept-non-empty,abort— wpisanie znaku, wypisanie linii bez zamykania, zatwierdzenie, przerwanie,first,last,best,up,down— ruch po liście,toggle-preview,preview,refresh-preview,change-preview-window— sterowanie podglądem,wait— wstrzymanie kolejnych akcji do zakończenia wyszukiwania (dodane w 0.74.0).
Akcje łączy się plusem, a kilka przypisań przecinkiem. --bind 'start:change-query(foo)+wait+best' ustawia zapytanie, czeka na przefiltrowanie i ustawia kursor na najlepszym trafieniu. Bez wait kursor przeskakiwałby na pozycję z niedokończonej listy.
Zmienne przekazywane procesom potomnym
Polecenia uruchamiane przez execute, become, reload, transform i --preview dostają stan fzf w zmiennych środowiskowych: FZF_QUERY, FZF_MATCH_COUNT, FZF_TOTAL_COUNT, FZF_SELECT_COUNT, FZF_POS, FZF_CURRENT_ITEM, FZF_ACTION, FZF_KEY, FZF_PROMPT, FZF_LINES, FZF_COLUMNS, a w podglądzie dodatkowo FZF_PREVIEW_LINES i FZF_PREVIEW_COLUMNS. FZF_SELECT_COUNT jest tu najbardziej użyteczne: pozwala napisać jedno przypisanie, które zachowuje się inaczej dla pojedynczej pozycji i dla zaznaczenia wielu.
Kompletny przykład: ripgrep sterowany z fzf
Ten przepis z dokumentacji projektu pokazuje wszystkie mechanizmy naraz — fzf przestaje filtrować cokolwiek sam i staje się interfejsem nad ripgrepem:
rfv() (
RELOAD='reload:rg --column --color=always --smart-case {q} || :'
OPENER='if [[ $FZF_SELECT_COUNT -eq 0 ]]; then
vim {1} +{2}
else
vim +cw -q {+f}
fi'
fzf --disabled --ansi --multi \
--bind "start:$RELOAD" --bind "change:$RELOAD" \
--bind "enter:become:$OPENER" \
--bind "ctrl-o:execute:$OPENER" \
--bind 'alt-a:select-all,alt-d:deselect-all,ctrl-/:toggle-preview' \
--delimiter : \
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \
--preview-window '~4,+{2}+4/3,<80(up)' \
--query "$*"
)Warto to przeczytać linia po linii, bo każda robi coś, czego nie widać na pierwszy rzut oka:
--disabledwyłącza własne dopasowanie fzf; filtrowaniem zajmuje się ripgrep, fzf tylko wyświetla i przechwytuje klawisze,change:reloadprzy każdym naciśnięciu klawisza odpala ripgrepa od nowa z aktualnym zapytaniem w{q}, a|| :zjada niezerowy kod wyjścia przy braku trafień,--delimiter :rozbija linie ripgrepa na pola, więc{1}to plik, a{2}numer linii,enter:becomepodmienia fzf na edytor, więc po zamknięciu edytora wracasz do powłoki;ctrl-o:executeotwiera edytor i wraca do listy,$FZF_SELECT_COUNTrozstrzyga, czy otworzyć jeden plik w konkretnej linii, czy zbudować listę quickfix z{+f},+{2}+4/3w--preview-windowprzewija podgląd tak, żeby trafiona linia wypadła w jednej trzeciej wysokości okna.
Zamiana vim {1} +{2} na phpstorm --line {2} {1} daje to samo z PhpStormem — launcher JetBrains przyjmuje numer linii tą samą składnią.
Sześć przepisów dla naszego stacku
Wszystkie działają w bashu i zsh, i wszystkie zakładają, że fzf jest w PATH. Wklej je do ~/.bashrc albo osobnego pliku ładowanego z konfiguracji powłoki.
1. Przełączanie gałęzi gita z podglądem historii
gsw() {
local branch
branch=$(git branch --all --color=always --sort=-committerdate \
| grep -v '/HEAD ' \
| fzf --ansi --height 60% --layout reverse --no-multi \
--header 'Gałęzie, najświeższe u góry' \
--preview 'git log --color=always --date=short \
--pretty="%C(auto)%cd %h%d %s" -n 30 \
$(sed "s/.* //;s#remotes/[^/]*/##" <<< {})' \
--preview-window 'down,60%,wrap') || return
git switch "$(sed 's/.* //;s#remotes/[^/]*/##' <<< "$branch")"
}--sort=-committerdate jest tu ważniejsze niż wygląda: przy trzydziestu gałęziach alfabetyczna kolejność gita nie pomaga, a chronologiczna wystawia na górę to, nad czym ktoś pracował wczoraj. sed obcina znacznik bieżącej gałęzi i prefiks remotes/origin/.
2. Komendy artisana z podglądem opisu
sa() {
local cmd
cmd=$(vendor/bin/sail artisan list --raw \
| fzf --height 70% --layout reverse \
--delimiter '\s{2,}' --with-nth 1 \
--header 'artisan' \
--preview 'echo {2}' \
--preview-window 'down,4,wrap' \
--accept-nth 1) || return
vendor/bin/sail artisan "$cmd" "$@"
}artisan list --raw wypisuje nazwę komendy i jej opis rozdzielone kilkoma spacjami. --delimiter '\s{2,}' rozbija to na dwa pola, --with-nth 1 pokazuje samą nazwę, podgląd wyświetla opis z {2}, a --accept-nth 1 zwraca nazwę bez opisu. Cała lista pobiera się raz, więc jedno wywołanie kontenera na start, a nie jedno na każdy ruch kursora.
Gdyby --accept-nth nie było dostępne w twojej wersji, wystarczy dorzucić | awk '{print $1}' po fzf. To jeden z tych momentów, w których warto sprawdzić fzf --version.
3. Logi kontenera
dlog() {
local id
id=$(docker ps --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}' \
| fzf --height 50% --layout reverse \
--header 'Wybierz kontener' \
--preview 'docker logs --tail 60 {1} 2>&1' \
--preview-window 'down,65%,wrap' \
--bind 'ctrl-r:refresh-preview' \
--accept-nth 1) || return
docker logs -f --tail 200 "$id"
}Podgląd pokazuje ostatnie sześćdziesiąt linii logu, więc widać, który kontener się wywraca, jeszcze przed wybraniem. 2>&1 jest konieczne, bo docker logs rozdziela strumienie, a większość aplikacji pisze do stderr. ctrl-r:refresh-preview odświeża podgląd bez opuszczania listy.
4. Trasy aplikacji
routes() {
vendor/bin/sail artisan route:list --json \
| jq -r '.[] | [.method, .uri, (.name // "-"), .action] | @tsv' \
| fzf --height 80% --layout reverse --multi \
--header 'metoda / uri / nazwa / akcja' \
--nth 1,2,3 \
--preview 'echo {4} | tr "@" "\n"' \
--preview-window 'down,4,wrap'
}--nth 1,2,3 ogranicza wyszukiwanie do metody, URI i nazwy trasy, więc wpisanie post nie trafia w kontrolery z Post w nazwie klasy. Akcja nadal jest widoczna w podglądzie i wychodzi na wyjście.
5. Przywracanie plików z indeksu
gunstage() {
git status --porcelain \
| fzf --multi --height 60% --layout reverse --ansi \
--header 'TAB zaznacza, ENTER wycofuje z indeksu' \
--preview 'git diff --color=always HEAD -- {2}' \
--preview-window 'right,65%' \
--accept-nth 2 \
| xargs -r git restore --staged --
}xargs -r nie uruchamia gita przy pustym wejściu, co pokrywa przypadek anulowania fzf klawiszem ESC. Ten przepis zakłada ścieżki bez spacji; przy repozytorium, w którym spacje w nazwach plików się zdarzają, trzeba przejść na --read0, --print0 i xargs -0.
6. Pakiety Composera
cshow() {
composer show --direct --format=json \
| jq -r '.installed[] | [.name, .version] | @tsv' \
| fzf --height 60% --layout reverse \
--header 'Bezpośrednie zależności' \
--preview 'composer show {1}' \
--preview-window 'right,60%,wrap' \
--accept-nth 1
}Podgląd odpala composer show przy każdym ruchu kursora, co przy dużym composer.lock zajmuje kilkaset milisekund. Zauważalne, ale znośne — i to dobre miejsce, żeby przejść do sekcji o pułapkach.
--listen: sterowanie z zewnątrz
Opcja --listen uruchamia w fzf serwer HTTP. Proces z zewnątrz może wtedy wysłać metodą POST dowolną akcję do wykonania i odczytać metodą GET stan programu. Numer portu, gdy podasz 0, trafia do FZF_PORT, więc procesy potomne znajdują go same:
fzf --listen 0 --bind 'start:execute-silent(echo $FZF_PORT > /tmp/fzf.port)'
# z innego terminala
curl -X POST "localhost:$(cat /tmp/fzf.port)" -d 'change-query(controller)+first'
curl "localhost:$(cat /tmp/fzf.port)?limit=10" Otwiera to drogę do rzeczy, których w fzf normalnie nie ma: zewnętrzny proces monitorujący, który przeładowuje listę, gdy zmieni się plik; skrypt CI, który podstawia zapytanie; integracja z edytorem. Wymaga też ostrożności i autorzy projektu to przewidzieli.
Akcje uruchamiające procesy — execute, become, reload — są w trybie --listen zablokowane. Żeby je dopuścić, trzeba jawnie użyć --listen-unsafe, a nazwa opcji jest tu opisem, nie ostrzeżeniem na wszelki wypadek: proces, który dosięgnie tego portu, wykonuje dowolne polecenie z uprawnieniami użytkownika. Do tego dochodzi uwierzytelnianie: zmienna FZF_API_KEY wymusza nagłówek x-api-key o tej samej wartości, i jest obowiązkowa przy adresie nasłuchu innym niż localhost. Zabezpiecza to przed atakiem typu DNS rebinding, w którym strona otwarta w przeglądarce wysyła żądania na twój localhost.
Sam serwer też miał już swoją historię. W wydaniu 0.73.1 z maja 2026 poprawiono akumulację ciała żądania HTTP w złożoności kwadratowej: jedno żądanie o rozmiarze około 390 KB blokowało jednowątkowy serwer na osiem sekund. Zgłosił to zespół AFINE. Wniosek na przyszłość jest prosty: --listen na gniazdo unixowe albo na 127.0.0.1, nigdy na 0.0.0.0, i przypięta wersja fzf.
Wydajność
Dwa ostatnie wydania zajmowały się wyłącznie prędkością i liczby są konkretne. W 0.74.2 z 1 sierpnia 2026 zoptymalizowano krótkie zapytania, bo pierwszy naciśnięty klawisz skanuje cały zbiór kandydatów: zapytania jednoznakowe przyspieszyły do 2,4 razy, dwuznakowe do 1,4 razy, doszło też pominięcie zbędnych przebiegów przy sortowaniu radix. W 0.74.3 z 17 sierpnia 2026 poszło wejście spoza ASCII: zapytania ASCII do 16 razy szybciej, spoza ASCII do 12 razy, wczytywanie łaciny z diakrytykami o 37% szybciej, a zużycie pamięci przy wejściu CJK niższe o 29%. Dla polskich nazw plików i polskich komunikatów w logach ma to znaczenie wprost.
Do skryptów przydają się trzy opcje, które wyłączają interaktywność:
--filter=STR— fzf nie startuje interfejsu, tylko wypisuje dopasowane linie posortowane po trafności; przydatne w potoku, gdzie chcesz rankingu fzf bez okna,--select-1i--exit-0— automatyczne zatwierdzenie przy dokładnie jednym trafieniu i natychmiastowe wyjście przy braku trafień; to para, która zamienia interaktywny wybór w „zapytaj tylko wtedy, gdy jest o co”,--sync— wstrzymuje rysowanie interfejsu do wczytania całego wejścia; potrzebne przy filtrowaniu wielostopniowym, gdy jeden fzf karmi drugiego.
Kody wyjścia są udokumentowane i skrypt powinien je rozróżniać: 0 to normalne zatwierdzenie, 1 brak dopasowania, 2 błąd, 126 brak uprawnień do polecenia z become, 127 nieprawidłowe polecenie w become, 130 przerwanie przez CTRL-C albo ESC.
Pułapki
Kod 130 to nie błąd
Najczęstszy błąd w skryptach z fzf: potraktowanie każdego niezerowego kodu wyjścia jednakowo. Użytkownik, który nacisnął ESC, chciał anulować, a nie zobaczyć komunikat o błędzie i nie wykonać operacji na pustym łańcuchu. Funkcja bez || return po podstawieniu z fzf zrobi git switch "". Wszystkie przepisy powyżej mają to zabezpieczenie i nie jest ono ozdobą.
Podgląd wykonuje się przy każdym ruchu kursora
Przewinięcie listy o dwadzieścia pozycji to dwadzieścia uruchomień polecenia podglądu. Przy bat na lokalnym pliku jest to niewidoczne. Przy vendor/bin/sail artisan, docker exec albo zapytaniu do zdalnego API każdy taki ruch kosztuje setki milisekund i interfejs zaczyna się kleić. Dwa wyjścia: przygotować dane raz i pokazywać je z pól linii (jak w przepisie z artisanem), albo powiązać kosztowny podgląd z klawiszem przez --bind 'ctrl-p:preview(...)' zamiast trzymać go w --preview.
Wersjonowanie 0.x i pakiety dystrybucyjne
fzf nie obiecuje zgodności między wydaniami minor, a wydania wychodzą często. Konfiguracja skopiowana z wpisu na blogu sprzed dwóch lat może używać opcji, których dzisiejszy fzf nie zna, albo odwrotnie — na fzf z apt nie zadziała nic z ostatniego roku. Do tego wersja z repozytorium dystrybucji bywa o rok starsza. Przy skryptach współdzielonych w zespole warto ustalić minimalną wersję i sprawdzić ją na starcie.
Cudzysłowy w --bind
Wartość --bind przechodzi przez powłokę, a potem przez parser fzf, i dopiero potem trafia do sh -c. Trzy warstwy cytowania to trzy okazje do pomyłki. Praktyczna reguła: polecenie w akcji trzymać w apostrofach, a jeśli musi zawierać apostrof, wynieść je do zmiennej powłoki i wstawić przez "$ZMIENNA", jak w przepisie z ripgrepem. Przecinek i plus mają w składni --bind znaczenie specjalne, więc polecenie zawierające przecinek trzeba objąć nawiasami w formie execute(...).
Nazwy plików ze spacjami i znakami nowej linii
Domyślnie fzf rozdziela wejście i wyjście znakiem nowej linii, więc plik z taką nazwą rozpada się na dwie pozycje. Rozwiązanie to --read0 i --print0 po stronie fzf, -print0 albo -0 po stronie find i fd oraz xargs -0 na końcu potoku. Spacje same w sobie fzf obsługuje poprawnie, bo podstawiane wartości otacza apostrofami; problemem jest dopiero xargs bez -0.
Kolejność wyjścia przy --multi
Przy zaznaczeniu wielu pozycji fzf wypisuje je w kolejności zaznaczania, nie w kolejności na liście. Skrypt, który zakłada porządek wejściowy — na przykład stosuje łatki po kolei — musi je posortować sam.
Konflikty skrótów
CTRL-T, CTRL-R i ALT-C zderzają się z tym, co ma przypisane tmux, terminal albo menedżer okien. Na macOS ALT-C bywa przechwytywane przez terminal jako znak specjalny i wymaga włączenia opcji „Use Option as Meta key”. Każdy skrót wyłącza się osobno przez ustawienie FZF_CTRL_T_COMMAND, FZF_CTRL_R_COMMAND albo FZF_ALT_C_COMMAND na pusty łańcuch.
Vim, Neovim i inne integracje
Repozytorium junegunn/fzf zawiera warstwę podstawową dla Vima, czyli funkcję fzf#run(), na której buduje się własne komendy. Gotowe komendy (:Files, :Rg, :Buffers, :GFiles?) są w osobnym projekcie junegunn/fzf.vim:
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
Plug 'junegunn/fzf.vim'W Neovimie konfigurowanym w Lua sensowniejszym wyborem jest ibhagwan/fzf-lua, który używa tej samej binarki, ale interfejs i konfigurację ma po stronie Lua. Poza edytorami fzf jest też biblioteką Go — funkcja Run() pozwala wbudować go w program zamiast uruchamiać jako podproces, choć przy takim użyciu trzeba pamiętać, że fzf instaluje obsługę sygnałów i zmiany rozmiaru terminala (błąd z ich utrzymywaniem się po zakończeniu Run() poprawiono w 0.74.2).
Kiedy fzf nie jest odpowiedzią
Uczciwie o granicach. fzf filtruje linie tekstu i nic więcej — nie rozumie struktury, więc do przeszukiwania JSON-a nadal potrzebny jest jq, a do kodu narzędzie z indeksem, jak ripgrep czy serwer języka. Nie zapamiętuje niczego między uruchomieniami poza plikiem z --history. Nie zastąpi interfejsu, w którym trzeba edytować dane, a nie wybierać, bo wpisywanie odbywa się w jednym polu zapytania. Przy liście dwudziestu pozycji, którą i tak widać całą, select z basha wystarczy i będzie czytelniejszy w skrypcie.
Jest też koszt zespołowy: funkcje powłoki oparte na fzf działają tylko u tych, którzy je mają. Skrypt w repozytorium projektu, zakładający obecność fzf, przestaje działać u nowej osoby w pierwszym dniu pracy. My trzymamy takie rzeczy w prywatnych plikach dotfiles, a to, co ma działać u wszystkich, piszemy jako komendę artisana.
Podsumowanie
fzf daje:
- jeden plik wykonywalny bez zależności, dostępny w każdym menedżerze pakietów, w wydaniach na GitHubie i od 0.74.1 jako pakiet
.deb, - model filtra uniksowego, który wstawia się w dowolny potok bez konfiguracji,
- trzy skróty klawiszowe (CTRL-T, CTRL-R, ALT-C) i uzupełnianie przez
**, gotowe dla basha, zsh, fisha i nushella po jednej linii w konfiguracji powłoki, - składnię wyszukiwania rozszerzonego z dopasowaniem dokładnym, prefiksem, sufiksem, zaprzeczeniem i alternatywą, plus gotowe schematy rankingu
pathihistory, - silnik zdarzeń (
--bind,reload,become,transform,--preview) wystarczający do zbudowania interfejsu terminalowego w kilkunastu linijkach zamiast w osobnym programie, - pływające okno w tmuksie i Zellijym przez
--popup, od tmuksa 3.7 jako panel, który nie blokuje sesji, - prędkość liczoną w milisekundach przy milionach pozycji, z optymalizacjami krótkich zapytań i wejścia spoza ASCII w wydaniach z sierpnia 2026,
- projekt w bardzo dobrej kondycji: 82 tysiące gwiazdek, 313 współautorów, ciągła praca autora od 2013 roku, wydanie co dwa–trzy tygodnie.
Na co uważać:
- wersjonowanie 0.x bez obietnicy zgodności między wydaniami minor — w CI i w obrazie Dockera przypinaj wersję, a konfigurację z internetu weryfikuj względem
fzf --version, - wersja z repozytoriów stabilnych dystrybucji bywa o rok starsza od bieżącej i nie zna opcji z większości aktualnych przykładów,
- kod wyjścia
130(ESC, CTRL-C) trzeba obsłużyć osobno, inaczej skrypt wykonuje operację na pustym argumencie, - polecenie w
--previewuruchamia się przy każdym ruchu kursora — kosztowny podgląd wiąż z klawiszem albo przygotuj dane wcześniej, - trzy warstwy cytowania w
--bind(powłoka, parser fzf,sh -c) to najczęstsze źródło niedziałających przypisań, --listen-unsafepozwala zewnętrznemu procesowi wykonać dowolne polecenie; nasłuch tylko na gniazdo unixowe albo127.0.0.1, a poza localhostem obowiązkowo zFZF_API_KEY,- przy
--multiwyjście jest w kolejności zaznaczania, nie wejściowej, - funkcje powłoki oparte na fzf są prywatne z natury — kod, który ma działać u całego zespołu, lepiej napisać jako komendę artisana.
Licencja: MIT, Copyright 2013–2026 Junegunn Choi. Najbardziej liberalny wariant przy użyciu komercyjnym: wolno używać, modyfikować i rozprowadzać, także w produkcie zamkniętym, przy zachowaniu noty o prawach autorskich. Żadnych obowiązków publikacyjnych, żadnego licznika stanowisk, żadnej wersji płatnej z innym zestawem funkcji. Praktyczna konsekwencja przy naszej pracy: binarkę fzf wolno wstawić do obrazu Dockera wysyłanego klientowi i do narzędzi wewnętrznych bez pytania kogokolwiek o zgodę, a jeśli projekt kiedykolwiek zmieni kierunek, MIT zostawia społeczności możliwość forka. Warto tylko odnotować, że fzf jest przez trzynaście lat prowadzony w zasadzie przez jedną osobę — przy 313 współautorach większość commitów wychodzi od Junegunna Choia. Przy narzędziu deweloperskim to ryzyko akceptowalne, przy komponencie w produkcji warto by je zapisać w rejestrze.