Как работает Bash globbing и расширение шаблонов
Узнайте, как Bash преобразует подстановочные знаки в список имен файлов до выполнения команды. Разбор работы nullglob, dotglob, globstar и особенностей синтаксиса extglob.
Что делает bash globbing до запуска вашей команды
Bash globbing — это процесс, в ходе которого оболочка превращает шаблон, такой как *.log, в отсортированный список существующих имен файлов еще до запуска команды. rm *.log никогда не передает rm сам шаблон. Bash считывает содержимое каталога, оставляет только подходящие по шаблону имена, сортирует их, а затем запускает rm, передавая каждое имя как отдельный аргумент. Программа rm не может определить, что изначально использовался какой-либо шаблон.
Почти все неожиданные моменты при работе с globbing вытекают из этого факта. Сопоставление выполняется оболочкой на основе файлов, существующих в данный момент, а команда получает только готовый список. Официальное название этого процесса — раскрытие путей (pathname expansion). 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.logmktemp -d создает новый каталог и возвращает путь к нему, а $(...) сохраняет этот путь в переменную lab. Если этот синтаксис вам не знаком, в том, как подстановка команд захватывает вывод команды всё подробно объяснено.
Посмотрите на созданную структуру и проверьте версию вашей оболочки, так как одно примечание в конце зависит от версии:
ls -a
bash --versionВсе примеры работают в bash 4.0 и более поздних версиях. Примечание globskipdots ближе к концу относится к bash 5.2 и новее, которые поставляются в текущих релизах Ubuntu и Debian по состоянию на август 2026 года.
Команда предварительного просмотра, используемая далее, — это 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 выполняет ту же задачу в конвейере.
Классы символов и диапазоны
Внутри квадратных скобок можно указать список, диапазон или отрицание.
[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, а оболочка уже успеет раскрыть шаблон в своей собственной локали до того, как ls начнет выполнение, что является повторением правила из первого раздела.
Раскрытие фигурных скобок — это не подстановка путей (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}.csvcp app.log{,.bak} раскрывается в два слова, app.log и app.log.bak, так как пустой элемент перед запятой воспроизводит исходный текст. {01..10} сохраняет дополнение нулями, поскольку наличие ведущего нуля в любой из границ заставляет bash дополнять каждое сгенерированное число до одинаковой ширины.
Порядок раскрытия также объясняет ошибку, с которой часто сталкиваются пользователи. Bash сначала раскрывает фигурные скобки, затем переменные, и в последнюю очередь выполняет подстановку путей.
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. Чтобы удалить его впоследствии, потребуется использовать кавычки, иначе оболочка снова раскроет шаблон: rm -- '*.tmp'.
Две опции оболочки меняют это правило. Попробуйте каждую из них с шаблоном, который не находит совпадений, а затем отключите её.
shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob
shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglobnullglob заставляет нераскрытый шаблон превращаться в пустую строку, поэтому команда выполняется с меньшим количеством аргументов, чем вы указали. Это именно то, что требуется при работе с циклами. Если nullglob выключен, for f in *.tmp выполняет тело цикла один раз, при этом f содержит литеральный шаблон, что является ошибкой почти в любом скрипте. Если nullglob включен, тело цикла выполняется ноль раз.
nullglob несет в себе скрытую угрозу, так как команда, лишенная всех аргументов, всё равно выполняется. ls *.nope превращается в обычный ls, который выводит содержимое всего каталога. grep -l needle *.nope превращается в grep -l needle, которому нечего читать, поэтому он ожидает ввода из стандартного потока (stdin) и выглядит так, будто завис. Включайте 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 не установлена, ** ведет себя как обычный * и не выходит за пределы одного каталога. Запустите тот же шаблон с выключенной опцией для сравнения: если рекурсивный шаблон, скопированный из стороннего источника, возвращает гораздо меньше результатов, чем ожидалось, причина обычно кроется именно в этом. globstar по умолчанию выключена во всех версиях bash.
Важное ограничение: ** не переходит по символическим ссылкам на каталоги. Для дерева, собранного с помощью символических ссылок, потребуется find -L.
nocaseglob для имен, различающихся регистром
shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglobnocaseglob заставляет сопоставление имен файлов игнорировать регистр, поэтому суффикс в верхнем регистре .LOG совпадает с шаблоном в нижнем регистре .log. Сравните этот список с тем, что был в первом разделе. Сразу же отключите эту опцию, так как оставленная включенной, она меняет правила сопоставления для всех последующих команд в этой оболочке, что крайне затрудняет отладку спустя час работы.
nocaseglob применяется только к раскрытию имен файлов. Аналогичная опция для сопоставления шаблонов внутри оператора case или проверки в двойных скобках — это nocasematch, и они настраиваются независимо друг от друга.
extglob: шаблоны, позволяющие исключить элементы
Расширенные шаблоны по умолчанию отключены. Включите их отдельной строкой:
shopt -s extglobСуществует пять форм, каждая из которых принимает список шаблонов, разделенных |:
?(list)соответствует нулю или одному вхождению.*(list)соответствует нулю или более вхождений.+(list)соответствует одному или более вхождений.@(list)соответствует ровно одному из альтернативных вариантов.!(list)соответствует всему, что не совпадает ни с одним из альтернативных вариантов.
Расширенный шаблон должен считываться оболочкой, в которой эта опция уже включена, поэтому приведенный ниже пакет команд помещается в небольшой скрипт, запускаемый с помощью bash -O extglob. Флаг -O устанавливает опцию shopt до того, как новая оболочка считает первую строку файла, что гарантирует корректный разбор всех шаблонов. Последний абзац этого раздела объясняет, почему порядок действий имеет значение.
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. Он не имеет отношения к расширению истории и символу восклицательного знака, так как это отдельная функция, работающая на другом этапе.
Ещё одна ловушка связана с моментом разбора. Bash разбирает полную команду перед её выполнением, поэтому включение extglob и использование расширенного шаблона в одной строке приведет к ошибке: шаблон разбирается в тот момент, когда опция ещё выключена. Держите shopt -s extglob на отдельной строке перед шаблонами, ближе к началу скрипта. Тело функции разбирается в момент её определения, поэтому опция должна быть включена до определения, а не до вызова. Запуск оболочки с 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.
Glob-шаблоны не являются регулярными выражениями, хотя и используют общие символы. В glob-шаблоне * означает любую последовательность символов. В регулярном выражении * означает ноль или более вхождений предыдущего элемента, поэтому grep '*.log' запрашивает совсем не то, что кажется на первый взгляд.
Переменные подчиняются тому же порядку операций. Раскрытие glob-шаблонов происходит после раскрытия переменных, поэтому не заключенная в кавычки переменная, содержащая шаблон, будет раскрыта относительно содержимого каталога.
pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"Без кавычек bash применяет glob-раскрытие к результату переменной. В кавычках шаблон остается литералом. Из этого следует важное следствие: слова, полученные в результате раскрытия glob-шаблонов, больше не разбиваются по пробелам. Поэтому for f in *.log корректно обрабатывает имя файла с пробелом как одно имя, в то время как for f in $(ls *.log) разбивает это имя на два слова. Используйте цикл непосредственно по результату glob-шаблона.
Предварительный просмотр деструктивного шаблона перед выполнением
Никогда не используйте 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, примененный к неверной переменной, — это именно та случайность, о которой идет речь в этом разделе.
cd ~
echo "$lab"
rm -rf -- "$lab"FAQ
Почему find . -name *.log находит не те файлы?
Потому что bash раскрывает *.log до того, как запускается 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, а каталоги включаются в выборку, поэтому предварительно проверьте шаблон с помощью printf '%s\n' !(*.log), прежде чем передавать его в rm.
Почему * пропускает скрытые файлы?
Точка в начале имени должна сопоставляться буквально, поэтому * никогда не находит имена, начинающиеся с неё. Используйте .* для доступа к скрытым именам или установите shopt -s dotglob, чтобы включить их во все обычные шаблоны. Записи . и .. всегда исключаются при включенном dotglob, а в bash 5.2 и более поздних версиях они также исключаются из .* благодаря опции globskipdots, которая включена по умолчанию.
Нужен ли globstar для работы **?
Да. Без shopt -s globstar bash воспринимает ** как обычный *, который ищет только внутри одного каталога и останавливается. При включенном globstar **, составляющий отдельный компонент пути, сопоставляется на любой глубине, поэтому **/*.log охватывает текущий каталог и все подкаталоги, а **/ сам по себе находит только каталоги. Учтите, что ** не переходит по символическим ссылкам на каталоги, поэтому используйте find -L для деревьев, построенных на симлинках.