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

359
Jim DeLaHunt

Я хочу написать /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 

Я думаю, что понимаю, почему это происходит: на странице руководства bash (которая, как я полагаю, также подходит /bin/sh) говорится, что «список следующих слов inрасширен, генерируя список элементов.… Если расширение элементов, следующих за, приводит к пустому список, команды не выполняются, а возвращаемый статус равен 0. " Видимо, когда *.ext«развернуто», список результатов включает *.extвместо пустого списка. Но это не объясняет, как это остановить.

(Обновление: существует sh (1)справочная страница из проекта «Семейная реликвия», в которой описана оболочка Bourne sh, а не более поздняя оболочка Bourne Again bash. В разделе « Создание имени файла» четко сказано: «Если не найдено имя файла, которое соответствует шаблону, слово остается без изменений. "Это объясняет, почему shоставляет *.extобразец в списке.)

Что представляет собой компактный идиоматичный способ написать этот цикл, чтобы он запускался ноль раз без ошибок, если нет подходящих файлов, но будет запускаться один раз для каждого соответствующего файла? Еще лучше то, что будет работать с несколькими шаблонами, такими как:

for f in *.ext1 special.ext1 *.ext2; do ... 

Я предпочитаю использовать синтаксис, совместимый с /bin/sh, для переносимости. Я использую Mac OS X 10.11.6, но я надеюсь на синтаксис, который работает на любой Unix-подобной ОС.

Я придумал пару неуклюжих, не идиоматических способов написать такой цикл. Если я не сразу получу хорошие ответы, я внесу их в протокол как ответы.

0
Это тривиально, если вы готовы перейти на bash. Ignacio Vazquez-Abrams 6 лет назад 0
@ IgnacioVazquez-Abrams Я предпочитаю придерживаться `/ bin / sh`, но это не абсолютно. Я думаю, что ответ формы «переключитесь на bash, затем сделайте это» полезен. Он не будет так хорош, как тот, который говорит: «Вот как это сделать в sh», но, возможно, такого ответа не существует. Jim DeLaHunt 6 лет назад 0
В bash есть `nullglob`, что предотвратит запуск цикла. Ignacio Vazquez-Abrams 6 лет назад 0
Большой! Превратите это в хороший ответ, и вы получите очки. Jim DeLaHunt 6 лет назад 0

2 ответа на вопрос

2
Gordon Davisson

Самый простой переносимый способ сделать это - пропустить цикл, если расширение не создает чего-то, что действительно существует:

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" ]т. Д. Завершаются успешно, и эти итерации цикла выполняются нормально.

Кстати, вы можете изменить это, например, [ -f "$f" ] || continueчтобы пропустить все, что не является простым файлом (если вы хотите пропустить каталоги и т. Д.).

Конечно, если вы работаете в bash, вы можете просто запустить shopt -s nullglobсначала, чтобы он не оставил в списке непревзойденных шаблонов. А затем shopt -u nullglobвпоследствии, чтобы предотвратить неожиданные побочные эффекты (например, grep pattern *.nonexistentбудет пытаться читать со стандартного ввода, если nullglobустановлено).

Умное использование `continue` и отличное объяснение. Я думаю, что использование `nullglob` превращается во второй ответ, если я правильно понимаю. @Ignacio Vazquez-Abrahams упомянул об этом в комментарии под вопросом. Jim DeLaHunt 6 лет назад 0
0
Jim DeLaHunt

Переключитесь на Bourne Again Shell ( /bin/bash) из Bourne Shell ( /bin/sh), и станет возможным простое решение.

На bash(1)странице man упоминается опция nullglob :

Если установлено, bash позволяет шаблонам, которые не соответствуют ни одному файлу (см. Выше «Путь к имени» ), раскрываться в пустую строку, а не в себя.

В разделе «Расширение пути » написано:

После слов расщепления, ... Баш сканирует каждое слово для символов *, ? и [ . Если появляется один из этих символов, слово считается шаблоном и заменяется отсортированным по алфавиту списком имен файлов, соответствующих шаблону. Если не найдено подходящих имен файлов, а параметр оболочки nullglob не включен, слово остается без изменений. Если установлена опция nullglob и совпадений не найдено, слово удаляется.…

Итак, установка параметра nullglob дает желаемое поведение для шаблонов. Если ни один файл не соответствует шаблону, цикл не выполняется.

#!/bin/bash shopt -s nullglob for f in *.ext; do handle "$f" done 

Но опция nullglob вполне может помешать желаемому поведению других команд. Таким образом, вы, вероятно, найдете целесообразным сохранить существующий параметр nullglob и восстановить его после этого. К счастью, 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 ... 

Параметр nullglob не влияет на слово в списке, которое не является «шаблоном», поэтому special.ext1все равно будет передано в цикл. Единственное решение этого @Gordon Дэвиссон «s continueвыражение [ -e "$f" ] || continue.

Выражение благодарности: на @Ignacio Vazquez-Abrams и @Gordon Davisson намекали bash(1)и его опция nullglob . Спасибо. Поскольку они не сразу дали ответ с полностью проработанным примером, я делаю это сам.