SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor

Bash globbing: wildcard, nullglob і extglob

Розберіться, як Bash розгортає wildcard у файли до запуску команди, і коли вмикати nullglob, dotglob, globstar та extglob для безпечних скриптів.

Що робить bash globbing до запуску команди

Bash globbing — це перетворення оболонкою шаблону, наприклад *.log, на відсортований список наявних імен файлів до запуску команди. rm *.log ніколи не передає rm шаблон. Bash читає каталог, залишає імена, які відповідають шаблону, сортує їх, а потім запускає rm, передаючи кожне ім’я як окремий аргумент. Програма rm не може визначити, що шаблон взагалі використовувався.

Майже все, що здається несподіваним у globbing, випливає з цього факту. Оболонка виконує зіставлення з файлами, наявними в цей момент, а команда отримує лише готовий список. Формальна назва цього процесу — розгортання шляхів. Globbing — повсякденний термін; обидва терміни означають те саме.

Створіть невелике дерево для практики

Усі наведені нижче приклади працюють з одними й тими самими файлами. Створіть їх у тимчасовому каталозі, щоб поруч не було важливих даних і все можна було видалити однією командою.

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 створює новий каталог і повертає його шлях, а $(...) зберігає цей шлях у змінній lab. Якщо цей синтаксис для вас новий, у матеріалі як підстановка команд отримує вивід команди наведено повне пояснення.

Перегляньте створену структуру та перевірте свою оболонку, оскільки одна примітка наприкінці залежить від версії:

ls -a
bash --version

Усе це працює в bash 4.0 і новіших версіях. Примітка щодо globskipdots наприкінці стосується bash 5.2 і новіших версій. Станом на August 2026 саме ці версії постачаються з актуальними випусками Ubuntu та Debian.

У всіх прикладах використовується команда попереднього перегляду printf '%s\n' PATTERN. Вона виводить один збіг у кожному рядку й нічого не змінює на диску, тому її безпечно запускати з будь-яким шаблоном, щодо якого ви не впевнені.

Три шаблони: зірочка, знак питання та квадратні дужки

  • * відповідає будь-якій послідовності символів, зокрема порожній.
  • ? відповідає рівно одному символу.
  • [...] відповідає рівно одному символу з набору всередині квадратних дужок.
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *

Порівняйте другий рядок зі списком файлів, який ви створили. ? позначає один символ і не більше, тому суфікс із двох цифр не відповідає цьому шаблону. Розширте його до app.log.[0-9]* і запустіть команду ще раз, щоб побачити різницю.

До всіх трьох шаблонів застосовуються два правила. Жоден шаблон не відповідає /, тому він залишається в межах одного рівня каталогів. Початковий . потрібно вводити буквально, тому шаблон, що починається з *, пропускає всі приховані імена. Саме тому rm * не обробляє ваші dotfiles.

Результати glob сортуються за порядком зіставлення поточної локалі. Це посимвольне текстове сортування. Тому суфікс 10 сортується перед суфіксом 2, оскільки під час порівняння жоден із них не інтерпретується як число. Запустіть printf '%s\n' app.log.*, а потім ls -v app.log.* і порівняйте результати: ls -v сортує вбудовані числа за їхнім значенням, а sort -V виконує те саме в pipeline.

Класи символів і діапазони

У квадратних дужках можна записати список, діапазон або заперечення.

  • [ch] відповідає одному символу: c або h.
  • [a-f] відповідає одному символу в діапазоні від a до f.
  • [!0-9] відповідає одному символу, який не є цифрою. [^0-9] означає те саме.
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*

Діапазони — це частина, яка працює по-різному в різних системах. Діапазон визначається за порядком сортування поточної локалі, а не за простим порядком ASCII, тому в деяких системах [a-z] також відповідає великим літерам. У Bash є параметр globasciiranges, який примусово задає звичайний порядок ASCII. У нових збірках його типово ввімкнено, тому перевірте це командою shopt globasciiranges, а не робіть припущень. Скрипт, який має однаково працювати всюди, може явно зафіксувати порядок, задавши LC_ALL=C окремим рядком ближче до початку. Це повертає всі діапазони до звичайного порядку байтів.

Це присвоєння має бути окремою командою. Якщо записати його як префікс, LC_ALL=C ls [a-z]* задає локаль для ls і більше ні для чого. Shell уже розгорнув шаблон у власній локалі до того, як ls почав виконання. Це знову підтверджує правило з першого розділу.

Розгортання дужок — це не globbing

Фігурні дужки схожі на wildcard, але працюють зовсім інакше. Розгортання дужок відбувається набагато раніше за globbing і ніколи не перевіряє файлову систему.

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

Жоден із цих файлів не існує, але розгортання все одно виконується. У цьому і полягає різниця: glob ставить запит файловій системі, а дужки лише генерують текст. Тому дужки підходять для створення об’єктів, а не для їх пошуку.

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

cp app.log{,.bak} розгортається у два слова — app.log і app.log.bak, оскільки порожній елемент перед комою відтворює початковий текст. {01..10} зберігає доповнення нулями, оскільки початковий нуль у будь-якій із меж змушує bash доповнювати всі згенеровані числа до однакової ширини.

Порядок розгортання також пояснює поширену помилку. Bash спочатку розгортає дужки, потім змінні, а pathname expansion виконує в останню чергу.

n=5
echo {1..$n}

Розгортання дужок уже завершилося в момент, коли $n перетворюється на число, тому діапазон не створюється. Використовуйте seq 1 "$n" або цикл у стилі C, записаний як for ((i=1; i<=n; i++)).

Що відбувається, коли шаблон нічого не знаходить

Це перший сценарій збою, з яким одного разу стикаються всі. Типово bash залишає шаблон, який нічого не знаходить, без змін. Слово передається команді саме в тому вигляді, у якому ви його ввели, тому команда отримує символи *, ., t, m і p та намагається сприйняти їх як ім’я файлу.

printf '%s\n' *.tmp

Для printf це безпечно. Але для команди, яка створює об’єкти, це небезпечно. touch *.tmp у каталозі без файлів .tmp створює файл з буквальним ім’ям *.tmp. Щоб видалити його пізніше, потрібне екранування, інакше shell знову розгорне цей шаблон: rm -- '*.tmp'.

Два параметри shell змінюють це правило. Перевірте кожен із них на шаблоні, який нічого не знаходить, а потім вимкніть його.

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

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

nullglob змушує шаблон, який нічого не знаходить, розгортатися в порожній рядок. У результаті команда запускається з меншою кількістю аргументів, ніж ви вказали. Саме цього потрібно досягти в циклі. Коли nullglob вимкнено, for f in *.tmp виконує тіло циклу один раз, а f містить буквальний шаблон. Майже в кожному скрипті це помилка. Коли nullglob увімкнено, тіло циклу не виконується жодного разу.

nullglob має власну небезпеку, оскільки команда без усіх аргументів усе одно запускається. ls *.nope перетворюється на окрему команду ls, яка виводить увесь вміст каталогу. grep -l needle *.nope перетворюється на grep -l needle без файлу для читання, тому команда очікує введення зі стандартного вводу й виглядає так, ніби зависла. Увімкніть nullglob навколо циклу, якому він потрібен, а після циклу знову вимкніть.

failglob використовує інший підхід: шаблон, який нічого не знаходить, вважається помилкою. bash повідомляє про неї, а команда взагалі не запускається. Це добре налаштування для інтерактивної оболонки, оскільки воно не дає передати помилково введений шаблон до rm як буквальне ім’я.

Приховані файли: dotglob

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

Виконайте цей блок і порівняйте список із простим printf '%s\n' * з попереднього прикладу. dotglob прибирає правило про початкову крапку, тому * також відповідає прихованим іменам. Елементи . і .. за dotglob завжди залишаються виключеними.

Без dotglob звичайним способом отримати доступ до прихованих файлів є .*, але раніше це було небезпечно. У bash до версії 5.2 .* також відповідав . і .., тому рекурсивна команда, наприклад chmod -R 755 .*, одразу переходила до батьківського каталогу. У Bash 5.2 додано параметр globskipdots, увімкнений за замовчуванням, який виключає . і .. з усіх розгортань. Виконайте shopt globskipdots, перш ніж покладатися на цю поведінку, оскільки на старій системі цього параметра не буде.

globstar: рекурсивний пошук у всьому дереві

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

Якщо встановлено globstar, **, який утворює повний компонент шляху, відповідає файлам і каталогам на будь-якій глибині, зокрема на нульовому рівні вкладеності. Тому **/*.log охоплює поточний каталог, а також logs/ і все всередині archive/. **/ із завершальною косою рискою відповідає лише каталогам. Це зручний швидкий спосіб переглянути структуру дерева.

Без встановленого globstar ** працює як звичайний * і залишається в одному каталозі. Запустіть той самий шаблон із вимкненою опцією та порівняйте результат. Якщо рекурсивний шаблон, скопійований з іншого джерела, повертає набагато менше результатів, ніж очікувалося, найімовірніше, причина саме в цьому. У bash globstar типово вимкнено.

Варто знати про одне обмеження: ** не переходить за символічними посиланнями на каталоги. Для дерева, об’єднаного символічними посиланнями, потрібен find -L.

nocaseglob, для імен, у яких відрізняється регістр

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

nocaseglob змушує зіставлення імен файлів ігнорувати регістр, тому суфікс у верхньому регістрі .LOG відповідає шаблону в нижньому регістрі .log. Порівняйте цей список із результатом з першого розділу. Одразу знову вимкніть цей параметр, оскільки в увімкненому стані він змінює зіставлення для кожної наступної команди в цій оболонці, а через годину таку причину помилки буде складно знайти.

nocaseglob застосовується лише до розгортання імен файлів. Пов’язаний параметр для зіставлення шаблонів усередині оператора case або тесту з подвійними квадратними дужками — nocasematch. Їх потрібно встановлювати окремо.

extglob: шаблони для вибору всього, крім заданого

Розширені шаблони за замовчуванням вимкнені. Увімкніть їх окремим рядком:

shopt -s extglob

Є п’ять форм. Кожна приймає список шаблонів, розділених символом |:

  • ?(list) відповідає нульовій або одній появі.
  • *(list) відповідає нульовій або кільком появам.
  • +(list) відповідає одній або кільком появам.
  • @(list) відповідає рівно одному з варіантів.
  • !(list) відповідає всьому, що не відповідає жодному з варіантів.

Розширений шаблон має обробляти shell, у якому цю опцію вже ввімкнено. Тому наведений нижче блок потрібно розмістити в невеликому скрипті, запущеному з bash -O extglob. Прапорець -O встановлює опцію shopt до того, як новий shell прочитає перший рядок файлу. Завдяки цьому всі шаблони в ньому правильно розбираються. Останній абзац цього розділу пояснює, чому порядок має значення.

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]) означає одну або кілька цифр, тому відповідає числовому суфіксу будь-якої довжини. Це виправляє обмеження ?, розглянуте раніше. Варто виконати обидва шаблони послідовно, щоб побачити різницю.

Форма заперечення відповідає запиту «все, крім», і саме заради неї зазвичай вмикають extglob. Є два моменти, які часто дивують користувачів.

Вона відповідає не лише файлам, а й каталогам. Тому ls !(*.log) додає до результату вміст кожного відповідного каталогу, а не назву самого каталогу. Використовуйте ls -d або перегляд printf, щоб побачити самі назви.

Вона також дотримується правила для крапки, як і будь-який інший glob. Тому !(*.log) відповідає кожній видимій назві, яка не закінчується на .log. Для прихованих назв потрібно також встановити dotglob.

! у !(...) — це заперечення glob. Воно не пов’язане з розгортанням історії та символом bang, оскільки це окрема функція, яка виконується на іншому етапі.

Є ще одна пастка, і вона виникає під час розбору. Bash розбирає всю команду до виконання будь-якої її частини. Тому ввімкнення extglob і використання розширеного шаблону в одному рядку не працює: шаблон розбирається, поки опція ще вимкнена. Розміщуйте shopt -s extglob в окремому рядку перед шаблонами та ближче до початку скрипту. Тіло функції розбирається під час її визначення, тому опцію потрібно ввімкнути до визначення функції, а не до її виклику. Запуск shell із bash -O extglob, як у наведеному вище блоці, встановлює правильний порядок ще до початку розбору.

Оболонка розгортає шаблон, а команда — ні

Це другий режим відмови. Він пояснює цілу групу помилок.

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

У першому рядку bash виконує зіставлення, а grep отримує список імен файлів. У другому рядку шаблон узято в лапки, тому find отримує п’ять символів *.log і виконує власне зіставлення на кожному рівні нижче початкового каталогу. Третій рядок містить помилку: bash спочатку розгортає шаблон лише в поточному каталозі, тому find отримує вказівку шукати одне конкретне ім’я. У каталозі з одним збігом команда непомітно шукає не те, що потрібно. Якщо збігів два або більше, find повідомляє про помилку використання, оскільки додаткові імена потрапляють на місце, де команда очікує вираз.

Правило просте. Якщо шаблон призначений для команди, візьміть його в лапки. Якщо шаблон призначений для оболонки, не беріть його в лапки. Це саме розділення діє для шаблонів --exclude у tar і rsync, а також для фільтрів --include у grep.

Глоби також не є регулярними виразами, хоча містять деякі спільні символи. У глобі * означає будь-яку послідовність символів. У регулярному виразі * означає нуль або більше повторень попереднього елемента, тому grep '*.log' задає зовсім іншу умову, ніж може здаватися.

Змінні обробляються в такому самому порядку. Розгортання глобів відбувається після розгортання змінних, тому значення змінної без лапок, що містить шаблон, буде розгорнуто відносно поточного каталогу.

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

Без лапок bash розгортає результат підстановки змінної як глоб. У лапках шаблон залишається буквальним текстом. Є пов’язаний наслідок, на який слід зважати: слова, утворені внаслідок розгортання глобу, повторно не розділяються за пробілами. Тому for f in *.log обробляє ім’я файлу з пробілом як одне ім’я, а for f in $(ls *.log) розділяє це ім’я на два слова. Перебирайте глоб безпосередньо.

Перевірте деструктивний шаблон перед виконанням

Ніколи не використовуйте rm як першу команду для перевірки нового шаблону. Спочатку виконайте шаблон із безпечною командою, перегляньте список, а потім змініть лише команду на початку рядка.

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

Якщо один раз записати шаблон у target, три рядки не розходитимуться. $target навмисно залишено без лапок, оскільки саме це дає змогу bash розгорнути шаблон. printf '%s\n' виводить по одному імені в кожному рядку й нічого не змінює. ls -ld показує самі записи, а не вміст каталогів. У стовпці режиму позначено, які записи є каталогами. Саме це потрібно перевірити перед рекурсивним видаленням. Про цей стовпець дивіться в розділі Як читати біти дозволів drwxr-xr-x.

Поверніть попередній рядок клавішею зі стрілкою вгору та змініть лише команду, щоб шаблон залишився ідентичним. Помилки часто виникають, коли шаблон повторно вводять з пам’яті.

Поставте -- перед шаблоном. Він позначає кінець параметрів. Тому ім’я файлу, що починається з -, буде оброблено як ім’я, а не як прапорець.

Якщо потрібно на певній ділянці сценарію вимкнути globbing, set -f вимикає його, а set +f знову вмикає.

Ця звичка важлива, оскільки rm не має функції скасування. Відновлення файлів, видалених за допомогою rm -rf, описане в Відновлення файлів, видалених за допомогою rm -rf, займає багато часу й зазвичай не дає повного результату. Тому попередній перегляд набагато дешевший за наслідки помилки.

Після завершення очистьте робочий каталог. Виведіть шлях і спочатку перевірте його, оскільки rm -rf, застосований до неправильної змінної, — саме такий випадок, про який ідеться в цьому розділі.

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

FAQ

Чому find . -name *.log знаходить не ті файли?

Тому що bash розгортає *.log ще до запуску find. Оболонка зіставляє шаблон із поточним каталогом і передає ім’я отриманого файлу до find. Після цього find шукає це ім’я на кожному рівні вкладеності. Якщо шаблону відповідають два або більше файлів, find отримує додаткові аргументи там, де очікує вираз, і повідомляє про помилку використання. Візьміть шаблон у лапки як find . -name '*.log', щоб зіставлення виконував сам find за власними правилами та з власною рекурсією.

Що означає, якщо мій шаблон відображається як звичайний текст?

Це означає, що нічого не знайдено. За замовчуванням bash залишає шаблон без змін, якщо йому не відповідає жоден файл, і передає необроблені символи команді. Команда сприймає їх як ім’я файлу. Установіть shopt -s failglob, щоб перетворити невідповідний шаблон на помилку, яка зупиняє команду, або shopt -s nullglob, щоб розгорнути його в порожній рядок. За використання nullglob перевірте, чи має команда сенс без аргументів, оскільки ls *.nope перетворюється на окремий ls і виводить увесь каталог.

Як у bash зіставити всі файли, крім файлів, що відповідають одному шаблону?

Окремим рядком увімкніть розширені шаблони за допомогою shopt -s extglob, а потім використайте форму заперечення. !(*.log) відповідає іменам, які не закінчуються на .log, а !(*.log|*.md) виключає обидва варіанти. Приховані файли не потрапляють до результату, якщо додатково не встановлено dotglob. До результату також входять каталоги, тому перед передаванням шаблону до rm перевірте його за допомогою printf '%s\n' !(*.log).

Чому * пропускає приховані файли?

Початкову крапку потрібно зіставляти явно, тому * ніколи не відповідає імені, яке починається з крапки. Використайте .* для пошуку прихованих імен або встановіть shopt -s dotglob, щоб включити їх до всіх звичайних шаблонів. Записи . і .. завжди виключаються за використання dotglob. У bash 5.2 і новіших версіях вони також виключаються з .* завдяки параметру globskipdots, який увімкнено за замовчуванням.

Чи потрібен globstar, щоб працював **?

Так. Без shopt -s globstar bash сприймає ** як звичайний *. Він відповідає лише в межах одного каталогу й не переходить далі. Якщо globstar увімкнено, **, який утворює цілий компонент шляху, відповідає на будь-якій глибині. Тому **/*.log охоплює поточний каталог і всі його підкаталоги, а **/ окремо відповідає лише каталогам. Зверніть увагу: ** не переходить за символьними посиланнями на каталоги. Для дерева, побудованого із символьних посилань, використовуйте find -L.

#bash#globbing#shell#linux#scripting