Использование IFS для обработки пробелов в именах файлов

На вопрос Как переименовать все файлы в папке с именем, оканчивающимся на «_backup» @Radu Rădeanu дал хороший ответ, который также пригодился мне:

find . -type f -name '*.jpg_backup' -print0 \
| while IFS= read -r -d '' file ; do mv -- "$file" \
"$(echo $file | sed 's/_backup//g')"; done

Однако Я бы хотел полностью понять его однострочность. Точно, часть, которую я не понимаю:

while IFS= read -r -d '' file

Я понимаю, что IFS является «внутренним разделителем полей», и я предполагаю, что это удаляет или игнорирует пробелы, но я не понимаю синтаксис и варианты.

Я также хотел бы понять, почему -- необходимо после mv.

Может ли кто-нибудь помочь? Благодаря.

5
задан 13 April 2017 в 15:24

2 ответа

read word1 word2 … rest выполняет следующее:

  1. Читает строку.
  2. Пока текущий ввод заканчивается обратной косой чертой, прочитайте еще одну строку и добавьте ее к первой, за вычетом обратной косой черты.
  3. Разбейте ввод на отдельные слова, где любой символ в значении переменной IFS считается разделителем слов.
  4. Присвойте первое слово переменной word, второе слово переменной word2 и т. Д.
  5. Если остались слова, они присваиваются переменной rest, с сохранением оригинальных разделителей слов.

Опция -r деактивирует шаг 2, поэтому \ в конце строки не считается разделителем слов.

Если для IFS задана пустая строка, разделителей слов нет, поэтому вся строка представляет собой одно большое слово: шаг 3 ничего не делает, а шаг 5 заканчивается назначением исходной строки указанной переменной. Относительно того, почему IFS установлено в этом месте, см. Почему while IFS= read используется так часто, вместо IFS=; while read..? .

Опция -d изменяет понятие строки: обычно строка заканчивается символом новой строки; с -d '' (пустой аргумент для -d), bash вместо этого читает входные записи, разделенные нулевым байтом.

В результате find … -print0 | while IFS= read -r -d '' file; do … выполняет тело цикла для каждого совпадения, которое печатает find, независимо от любых специальных символов, которые могут встречаться в имени файла.


Что касается -- в mv -- "$file", то оно есть в том случае, если значение file начинается с тире: mv интерпретирует его как опцию. В данном конкретном случае в этом нет необходимости, поскольку выход этой команды find всегда начинается с ./. Систематическое использование -- в сценариях, возможно, является хорошей гигиеной.


В этом фрагменте есть дефект: он по-прежнему не справляется с именами файлов, содержащими пробелы или \[?*, из-за бита "$(echo $file | sed 's/_backup//g')". Расширение переменной, такое как $file, не просто подставляет значение переменной: оно разбивает переменную на слова, используя значение IFS (как это делает read), и обрабатывает каждое слово как шаблон подстановочного знака, который заменяется списком подходящих файлов, если он совпадает с любым. Чтобы избежать такого поведения, напишите "$file". Это общее правило программирования оболочки: всегда ставьте двойные кавычки вокруг подстановок переменных (и подстановок команд $(…) тоже), если только вы не знаете, почему их нужно опускать. (Если вам нужна мелочь, см. Когда необходимо заключать в двойные кавычки? ; $ VAR vs $ {VAR} и цитировать или не цитировать и . Что такое значимость одинарных и двойных кавычек в переменных среды? также может представлять интерес).

Быстрое исправление заключается в добавлении двойных кавычек:

mv -- "$file" "$(echo "$file" | sed 's/_backup//g')"

Это работает здесь из-за способа ввода данных, но для общих значений file он терпит неудачу в нескольких редких случаях. случаи:

  • Если значение file состоит из символа -, за которым следуют одна или несколько букв среди Een, то echo будет рассматривать его как опцию и ничего не выводить.
  • Подстановка команд пожирает последние символы новой строки выходных данных, поэтому, если $file заканчивается одним или несколькими символами новой строки, они будут усечены.

Bash имеет строковую подстановку, которую можно использовать вместо sed здесь. Это более надежно (вам не нужно беспокоиться о тонкостях со специальными символами) и быстрее.

mv -- "$file" "${file//_backup/}"

Несмотря на то, что задан вопрос , это неправильная операция: она удаляет _backup в любом месте имени файла, а не только в конце. Здесь сделать это правильно будет проще.

mv -- "$file" "${file%_backup}"
0
ответ дан 13 April 2017 в 15:24

Если у вас есть имя файла, начинающееся с дефиса, вам нужно -- отделить опции mv от фактических аргументов имени файла.

Если у вас есть имя файла, которое начинается или оканчивается пробелами, то заклинание чтения с IFS= обеспечит, чтобы переменная file содержала именно то имя файла, которое вам нужно.

Опция -d '' считывает разделенные нулем байты «строки» вместо строк, разделенных новой строкой - это потому, что вы используете опцию find -print0.

0
ответ дан 13 April 2017 в 15:24

Другие вопросы по тегам:

Похожие вопросы: