Синтаксис sh для обработки нуля файлов, соответствующих подстановочному символу, а также многое другое?

Синтаксис sh для обработки нуля файлов, соответствующих подстановочному символу, а также многое другое?

Я хочу написать /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)и егонульглобвариант. Спасибо. Поскольку они не сразу дали ответ с полностью проработанным примером, я делаю это сам.

Связанный контент