SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor

Hoe werkt Bash globbing en wildcards?

Leer hoe Bash patronen omzet in bestandsnamen via pathname expansion. Ontdek het gebruik van nullglob, dotglob, globstar en extglob voor nauwkeurige selectie in uw scripts.

Wat bash globbing doet voordat uw commando wordt uitgevoerd

Bash globbing is het proces waarbij de shell een patroon zoals *.log omzet in een gesorteerde lijst van bestaande bestandsnamen, nog voordat het commando start. rm *.log krijgt nooit een patroon doorgegeven via rm. Bash leest de directory, behoudt de namen die overeenkomen, sorteert deze en voert vervolgens rm uit met elke naam als een afzonderlijk argument. Het programma rm kan niet zien dat er ooit een patroon is gebruikt.

Bijna alles wat verrassend is aan globbing, vloeit voort uit dit ene feit. De matching wordt uitgevoerd door de shell, op basis van de bestanden die op dat moment bestaan, en het commando ziet alleen de uiteindelijke lijst. De formele term hiervoor is pathname expansion. Globbing is de alledaagse term, en beide betekenen hetzelfde.

Bouw een kleine boomstructuur om mee te oefenen

Elk onderstaand voorbeeld werkt met dezelfde bestanden. Maak deze aan in een tijdelijke map, zodat er niets belangrijks in de buurt staat en opruimen slechts één commando kost.

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.log

mktemp -d maakt een nieuwe map aan en geeft het pad terug, en $(...) vangt dat pad op in de variabele lab. Als deze syntaxis nieuw voor u is, legt hoe command substitution de output van een commando opvangt dit volledig uit.

Bekijk wat u heeft gebouwd en controleer uw shell, aangezien één opmerking aan het einde afhankelijk is van de versie:

ls -a
bash --version

Alles hier werkt op bash 4.0 en later. De opmerking globskipdots nabij het einde is van toepassing op bash 5.2 en later, wat de standaard is in de huidige Ubuntu- en Debian-releases per augustus 2026.

Het preview-commando dat overal wordt gebruikt is printf '%s\n' PATTERN. Het print één resultaat per regel en wijzigt niets op de schijf, dus het is veilig om uit te voeren met elk patroon waar u niet zeker van bent.

De drie wildcards: sterretje, vraagteken en haakjes

  • * komt overeen met elke reeks tekens, inclusief een lege reeks.
  • ? komt overeen met exact één teken.
  • [...] komt overeen met exact één teken uit de set binnen de haakjes.
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *

Vergelijk de tweede regel met de bestandslijst die u heeft aangemaakt. ? staat voor één teken en niets meer, dus een achtervoegsel van twee cijfers valt buiten dat patroon. Verbreed het naar app.log.[0-9]* en voer het opnieuw uit om het verschil te zien.

Er zijn twee regels van toepassing op alle drie de wildcards. Geen enkele wildcard komt ooit overeen met een /, waardoor een patroon binnen één mapniveau blijft. En een leidende . moet letterlijk worden getypt, dus een patroon dat begint met * slaat elke verborgen naam over. Die tweede regel is de reden waarom rm * uw dotfiles ongemoeid laat.

Glob-resultaten worden gesorteerd op basis van de sorteervolgorde van de huidige locale, wat een tekstuele sortering teken voor teken is. Een achtervoegsel van 10 sorteert daarom vóór een achtervoegsel van 2, omdat de vergelijking geen van beide als een getal leest. Voer printf '%s\n' app.log.* en daarna ls -v app.log.* uit en vergelijk de twee: ls -v sorteert ingebedde getallen op waarde, en sort -V doet hetzelfde werk in een pipeline.

Karakterklassen en bereiken

Binnen de vierkante haken kunt u een lijst, een bereik of een negatie opgeven.

  • [ch] komt overeen met één karakter, ofwel c of h.
  • [a-f] komt overeen met één karakter in het bereik a tot en met f.
  • [!0-9] komt overeen met één karakter dat geen cijfer is. [^0-9] betekent hetzelfde.
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*

Bereiken zijn het onderdeel dat voor verschillen tussen systemen zorgt. Een bereik wordt opgelost op basis van de sorteervolgorde van de huidige locale in plaats van op basis van standaard ASCII. Op sommige systemen komt [a-z] daarom ook overeen met hoofdletters. Bash heeft een globasciiranges-optie die de standaard ASCII-volgorde afdwingt. Recente builds schakelen dit standaard in, dus controleer uw instelling met shopt globasciiranges in plaats van ervan uit te gaan. Een script dat overal op dezelfde manier moet matchen, kan de volgorde zelf vastleggen door LC_ALL=C op een eigen regel bovenaan het script te plaatsen. Dit zet elk bereik terug naar de standaard byte-volgorde.

Die toewijzing moet een op zichzelf staand commando zijn. Wanneer het als prefix wordt geschreven, stelt LC_ALL=C ls [a-z]* de locale alleen in voor ls. De shell heeft het patroon dan echter al uitgebreid in zijn eigen locale voordat ls überhaupt start; dit is de regel uit het eerste gedeelte die hier opnieuw van toepassing is.

Brace expansion is geen globbing

Braces lijken op een wildcard, maar gedragen zich er totaal niet naar. Brace expansion wordt veel eerder uitgevoerd dan globbing en kijkt nooit op de schijf.

echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}

Geen van deze bestanden bestaat, en de expansie vindt toch plaats. Dat is het hele verschil: een glob stelt een vraag aan het bestandssysteem, terwijl een brace alleen tekst genereert. Dit is ook de reden waarom braces het juiste gereedschap zijn voor het aanmaken van zaken in plaats van het vinden ervan.

mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csv

cp app.log{,.bak} expandeert naar twee woorden, app.log en app.log.bak, omdat het lege item voor de komma de oorspronkelijke tekst reproduceert. {01..10} behoudt zijn nul-opvulling, omdat een voorloopnul op een van beide eindpunten ervoor zorgt dat bash elk gegenereerd getal naar dezelfde breedte opvult.

De volgorde van expansie verklaart ook een fout waar mensen vaak tegenaan lopen. Bash expandeert eerst braces, daarna variabelen, en voert als laatste pathname expansion uit.

n=5
echo {1..$n}

Brace expansion is al voltooid tegen de tijd dat $n een getal wordt, dus er wordt nooit een bereik opgebouwd. Gebruik seq 1 "$n", of een lus in C-stijl geschreven als for ((i=1; i<=n; i++)).

Wat gebeurt er als een patroon niets matcht

Dit is het eerste faalscenario, en iedereen loopt er wel eens tegenaan. Standaard laat bash een niet-gematcht patroon volledig ongemoeid. Het woord bereikt het commando precies zoals u het heeft getypt, dus het commando ontvangt de tekens *, ., t, m en p en probeert deze als bestandsnaam te behandelen.

printf '%s\n' *.tmp

Dat is ongevaarlijk bij printf. Het is niet ongevaarlijk bij een commando dat bestanden aanmaakt. touch *.tmp in een map zonder .tmp-bestanden maakt een bestand aan met de letterlijke naam *.tmp, en het achteraf verwijderen vereist quoting zodat de shell het niet opnieuw expandeert: rm -- '*.tmp'.

Twee shell-opties veranderen deze regel. Probeer ze elk uit op een patroon dat niets matcht en schakel ze daarna weer uit.

shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob

shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglob

nullglob zorgt ervoor dat een niet-gematcht patroon expandeert naar niets, waardoor het commando wordt uitgevoerd met minder argumenten dan u heeft geschreven. Dat is wat u wilt rondom een loop. Met nullglob uitgeschakeld, voert for f in *.tmp de body één keer uit waarbij f het letterlijke patroon bevat; dit is een bug in bijna elk script dat dit doet. Met nullglob ingeschakeld, wordt de body nul keer uitgevoerd.

nullglob brengt een eigen risico met zich mee, omdat een commando zonder argumenten nog steeds wordt uitgevoerd. ls *.nope wordt een kale ls, die de volledige map weergeeft. grep -l needle *.nope wordt grep -l needle, die geen bestand heeft om te lezen, waardoor het wacht op standaardinvoer en lijkt te zijn vastgelopen. Schakel nullglob in rondom de loop die het nodig heeft, en schakel het daarna weer uit.

failglob kiest de andere weg: een niet-gematcht patroon is een fout, bash meldt dit, en het commando wordt helemaal niet uitgevoerd. Dit is een goede instelling voor een interactieve shell, omdat het voorkomt dat een verkeerd getypt patroon als letterlijke naam wordt doorgegeven aan rm.

Verborgen bestanden: dotglob

shopt -s dotglob
printf '%s\n' *
shopt -u dotglob

Voer dit blok uit en vergelijk de lijst met de eerdere printf '%s\n' *. dotglob heft de regel voor de beginnende punt op, waardoor * ook verborgen namen matcht. De items . en .. blijven onder dotglob altijd uitgesloten.

Zonder dotglob is .* de gebruikelijke manier om verborgen bestanden te bereiken, wat voorheen risicovol was. In bash-versies vóór 5.2 matchte .* ook . en .., waardoor een recursief commando zoals chmod -R 755 .* direct de bovenliggende map inliep. Bash 5.2 introduceerde de optie globskipdots, die standaard is ingeschakeld en . en .. uit elke expansie houdt. Voer shopt globskipdots uit voordat u hierop vertrouwt, aangezien oudere systemen deze optie niet ondersteunen.

globstar: recursief matchen door de gehele boomstructuur

shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstar

Wanneer globstar is ingeschakeld, matcht een ** die een volledig padcomponent vormt bestanden en mappen op elk niveau, inclusief nul niveaus diep. Hierdoor dekt **/*.log de huidige map, evenals logs/ en alles onder archive/. Een **/ met een afsluitende slash matcht uitsluitend mappen, wat een snelle methode is om de structuur van een boom te bekijken.

Zonder dat globstar is ingeschakeld, gedraagt ** zich als een gewone * en blijft deze beperkt tot één map. Voer hetzelfde patroon uit met de optie uitgeschakeld en vergelijk het resultaat: een recursief patroon dat elders is gekopieerd en veel minder resultaten geeft dan verwacht, wordt meestal hierdoor veroorzaakt. globstar staat standaard uitgeschakeld in elke bash.

Eén beperking is belangrijk om te weten: ** volgt geen symbolische koppelingen naar mappen. Een boomstructuur die is samengesteld met symlinks vereist in plaats daarvan find -L.

nocaseglob, voor namen waarbij hoofdlettergebruik verschilt

shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglob

nocaseglob zorgt ervoor dat het matchen van bestandsnamen hoofdletterongevoelig is, waardoor een .LOG-achtervoegsel in hoofdletters overeenkomt met een .log-patroon in kleine letters. Vergelijk deze lijst met die uit de eerste sectie. Schakel de optie direct weer uit, omdat deze, indien ingeschakeld, het matchen voor elk volgend commando in die shell verandert; dit is een verwarrend punt om een uur later te moeten debuggen.

nocaseglob is alleen van toepassing op het uitbreiden van bestandsnamen. De gerelateerde optie voor patroonmatching binnen een case-instructie of een test met dubbele haken is nocasematch, en beide worden afzonderlijk ingesteld.

extglob: patronen die "niet dit" kunnen aangeven

Extended patterns staan standaard uit. Schakel ze in op een eigen regel:

shopt -s extglob

Er zijn vijf vormen, die elk een lijst met patronen accepteren, gescheiden door |:

  • ?(list) komt overeen met nul of één keer voorkomen.
  • *(list) komt overeen met nul of meer keer voorkomen.
  • +(list) komt overeen met één of meer keer voorkomen.
  • @(list) komt overeen met precies één van de alternatieven.
  • !(list) komt overeen met alles wat niet met een alternatief overeenkomt.

Een extended pattern moet worden gelezen door een shell waar de optie al is ingeschakeld. Daarom staat het onderstaande batchbestand in een script dat wordt gestart met bash -O extglob. De vlag -O stelt een shopt-optie in voordat de nieuwe shell ook maar één regel van het bestand leest, waardoor elk patroon correct wordt geparseerd. De laatste alinea van deze sectie verklaart waarom de volgorde van belang is.

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]) staat voor één of meer cijfers en dekt dus een genummerd achtervoegsel van elke lengte. Dit is de oplossing voor de ?-beperking van eerder; het is de moeite waard om beide patronen na elkaar uit te voeren om dit te zien.

De negatievorm is het antwoord op "alles behalve" en is de reden om extglob überhaupt in te schakelen. Twee zaken hierover verrassen gebruikers vaak.

Het komt overeen met zowel mappen als bestanden, dus ls !(*.log) geeft de inhoud van elke map in het resultaat weer in plaats van de mapnaam zelf. Gebruik ls -d, of de printf-preview, om de namen zelf te zien.

Het volgt ook de dot-regel zoals elk ander glob-patroon, dus !(*.log) dekt elke zichtbare naam die niet eindigt op .log. Voor verborgen namen moet ook dotglob zijn ingesteld.

De ! in !(...) is glob-negatie. Dit staat los van history expansion en het uitroepteken, wat een afzonderlijke functie is die in een andere fase wordt uitgevoerd.

Nog een valkuil, en deze treedt op tijdens het parsen. Bash parseert een volledig commando voordat het een deel ervan uitvoert. Daarom mislukt het inschakelen van extglob en het gebruik van een extended pattern op dezelfde regel: het patroon wordt geparseerd terwijl de optie nog uit staat. Houd shopt -s extglob op een eigen regel, vóór de patronen en bovenaan in een script. Een functiebody wordt geparseerd op het moment dat de functie wordt gedefinieerd; de optie moet dus aan staan vóór de definitie, niet vóór de aanroep. Door de shell te starten met bash -O extglob, zoals in het bovenstaande batchbestand, wordt de volgorde vastgelegd voordat het parsen begint.

De shell breidt het patroon uit, het commando niet

Dit is het tweede type fout en het verklaart een hele reeks bugs.

grep -l needle *.log
find . -name '*.log'
find . -name *.log

De eerste regel laat bash de matching uitvoeren en grep ontvangt een lijst met bestandsnamen. De tweede regel plaatst het patroon tussen aanhalingstekens, waardoor find de vijf tekens *.log ontvangt en zelf de matching uitvoert, op elk niveau onder het startpunt. De derde regel bevat de bug: bash breidt het patroon eerst uit, uitsluitend binnen de huidige map, waardoor find de opdracht krijgt om naar één specifieke naam te zoeken. In een map met één overeenkomst zoekt het programma stilletjes naar het verkeerde object. Bij twee of meer overeenkomsten rapporteert find een gebruiks-fout, omdat de extra namen terechtkomen op de plek waar een expressie wordt verwacht.

De regel is kort. Als het patroon bedoeld is voor het commando, plaats het dan tussen aanhalingstekens. Als het bedoeld is voor de shell, laat het dan onveranderd. Hetzelfde onderscheid geldt voor --exclude-patronen in tar en rsync, en voor de --include-filters in grep.

Globs zijn geen reguliere expressies, ook al delen ze tekens. In een glob betekent * een willekeurige reeks tekens. In een reguliere expressie betekent * nul of meer van het voorgaande teken, waardoor grep '*.log' om iets heel anders vraagt dan het op het eerste gezicht lijkt.

Variabelen volgen dezelfde volgorde van bewerkingen. Globbing vindt plaats na variabele-expansie, dus een variabele zonder aanhalingstekens die een patroon bevat, wordt uitgebreid tegen de inhoud van de map.

pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"

Zonder aanhalingstekens voert bash globbing uit op het resultaat van de variabele. Met aanhalingstekens blijft het patroon letterlijk. Er is een gerelateerd gevolg waar u op kunt vertrouwen: de woorden die door globbing worden geproduceerd, worden nooit opnieuw gesplitst op spaties. Daarom verwerkt for f in *.log een bestandsnaam met een spatie als één naam, terwijl for f in $(ls *.log) die naam in twee woorden splitst. Gebruik de glob direct in een loop.

Bekijk een destructief patroon voordat u het uitvoert

Laat rm nooit het eerste commando zijn waarmee u een nieuw patroon test. Voer het patroon uit met iets ongevaarlijks, bekijk de lijst en wijzig daarna alleen het commando aan het begin van de regel.

target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $target

Het patroon één keer in target schrijven voorkomt dat de drie regels van elkaar gaan afwijken. $target staat bewust zonder aanhalingstekens, omdat bash het op die manier kan expanderen. printf '%s\n' print één naam per regel en wijzigt niets. ls -ld toont elk item zelf in plaats van de inhoud van mappen weer te geven. De kolom met de modus geeft aan welke items mappen zijn; dit is wat u moet controleren vóór elke recursieve verwijdering. De drwxr-xr-x permissiebits lezen behandelt deze kolom.

Roep de vorige regel op met de pijl-omhoog en bewerk alleen het commando, zodat het patroon identiek blijft. Het opnieuw typen van een patroon uit het hoofd is waar fouten insluipen.

Plaats -- vóór het patroon. Dit markeert het einde van de opties, zodat een bestandsnaam die begint met - als naam wordt behandeld in plaats van als vlag.

Als u globbing voor een deel van een script wilt uitschakelen, schakelt set -f het uit en set +f het weer in.

Deze gewoonte is belangrijk omdat rm geen ongedaan-functie heeft. Bestanden herstellen die met rm -rf zijn verwijderd is traag en meestal onvolledig; een voorbeeldweergave kost dus veel minder moeite dan het alternatief.

Ruim de oefenmap op wanneer u klaar bent. Print het pad en lees het eerst, want rm -rf dat naar de verkeerde variabele wijst, is precies het soort ongeluk waar deze sectie over gaat.

cd ~
echo "$lab"
rm -rf -- "$lab"

FAQ

Waarom vindt find . -name *.log de verkeerde bestanden?

Omdat bash *.log uitbreidt voordat find ooit start. De shell vergelijkt het patroon met de huidige map en geeft de resulterende bestandsnaam door aan find, die vervolgens op elke diepte naar die ene naam zoekt. Als twee of meer bestanden overeenkomen, ontvangt find extra argumenten waar het een expressie verwacht en rapporteert het een gebruiks-fout. Plaats het patroon tussen aanhalingstekens als find . -name '*.log' zodat find de matching zelf uitvoert, met zijn eigen regels en eigen recursie.

Wat betekent het als mijn patroon als letterlijke tekst verschijnt?

Dit betekent dat er geen overeenkomst is gevonden. Standaard laat bash een niet-overeenkomend patroon ongewijzigd en geeft de ruwe tekens door aan het commando, dat ze vervolgens als bestandsnaam behandelt. Stel shopt -s failglob in om een niet-overeenkomend patroon om te zetten in een fout die het commando stopt, of shopt -s nullglob om het naar niets te laten uitbreiden. Controleer bij nullglob of het commando nog steeds zinvol is zonder argumenten, omdat ls *.nope een kale ls wordt en de volledige map weergeeft.

Hoe match ik elk bestand behalve één patroon in bash?

Schakel uitgebreide patronen in met shopt -s extglob op een eigen regel en gebruik vervolgens de ontkenningsvorm. !(*.log) matcht namen die niet eindigen op .log, en !(*.log|*.md) sluit beide uit. Verborgen bestanden blijven buiten het resultaat tenzij dotglob ook is ingesteld, en mappen zijn inbegrepen, dus bekijk het patroon eerst met printf '%s\n' !(*.log) voordat u het aan rm doorgeeft.

Waarom slaat * verborgen bestanden over?

Een voorafgaande punt moet letterlijk worden gematcht, dus * matcht nooit een naam die daarmee begint. Gebruik .* om verborgen namen te bereiken, of stel shopt -s dotglob in om ze op te nemen in elk gewoon patroon. De items . en .. worden onder dotglob altijd uitgesloten, en bash 5.2 en later houden ze ook buiten .* via de optie globskipdots, die standaard is ingeschakeld.

Heb ik globstar nodig om ** te laten werken?

Ja. Zonder shopt -s globstar behandelt bash ** als een gewone *, die binnen één map matcht en daar stopt. Met globstar ingeschakeld matcht een ** die een volledig padcomponent vormt op elke diepte, dus **/*.log bereikt de huidige map en elke submap daaronder, en **/ op zichzelf matcht alleen mappen. Let op: ** volgt geen symbolische links naar mappen, dus gebruik find -L voor een boomstructuur die uit symlinks is opgebouwd.

#bash#globbing#shell#linux#scripting