Bash globbing: jak działają wzorce i rozwijanie ścieżek
Zrozum mechanizm rozwijania ścieżek w powłoce bash. Wyjaśniamy jak działają znaki wieloznaczne oraz opcje nullglob, dotglob, globstar i negacja wzorców w trybie extglob.
Działanie mechanizmu globbing w powłoce bash przed uruchomieniem polecenia
Mechanizm globbing w powłoce bash polega na zamianie wzorca, takiego jak *.log, na posortowaną listę istniejących nazw plików, co następuje przed uruchomieniem polecenia. rm *.log nigdy nie przekazuje do rm wzorca. Bash odczytuje zawartość katalogu, zachowuje pasujące nazwy, sortuje je, a następnie uruchamia rm, traktując każdą nazwę jako osobny argument. Program rm nie jest w stanie stwierdzić, czy w procesie brał udział jakikolwiek wzorzec.
Prawie wszystkie nieoczekiwane zachowania związane z mechanizmem globbing wynikają z tego jednego faktu. Dopasowywanie jest wykonywane przez powłokę w oparciu o pliki istniejące w danym momencie, a polecenie otrzymuje jedynie gotową listę. Formalna nazwa tego procesu to rozwijanie ścieżek (pathname expansion). Globbing to potoczne określenie, przy czym oba terminy oznaczają to samo.
Budowa małego drzewa do ćwiczeń
Każdy poniższy przykład operuje na tych samych plikach. Należy utworzyć je w katalogu tymczasowym, aby nie naruszyć istotnych danych, a sprzątanie ograniczyć do jednego polecenia.
lab=$(mktemp -d)
cd "$lab"
mkdir -p logs archive/2025 archive/2026
touch app.log app.log.1 app.log.2 app.log.10 error.log debug.LOG
touch notes.txt notes.md README 'weekly report.log'
touch .env .apprc
touch logs/nginx.log logs/nginx.log.1
touch archive/2025/dec.log archive/2026/jan.logmktemp -d tworzy nowy katalog i zwraca jego ścieżkę, a $(...) zapisuje tę ścieżkę w zmiennej lab. Jeśli ta składnia jest nowa, sposób przechwytywania wyjścia polecenia przez podstawienie wyjaśnia to szczegółowo.
Należy sprawdzić utworzoną strukturę oraz wersję powłoki, ponieważ jedna uwaga na końcu zależy od wersji:
ls -a
bash --versionWszystkie przykłady działają w bash 4.0 i nowszych. Uwaga globskipdots pod koniec dotyczy bash 5.2 i nowszych, które są dostarczane w bieżących wydaniach Ubuntu i Debian na sierpień 2026.
Poleceniem podglądu używanym w całym tekście jest printf '%s\n' PATTERN. Wyświetla ono jedno dopasowanie w linii i nie wprowadza żadnych zmian na dysku, więc można je bezpiecznie uruchamiać dla każdego wzorca, co do którego nie ma pewności.
Trzy znaki wieloznaczne: gwiazdka, znak zapytania i nawiasy kwadratowe
*dopasowuje dowolny ciąg znaków, w tym ciąg pusty.?dopasowuje dokładnie jeden znak.[...]dopasowuje dokładnie jeden znak wybrany z zestawu wewnątrz nawiasów.
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *Porównaj drugą linię z utworzoną listą plików. ? oznacza jeden znak i nic więcej, więc dwucyfrowy przyrostek nie pasuje do tego wzorca. Rozszerz go do app.log.[0-9]* i uruchom ponownie, aby zobaczyć różnicę.
Do wszystkich trzech znaków wieloznacznych mają zastosowanie dwie zasady. Żaden znak wieloznaczny nie dopasowuje /, więc wzorzec pozostaje w obrębie jednego poziomu katalogów. Ponadto wiodący . musi zostać wpisany dosłownie, więc wzorzec zaczynający się od * pomija wszystkie ukryte nazwy. Ta druga zasada sprawia, że rm * nie uwzględnia plików konfiguracyjnych (dotfiles).
Wyniki operacji glob są sortowane zgodnie z porządkiem sortowania bieżącej lokalizacji (locale), co oznacza sortowanie tekstowe znak po znaku. Dlatego przyrostek 10 jest sortowany przed przyrostkiem 2, ponieważ porównanie nigdy nie interpretuje żadnego z nich jako liczby. Uruchom printf '%s\n' app.log.*, a następnie ls -v app.log.* i porównaj oba wyniki: ls -v sortuje osadzone liczby według ich wartości, a sort -V wykonuje to samo zadanie w potoku.
Klasy znaków i zakresy
Wewnątrz nawiasów można umieścić listę, zakres lub negację.
[ch]dopasowuje jeden znak, będący alboc, alboh.[a-f]dopasowuje jeden znak z zakresu od a do f.[!0-9]dopasowuje jeden znak, który nie jest cyfrą.[^0-9]oznacza to samo.
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*Zakresy są elementem, którego działanie różni się między systemami. Zakres jest rozstrzygany zgodnie z kolejnością sortowania (collation order) bieżącego ustawienia regionalnego (locale), a nie według czystego ASCII, więc w niektórych systemach [a-z] dopasowuje również wielkie litery. Bash posiada opcję globasciiranges, która wymusza kolejność czystego ASCII, a nowsze kompilacje włączają ją domyślnie, dlatego należy sprawdzić własną konfigurację za pomocą shopt globasciiranges, zamiast zakładać jej działanie. Skrypt, który musi działać identycznie wszędzie, może samodzielnie ustalić kolejność poprzez ustawienie LC_ALL=C w osobnej linii, blisko początku pliku, co przywraca wszystkim zakresom kolejność bajtową.
To przypisanie musi być samodzielnym poleceniem. Zapisane jako prefiks, LC_ALL=C ls [a-z]* ustawia locale tylko dla ls i niczego więcej, a powłoka rozwinęła już wzorzec w swoim własnym locale, zanim ls w ogóle wystartuje, co stanowi powtórzenie zasady z pierwszej sekcji.
Rozwinięcie nawiasów klamrowych to nie globbing
Nawiasy klamrowe przypominają znaki wieloznaczne, lecz działają w zupełnie inny sposób. Rozwinięcie nawiasów klamrowych (brace expansion) następuje znacznie wcześniej niż globbing i nigdy nie sprawdza zawartości dysku.
echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}Żaden z tych plików nie istnieje, a rozwinięcie i tak zostaje wykonane. Na tym polega główna różnica: globbing zadaje pytanie systemowi plików, podczas gdy nawiasy klamrowe jedynie generują tekst. Dlatego właśnie nawiasy klamrowe są właściwym narzędziem do tworzenia obiektów, a nie do ich wyszukiwania.
mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csvcp app.log{,.bak} rozwija się do dwóch słów, app.log oraz app.log.bak, ponieważ pusty element przed przecinkiem powiela oryginalny tekst. {01..10} zachowuje dopełnienie zerami, ponieważ wiodące zero w którymkolwiek z punktów końcowych powoduje, że bash dopełnia każdą wygenerowaną liczbę do tej samej szerokości.
Kolejność rozwijania wyjaśnia również błąd, z którym użytkownicy często się spotykają. Bash najpierw rozwija nawiasy klamrowe, następnie zmienne, a na końcu wykonuje rozwijanie nazw ścieżek (pathname expansion).
n=5
echo {1..$n}Rozwinięcie nawiasów klamrowych kończy się, zanim $n stanie się liczbą, więc zakres nigdy nie zostaje utworzony. Należy użyć seq 1 "$n" lub pętli w stylu C zapisanej jako for ((i=1; i<=n; i++)).
Co się dzieje, gdy wzorzec nie pasuje do żadnego pliku
To pierwszy tryb awarii, na który każdy kiedyś trafia. Domyślnie bash pozostawia niedopasowany wzorzec w niezmienionej formie. Słowo dociera do polecenia dokładnie w takiej postaci, w jakiej zostało wpisane, więc polecenie otrzymuje znaki *, ., t, m oraz p i próbuje potraktować je jako nazwę pliku.
printf '%s\n' *.tmpJest to nieszkodliwe w przypadku printf. Nie jest jednak nieszkodliwe w przypadku polecenia, które tworzy pliki. Wykonanie touch *.tmp w katalogu, w którym nie ma plików .tmp, tworzy plik o nazwie dosłownie *.tmp, a jego późniejsze usunięcie wymaga użycia cudzysłowów, aby powłoka ponownie go nie rozwinęła: rm -- '*.tmp'.
Dwie opcje powłoki zmieniają tę zasadę. Wypróbuj każdą z nich na wzorcu, który do niczego nie pasuje, a następnie wyłącz ją.
shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob
shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglobnullglob sprawia, że niedopasowany wzorzec rozwija się do zera znaków, więc polecenie uruchamia się z mniejszą liczbą argumentów, niż zapisano. Jest to pożądane zachowanie w pętlach. Przy wyłączonym nullglob, for f in *.tmp wykonuje swoje ciało raz, a zmienna f przechowuje dosłowny wzorzec, co stanowi błąd w niemal każdym skrypcie. Przy włączonym nullglob, ciało pętli wykonuje się zero razy.
nullglob niesie ze sobą własne zagrożenie, ponieważ polecenie pozbawione wszystkich argumentów nadal się uruchamia. ls *.nope staje się samym ls, co powoduje wyświetlenie zawartości całego katalogu. grep -l needle *.nope staje się grep -l needle, które nie ma pliku do odczytu, więc oczekuje na dane ze standardowego wejścia, co wygląda jak zawieszenie programu. Włączaj nullglob tylko na czas trwania pętli, która go wymaga, a następnie wyłączaj go.
failglob wybiera inną drogę: niedopasowany wzorzec powoduje błąd, bash zgłasza go, a polecenie w ogóle się nie uruchamia. Jest to dobre ustawienie dla powłoki interaktywnej, ponieważ zapobiega przekazaniu błędnie wpisanego wzorca do rm jako dosłownej nazwy pliku.
Ukryte pliki: dotglob
shopt -s dotglob
printf '%s\n' *
shopt -u dotglobUruchom ten blok i porównaj listę z wcześniejszym wynikiem printf '%s\n' *. Opcja dotglob usuwa regułę kropki na początku nazwy, dzięki czemu * dopasowuje również ukryte pliki. Wpisy . oraz .. pozostają wykluczone przy włączonym dotglob.
Bez dotglob, .* jest standardowym sposobem na dostęp do ukrytych plików, co w przeszłości bywało niebezpieczne. W wersjach bash starszych niż 5.2, .* dopasowywało również . oraz .., przez co polecenia rekurencyjne, takie jak chmod -R 755 .*, przechodziły do katalogu nadrzędnego. W bash 5.2 wprowadzono opcję globskipdots, włączoną domyślnie, która wyklucza . oraz .. z każdego rozwijania ścieżek. Uruchom shopt globskipdots przed poleganiem na tym mechanizmie, ponieważ starsze systemy mogą go nie obsługiwać.
globstar: dopasowywanie w głąb drzewa katalogów
shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstarPrzy włączonej opcji globstar, wzorzec ** stanowiący pełny składnik ścieżki dopasowuje pliki oraz katalogi na dowolnym poziomie zagłębienia, włącznie z zerowym. Dzięki temu **/*.log obejmuje bieżący katalog, a także logs/ oraz całą zawartość archive/. Wzorzec **/ z końcowym ukośnikiem dopasowuje wyłącznie katalogi, co stanowi szybki sposób na sprawdzenie struktury drzewa.
Gdy opcja globstar nie jest ustawiona, ** zachowuje się jak zwykły * i pozostaje w obrębie jednego katalogu. Uruchom ten sam wzorzec przy wyłączonej opcji, aby porównać wyniki: jeśli skopiowany z zewnątrz wzorzec rekurencyjny zwraca znacznie mniej elementów niż oczekiwano, zazwyczaj przyczyną jest właśnie to ustawienie. Opcja globstar jest domyślnie wyłączona w każdej powłoce bash.
Warto znać jedno ograniczenie: ** nie podąża za dowiązaniami symbolicznymi do katalogów. Drzewo połączone za pomocą dowiązań symbolicznych wymaga użycia find -L.
nocaseglob, dla nazw różniących się wielkością liter
shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglobnocaseglob sprawia, że dopasowywanie nazw plików ignoruje wielkość liter, dzięki czemu przyrostek .LOG z wielkimi literami pasuje do wzorca .log z małymi literami. Porównaj to zestawienie z tym z pierwszej sekcji. Wyłącz tę opcję natychmiast, ponieważ pozostawienie jej włączonej zmienia sposób dopasowywania dla każdego kolejnego polecenia w tej powłoce, co jest mylące przy późniejszym debugowaniu.
nocaseglob dotyczy wyłącznie rozwijania nazw plików. Powiązaną opcją dla dopasowywania wzorców wewnątrz instrukcji case lub testu w podwójnym nawiasie jest nocasematch; obie opcje ustawia się niezależnie.
extglob: wzorce typu "nie to"
Rozszerzone wzorce są domyślnie wyłączone. Należy je włączyć w osobnej linii:
shopt -s extglobDostępnych jest pięć form, z których każda przyjmuje listę wzorców rozdzielonych znakiem |:
?(list)dopasowuje zero lub jedno wystąpienie.*(list)dopasowuje zero lub więcej wystąpień.+(list)dopasowuje jedno lub więcej wystąpień.@(list)dopasowuje dokładnie jedną z alternatyw.!(list)dopasowuje wszystko, co nie pasuje do żadnej z alternatyw.
Rozszerzony wzorzec musi zostać odczytany przez powłokę, która ma już włączoną tę opcję, dlatego poniższy zestaw instrukcji umieszcza się w małym skrypcie uruchamianym za pomocą bash -O extglob. Flaga -O ustawia opcję shopt, zanim nowa powłoka odczyta pierwszą linię pliku, co zapewnia poprawne parsowanie każdego wzorca. Ostatni akapit tej sekcji wyjaśnia, dlaczego kolejność ma znaczenie.
cat > patterns.sh <<'EOF'
printf '%s\n' @(app|error).log
printf '%s\n' app.log.+([0-9])
printf '%s\n' *.@(md|txt)
printf '%s\n' !(*.log)
printf '%s\n' !(*.log|*.md|README)
EOF
bash -O extglob patterns.sh+([0-9]) oznacza jedną lub więcej cyfr, dzięki czemu obsługuje numerowany przyrostek o dowolnej długości. Jest to rozwiązanie ograniczenia ? wspomnianego wcześniej; warto uruchomić oba wzorce jeden po drugim, aby zobaczyć różnicę.
Forma negacji jest odpowiedzią na zapytanie "wszystko oprócz" i stanowi główny powód włączania extglob. Dwie kwestie związane z tym mechanizmem bywają zaskakujące.
Wzorzec dopasowuje zarówno katalogi, jak i pliki, więc ls !(*.log) wyświetla zawartość każdego katalogu znajdującego się w wynikach, zamiast samej nazwy katalogu. Należy użyć ls -d lub podglądu printf, aby zobaczyć same nazwy.
Mechanizm ten podlega również regule kropki, tak jak każdy inny glob, więc !(*.log) obejmuje każdą widoczną nazwę, która nie kończy się na .log. W przypadku nazw ukrytych należy również ustawić dotglob.
Znak ! w !(...) to negacja globu. Nie ma ona związku z rozwijaniem historii i znakiem wykrzyknika, co jest oddzielną funkcją działającą na innym etapie.
Kolejna pułapka dotyczy czasu parsowania. Bash parsuje pełne polecenie przed uruchomieniem jakiejkolwiek jego części, więc włączenie extglob i użycie rozszerzonego wzorca w tej samej linii kończy się niepowodzeniem: wzorzec jest parsowany, gdy opcja jest jeszcze wyłączona. Polecenie shopt -s extglob należy umieścić w osobnej linii, przed wzorcami i blisko początku skryptu. Treść funkcji jest parsowana w momencie jej definiowania, więc opcja musi być włączona przed definicją, a nie przed wywołaniem. Uruchomienie powłoki za pomocą bash -O extglob, tak jak w powyższym przykładzie, ustala kolejność przed rozpoczęciem parsowania.
Powłoka rozwija wzorzec, polecenie tego nie robi
Jest to drugi tryb awarii, który wyjaśnia całą grupę błędów.
grep -l needle *.log
find . -name '*.log'
find . -name *.logPierwsza linia pozwala bash na dopasowanie, a grep otrzymuje listę nazw plików. Druga linia obejmuje wzorzec cudzysłowem, więc find otrzymuje pięć znaków *.log i samodzielnie wykonuje dopasowanie na każdym poziomie poniżej punktu startowego. Trzecia linia zawiera błąd: bash najpierw rozwija wzorzec, ograniczając się tylko do bieżącego katalogu, więc find otrzymuje polecenie wyszukania jednej konkretnej nazwy. W katalogu z jednym dopasowaniem narzędzie po cichu szuka niewłaściwego elementu. Przy dwóch lub więcej dopasowaniach find zgłasza błąd użycia, ponieważ dodatkowe nazwy trafiają w miejsce, w którym oczekiwane jest wyrażenie.
Zasada jest krótka. Jeśli wzorzec jest przeznaczony dla polecenia, należy go ująć w cudzysłów. Jeśli jest przeznaczony dla powłoki, należy pozostawić go bez cudzysłowu. Ten sam podział dotyczy wzorców --exclude w tar i rsync oraz filtrów --include w grep.
Wzorce glob nie są również wyrażeniami regularnymi, mimo że współdzielą niektóre znaki. W globie * oznacza dowolny ciąg znaków. W wyrażeniu regularnym * oznacza zero lub więcej wystąpień poprzedzającego znaku, więc grep '*.log' oznacza coś zupełnie innego, niż sugeruje wygląd.
Zmienne podlegają tej samej kolejności operacji. Rozwijanie wzorców (globbing) następuje po rozwinięciu zmiennych, więc nieujęta w cudzysłów zmienna zawierająca wzorzec zostanie rozwinięta względem zawartości katalogu.
pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"Bez cudzysłowu bash wykonuje globbing na wyniku zmiennej. W cudzysłowie wzorzec pozostaje dosłowny. Istnieje z tym związana konsekwencja, na której należy polegać: słowa wygenerowane przez globbing nigdy nie są ponownie dzielone na spacjach, więc for f in *.log traktuje nazwę pliku zawierającą spację jako jedną nazwę, podczas gdy for f in $(ls *.log) rozbija tę nazwę na dwa słowa. Należy iterować bezpośrednio po wzorcu glob.
Podgląd wzorca niszczącego przed jego uruchomieniem
Nigdy nie używaj rm jako pierwszego polecenia przy testowaniu nowego wzorca. Uruchom wzorzec z bezpiecznym poleceniem, przejrzyj listę, a następnie zmień tylko polecenie na początku linii.
target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $targetWpisanie wzorca do target zapobiega rozbieżnościom między trzema liniami, a $target pozostaje celowo nieujęte w cudzysłów, ponieważ pozwala to powłoce bash na jego rozwinięcie. printf '%s\n' wypisuje jedną nazwę w linii i nie wprowadza żadnych zmian. ls -ld wyświetla każdy element, zamiast listować zawartość katalogów, a kolumna trybu wskazuje, które wpisy są katalogami – jest to rzecz, którą należy sprawdzić przed każdym usunięciem rekurencyjnym. Odczytywanie bitów uprawnień drwxr-xr-x omawia tę kolumnę.
Przywołaj poprzednią linię strzałką w górę i edytuj tylko polecenie, aby wzorzec pozostał identyczny. Przepisywanie wzorca z pamięci jest momentem, w którym wkradają się błędy.
Umieść -- przed wzorcem. Oznacza ono koniec opcji, dzięki czemu nazwa pliku zaczynająca się od - jest traktowana jako nazwa, a nie jako flaga.
Jeśli chcesz wyłączyć globbing na pewien fragment skryptu, set -f wyłącza go, a set +f włącza ponownie.
Ten nawyk jest istotny, ponieważ rm nie posiada funkcji cofania. Odzyskiwanie plików usuniętych za pomocą rm -rf jest procesem powolnym i zazwyczaj niekompletnym, dlatego podgląd kosztuje znacznie mniej niż alternatywa.
Po zakończeniu pracy posprzątaj katalog ćwiczeniowy. Najpierw wypisz ścieżkę i przeczytaj ją, ponieważ rm -rf skierowane na niewłaściwą zmienną to dokładnie ten rodzaj wypadku, którego dotyczy ta sekcja.
cd ~
echo "$lab"
rm -rf -- "$lab"FAQ
Dlaczego find . -name *.log znajduje niewłaściwe pliki?
Ponieważ bash rozwija *.log, zanim find w ogóle wystartuje. Powłoka dopasowuje wzorzec do bieżącego katalogu i przekazuje wynikową nazwę pliku do find, który następnie szuka tej jednej nazwy na każdej głębokości. Jeśli pasują dwa lub więcej plików, find otrzymuje dodatkowe argumenty w miejscu, w którym oczekuje wyrażenia, i zgłasza błąd składni. Należy ująć wzorzec w cudzysłów jako find . -name '*.log', aby find samodzielnie wykonał dopasowanie, zgodnie z własnymi regułami i własną rekurencją.
Co oznacza, gdy mój wzorzec pojawia się jako dosłowny tekst?
Oznacza to, że nic nie zostało dopasowane. Domyślnie bash pozostawia niedopasowany wzorzec bez zmian i przekazuje surowe znaki do polecenia, które następnie traktuje je jako nazwę pliku. Należy ustawić shopt -s failglob, aby zamienić niedopasowany wzorzec w błąd przerywający działanie polecenia, lub shopt -s nullglob, aby wzorzec rozwijał się do zera. W przypadku nullglob należy sprawdzić, czy polecenie nadal ma sens bez żadnych argumentów, ponieważ ls *.nope staje się pustym ls i wyświetla zawartość całego katalogu.
Jak dopasować wszystkie pliki poza jednym wzorcem w bash?
Należy włączyć rozszerzone wzorce za pomocą shopt -s extglob w osobnej linii, a następnie użyć formy negacji. !(*.log) dopasowuje nazwy, które nie kończą się na .log, a !(*.log|*.md) wyklucza oba. Ukryte pliki pozostają poza wynikiem, chyba że ustawiono również dotglob, a katalogi są uwzględniane, więc należy podejrzeć wzorzec za pomocą printf '%s\n' !(*.log) przed przekazaniem go do rm.
Dlaczego * pomija ukryte pliki?
Kropka na początku nazwy musi zostać dopasowana dosłownie, więc * nigdy nie pasuje do nazwy, która się od niej zaczyna. Należy użyć .*, aby dotrzeć do ukrytych nazw, lub ustawić shopt -s dotglob, aby włączyć je do każdego zwykłego wzorca. Wpisy . oraz .. są zawsze wykluczone przy włączonym dotglob, a bash w wersji 5.2 i nowszych wyklucza je również z .* poprzez opcję globskipdots, która jest domyślnie włączona.
Czy potrzebuję globstar, aby ** działało?
Tak. Bez shopt -s globstar bash traktuje ** jako zwykły *, który dopasowuje zawartość wewnątrz jednego katalogu i na tym kończy. Przy włączonym globstar, ** stanowiące cały składnik ścieżki dopasowuje się na dowolnej głębokości, więc **/*.log przeszukuje bieżący katalog i każdy podkatalog poniżej, a samo **/ dopasowuje wyłącznie katalogi. Należy pamiętać, że ** nie podąża za dowiązaniami symbolicznymi do katalogów, więc należy użyć find -L dla drzewa zbudowanego z dowiązań symbolicznych.