Я хочу написать /bin/sh
скрипт оболочки, который будет обрабатывать любые файлы, соответствующие подстановочному символу. Легко обрабатывать 1 или более соответствующих файлов. Однако, я нахожу неудобным обрабатывать случай 0 соответствующих файлов.
Очевидная конструкция такова:
#!/bin/sh
for f in *.ext; do
handle "$f"
done
где *.ext
может быть одно или несколько выражений, которые оболочка сравнивает с путями к файлам, и handle
является командой оболочки, которая выполняется правильно, если указан путь к существующему файлу, но завершается ошибкой, если указан путь, который не соответствует файлу. (В случае, который вызывает этот вопрос, это *.flac
и ffmpeg
, но я думаю, что это не имеет значения.)
Если есть соответствующие файлы foo.ext
, bar.ext
то этот скрипт выполняет
handle "foo.ext"
handle "bar.ext"
как и ожидалось. Однако, если естьнетлюбые соответствующие файлы, этот скрипт выдает сообщение об ошибке, например,
handle: *.ext: No such file or directory
Я думаю, я понимаю, почему это происходит:Башстраница руководства(что, как я предполагаю, справедливо /bin/sh
также для ) говорит, что "список следующих слов in
расширяется, генерируя список элементов.… Если расширение следующих элементов приводит к пустому списку, никакие команды не выполняются, а возвращаемый статус равен 0". Очевидно, когда *.ext
"расширяется", список результатов включает *.ext
, вместо пустого списка. Но это не объясняет, как это остановить.
(Обновление: естьsh (1)
man-страница проекта Heirloomкоторый описывает Bourne Shell sh
, а не более позднюю Bourne Again Shell bash
. ПодГенерация имени файла, там четко сказано: «Если не найдено ни одного имени файла, соответствующего шаблону, то слово остается без изменений». Это объясняет, почему в списке sh
остается шаблон.)*.ext
Какой компактный идиоматический способ написать этот цикл так, чтобы он не запускался ни разу без ошибок, если нет соответствующих файлов, но запускался один раз для каждого соответствующего файла? А еще лучше, что будет работать с несколькими шаблонами, например:
for f in *.ext1 special.ext1 *.ext2; do ...
Я предпочитаю использовать синтаксис, совместимый с /bin/sh
, для переносимости. Я использую Mac OS X 10.11.6, но надеюсь на синтаксис, который работает на любой Unix-подобной ОС.
Я придумал пару неуклюжих, неидиоматических способов написать такой цикл. Если я не получу сразу хороших ответов, я внесу их в качестве ответов, для протокола.
решение1
Самый простой переносимый способ сделать это — пропустить цикл, если расширение не создает ничего, что на самом деле существует:
for f in *.ext1 *.ext2; do
[ -e "$f" ] || continue
handle "$f"
done
Объяснение: предположим, что есть несколько файлов .ext2, но нет файлов .ext1. В этом случае подстановочные знаки будут расширяться до *.ext1
filea.ext2
fileb.ext2
filec.ext2
. Поэтому [ -e "*.ext1" ]
происходит сбой и запускается continue
, что пропускает оставшуюся часть этой итерации цикла. Затем [ -e "filea.ext2" ]
etc завершаются успешно, и эти итерации цикла выполняются нормально.
Кстати, вы можете изменить это, например, [ -f "$f" ] || continue
чтобы пропускать все, что не является простым файлом (если вы хотите пропускать каталоги и т. д.).
Конечно, если вы работаете под bash, вы можете просто запустить shopt -s nullglob
сначала, чтобы не оставлять несовпадающих шаблонов в списке. А затем shopt -u nullglob
после, чтобы предотвратить неожиданные побочные эффекты (например, grep pattern *.nonexistent
попытается прочитать из stdin, если nullglob
установлено).
решение2
Переключитесь /bin/bash
из Bourne Shell ( ) в Bourne Again Shell ( /bin/sh
), и простое решение станет возможным.
Thebash(1)
страница руководстваупоминаетнульглобвариант:
Если установлено, bash допускает шаблоны, не соответствующие ни одному файлу (см.Расширение имени путивыше) расширяться до нулевой строки, а не самих себя.
TheРасширение имени путираздел гласит:
После разделения слов …bash сканирует каждое слово на наличие символов*,?, и[. Если появляется один из этих символов, то слово рассматривается как шаблон и заменяется отсортированным в алфавитном порядке списком имен файлов, соответствующих шаблону. Если соответствующие имена файлов не найдены, и опция оболочкинульглобне включен, слово остается неизменным. Еслинульглобопция установлена, и совпадений не найдено, слово удаляется.…
Итак, установивнульглобoption задает желаемое поведение для шаблонов. Если ни один файл не соответствует шаблону, цикл не выполняется.
#!/bin/bash
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
Нонульглобoption может помешать желаемому поведению других команд. Поэтому, вы, вероятно, сочтете разумным сохранить существующую настройкунульглоб, и восстановить его впоследствии. К счастью, shopt -p
встроенная команда выдает вывод в форме, которую можно повторно использовать в качестве ввода. Но она выдает вывод для всех опций, поэтому используйте grep
для выбораnullobjнастройка. Смотрите журнал ниже (где $
указывает на командную строку bash):
$ shopt -p | grep nullglob
shopt -u nullglob
$ shopt -s nullglob
$ shopt -p | grep nullglob
shopt -s nullglob
Итак, окончательный синтаксис bash для обработки нуля или более файлов, соответствующих шаблону подстановочных знаков, выглядит следующим образом:
#!/bin/bash
SAVED_NULLGLOB=$(shopt -p | grep nullglob)
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
eval "$SAVED_NULLGLOB"
Кроме того, в вопросе упоминалось несколько шаблонов, например:
for f in *.ext1 special.ext1 *.ext2; do ...
Theнульглобoption не влияет на слово в списке, которое не является "шаблоном", поэтому special.ext1
все равно будет передано в цикл. Единственное решение для этого -@Гордон Дэвиссонвыражение лица continue
, [ -e "$f" ] || continue
.
Подтверждение: оба@Игнасио Васкес-Абрамси@Гордон Дэвиссонупоминается bash(1)
и егонульглобвариант. Спасибо. Поскольку они не сразу дали ответ с полностью проработанным примером, я делаю это сам.