Я хочу написать сценарий оболочки /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 вместо пустой список Но это не объясняет, как это остановить.

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

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

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

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

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

2 ответа2

2

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

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 будет пытаться читать из stdin, если установлен nullglob ).

0

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

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

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

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

После разделения слов… 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 все равно будет передан в цикл. Единственное решение для этого - continue выражения @Gordon Davisson , [ -e "$f" ] || continue

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

Всё ещё ищете ответ? Посмотрите другие вопросы с метками .