Зацикливание файлов с пробелами в именах?

Я написал следующий сценарий, чтобы различать выходы двух директорий со всеми теми же файлами в них как таковые:

#!/bin/bash for file in `find . -name "*.csv"` do echo "file = $file"; diff $file /some/other/path/$file; read char; done 

Я знаю, что есть другие способы добиться этого. Любопытно, однако, этот скрипт терпит неудачу, когда в файлах есть пробелы. Как я могу справиться с этим?

Пример вывода find:

 ./zQuery - abc - Do Not Prompt for Date.csv 

Короткий ответ (ближе всего к вашему ответу, но обрабатывает пробелы)

 OIFS="$IFS" IFS=$'\n' for file in `find . -type f -name "*.csv"` do echo "file = $file" diff "$file" "/some/other/path/$file" read line done IFS="$OIFS" 

Лучший ответ (также обрабатывает подстановочные знаки и символы новой строки в именах файлов)

 find . -type f -name "*.csv" -print0 | while IFS= read -r -d '' file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done 

Лучший ответ (на основе ответа Жиля )

 find . -type f -name '*.csv' -exec sh -c ' file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty ' {} ';' 

Или даже лучше, чтобы не запускать один файл на каждый файл:

 find . -type f -name '*.csv' -exec sh -c ' for file do echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty done ' sh {} + 

Длительный ответ

У вас есть три проблемы:

  1. По умолчанию оболочка разбивает вывод команды на пробелы, вкладки и строки новой строки
  2. Имена файлов могут содержать подстановочные знаки, которые будут расширены
  3. Что делать, если есть каталог, имя которого заканчивается на *.csv ?

1. Разделение только на новые строки

Чтобы выяснить, для чего нужно установить file , оболочка должна как-то выводить вывод и интерпретировать его, иначе file будет всего лишь результатом вывода.

Оболочка считывает переменную IFS , которая по умолчанию установлена ​​на <space><tab><newline> .

Затем он смотрит на каждый символ на выходе find . Как только он увидит какой-либо символ в IFS , он считает, что отмечает конец имени файла, поэтому он устанавливает file на любые персонажи, которые он видел до сих пор, и запускает цикл. Затем он начинается там, где он остановился, чтобы получить следующее имя файла, и запускает следующий цикл и т. Д., Пока не достигнет конца вывода.

Таким образом, это эффективно:

 for file in "zquery" "-" "abc" ... 

Чтобы сказать, что только разбить вход на новые строки, вам нужно сделать

 IFS=$'\n' 

перед for ... find команду for ... find .

Это устанавливает IFS для одной новой строки, поэтому она разделяется только на строки новой строки, а не пробелы и вкладки.

Если вы используете sh или dash вместо ksh93 , bash или zsh , вам нужно вместо этого написать IFS=$'\n' :

 IFS=' ' 

Это, вероятно, достаточно, чтобы заставить ваш скрипт работать, но если вам интересно правильно обращаться с некоторыми другими угловыми случаями, читайте дальше …

2. Расширение $file без подстановочных знаков.

Внутри цикла, где вы делаете

 diff $file /some/other/path/$file 

оболочка пытается развернуть $file (снова!).

Он может содержать пробелы, но поскольку мы уже установили IFS выше, это не будет проблемой здесь.

Но он также может содержать подстановочные знаки, такие как * или ? , что приведет к непредсказуемому поведению. (Спасибо Жилю за это.)

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

 diff "$file" "/some/other/path/$file" 

Эта же проблема может также укусить нас в

 for file in `find . -name "*.csv"` 

Например, если у вас есть эти три файла

 file1.csv file2.csv *.csv 

(очень маловероятно, но все же возможно)

Было бы так, как будто вы бежали

 for file in file1.csv file2.csv *.csv 

который будет расширен до

 for file in file1.csv file2.csv *.csv file1.csv file2.csv 

заставляя file1.csv и file2.csv обрабатываться дважды.

Вместо этого мы должны сделать

 find . -name "*.csv" -print | while IFS= read -r file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done 

read считывает строки со стандартного ввода, разбивает строку на слова в соответствии с IFS и сохраняет их в указанных вами именах переменных.

Здесь мы говорим, что не разделить строку на слова и сохранить строку в $file .

Также обратите внимание, что read line изменилась на read line </dev/tty .

Это связано с тем, что внутри цикла стандартный ввод поступает от find по конвейеру.

Если бы мы только что read , это будет потреблять часть или все имя файла, а некоторые файлы будут пропущены.

/dev/tty – это терминал, на котором пользователь запускает скрипт. Обратите внимание, что это вызовет ошибку, если скрипт запущен через cron, но я предполагаю, что это не важно в этом случае.

Тогда, что, если имя файла содержит символы новой строки?

Мы можем справиться с этим, изменив -print на -print0 и используя read -d '' в конце конвейера:

 find . -name "*.csv" -print0 | while IFS= read -r -d '' file; do echo "file = $file" diff "$file" "/some/other/path/$file" read char </dev/tty done 

Это заставляет find помещать нулевой байт в конце каждого имени файла. Нулевые байты – это единственные символы, которые не разрешены в именах файлов, поэтому это должно обрабатывать все возможные имена файлов, какими бы странными они ни были.

Чтобы получить имя файла с другой стороны, мы используем IFS= read -r -d '' .

Где мы использовали read выше, мы использовали разделитель строк по умолчанию для новой строки, но теперь find использует значение null в качестве разделителя строк. В bash вы не можете передать символ NUL в аргументе команды (даже встроенные), но bash понимает -d '' как значение, указанное в NUL . Таким образом, мы используем -d '' чтобы read использовало тот же разделитель строк, что и find . Обратите внимание, что -d $'\0' , кстати, также работает, потому что bash не поддерживающий байты NUL, рассматривает его как пустую строку.

Чтобы быть верным, мы также добавляем -r , который говорит, что не обрабатывать обратную косую черту в именах файлов специально. Например, без -r , \<newline> удаляются, а \n преобразуется в n .

Более портативный способ написания этого, который не требует bash или zsh или запоминания всех вышеперечисленных правил об нулевых байтах (опять же, благодаря Gilles):

 find . -name '*.csv' -exec sh -c ' file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty ' {} ';' 

3. Пропуск каталогов, имена которых заканчиваются на * .csv

 find . -name "*.csv" 

также будут соответствовать каталогам, которые называются something.csv .

Чтобы избежать этого, добавьте -type f в команду find .

 find . -type f -name '*.csv' -exec sh -c ' file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty ' {} ';' 

Как указывает Гленн Джекман , в обоих этих примерах команды для выполнения для каждого файла выполняются в подоболочке, поэтому, если вы меняете какие-либо переменные внутри цикла, они будут забыты.

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

 i=0 while IFS= read -r -d '' file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty i=$((i+1)) done < <(find . -type f -name '*.csv' -print0) echo "$i files processed" 

Обратите внимание, что если вы попытаетесь скопировать и вставить это в командной строке, строка read line будет потреблять echo "$i files processed" , так что команда не будет запущена.

Чтобы этого избежать, вы можете удалить read line </dev/tty и отправить результат на пейджер как less .


ЗАМЕТКИ

Я удалил полукольцы ( ; ) внутри цикла. Вы можете вернуть их, если хотите, но они не нужны.

В наши дни $(command) более распространена, чем `command` . Это в основном потому, что проще написать $(command1 $(command2)) чем `command1 \`command2\`` .

read char самом деле не читает персонажа. Он читает целую строку, поэтому я изменил ее на read line .

Этот скрипт терпит неудачу, если какое-либо имя файла содержит пробелы или символы с чередованием оболочки \[?* . Команда find выводит одно имя файла в строке. Затем подстановка команды `find …` оценивается оболочкой следующим образом:

  1. Выполните команду find , захватите ее выход.
  2. Разделите вывод вывода на отдельные слова. Любой пробельный символ является разделителем слов.
  3. Для каждого слова, если это шаблон глобуса, разверните его в список файлов, которые он соответствует.

Например, предположим, что в текущем каталоге есть три файла, называемые `foo* bar.csv , foo 1.txt и foo 2.txt .

  1. Команда find возвращает ./foo* bar.csv .
  2. Оболочка разделяет эту строку в пространстве, создавая два слова: ./foo* и bar.csv .
  3. Поскольку ./foo* содержит метасимвол ./foo* , он расширяется до списка совпадающих файлов: ./foo 1.txt и ./foo 2.txt .
  4. Поэтому цикл for выполняется последовательно с ./foo 1.txt , ./foo 2.txt и bar.csv .

На этом этапе вы можете избежать большинства проблем, уменьшив разбиение слов и отключив подглаживание. Чтобы смягчить разбиение слов, установите для переменной IFS один символ новой строки; таким образом выход find будет только разделен на новые строки, и пробелы останутся. Чтобы отключить переключение, запустите set -f . Затем эта часть кода будет работать до тех пор, пока имя файла не будет содержать символ новой строки.

 IFS=' ' set -f for file in $(find . -name "*.csv"); do … 

(Это не часть вашей проблемы, но я рекомендую использовать $(…) над `…` . Они имеют то же значение, но версия backquote имеет странные правила цитирования.)

Ниже приведена еще одна проблема: diff $file /some/other/path/$file должен быть

 diff "$file" "/some/other/path/$file" 

В противном случае значение $file разделяется на слова, а слова рассматриваются как шаблоны glob, например, с помощью команды substitutio выше. Если вы должны помнить одну вещь о программировании оболочки, помните об этом: всегда используйте двойные кавычки вокруг переменных разложений ( $foo ) и подстановок команд ( $(bar) ) , если только вы не знаете, что хотите разбить. (Выше мы знали, что хотим разбить вывод на строки.)

Надежным способом вызова find является указание запустить команду для каждого найденного файла:

 find . -name '*.csv' -exec sh -c ' echo "$0" diff "$0" "/some/other/path/$0" ' {} ';' 

В этом случае другой подход заключается в сравнении двух каталогов, хотя вы должны явно исключить все «скучные» файлы.

 diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path 

У Афаика есть все, что вам нужно.

 find . -okdir diff {} /some/other/path/{} ";" 

find берет на себя заботу о том, чтобы вызывать программы в целом. -okdir подскажет вам перед diff (вы уверены, да / нет).

Никакой раковины не задействовано, никакое globbing, jokers, pi, pa, po.

В качестве побочного элемента: если вы совмещаете поиск с / while / do / xargs, в большинстве случаев вы делаете это неправильно. 🙂

Пронумеруйте любые файлы (включая любой специальный символ) с полностью безопасной находкой (см. Ссылку для документации):

 exec 9< <( find "$absolute_dir_path" -type f -print0 ) while IFS= read -r -d '' -u 9 do file_path="$(readlink -fn -- "$REPLY"; echo x)" file_path="${file_path%x}" echo "START${file_path}END" done 

Я удивлен, что никто не упоминал очевидное решение zsh :

 for file (**/*.csv(ND.)) { do-something-with $file } 

( (D) чтобы также включать скрытые файлы, (N) чтобы избежать ошибки, если нет совпадения (.) Чтобы ограничить обычные файлы.)

bash4.3 и выше теперь частично поддерживают его:

 shopt -s globstar nullglob dotglob for file in **/*.csv; do [ -f "$file" ] || continue [ -L "$file" ] && continue do-something-with "$file" done 

Имена файлов с пробелами в них выглядят как несколько имен в командной строке, если они не цитируются. Если ваш файл имеет имя «Hello World.txt», строка diff расширяется до:

 diff Hello World.txt /some/other/path/Hello World.txt 

который выглядит как четыре имени файла. Просто поставьте кавычки вокруг аргументов:

 diff "$file" "/some/other/path/$file" 

Двойное цитирование – ваш друг.

 diff "$file" "/some/other/path/$file" 

В противном случае содержимое переменной получает слово-расщепление.

С помощью bash4 вы также можете использовать встроенную функцию mapfile для установки массива, содержащего каждую строку, и итерации по этому массиву.

 $ tree . ├── a │ ├── a 1 │ └── a 2 ├── b │ ├── b 1 │ └── b 2 └── c ├── c 1 └── c 2 3 directories, 6 files $ mapfile -t files < <(find -type f) $ for file in "${files[@]}"; do > echo "file: $file" > done file: ./a/a 2 file: ./a/a 1 file: ./b/b 2 file: ./b/b 1 file: ./c/c 2 file: ./c/c 1 

Я удивлен, что не вижу readarray о readarray . Это очень удобно при использовании в сочетании с оператором <<< :

 $ touch oneword "two words" $ readarray -t files <<<"$(ls)" $ for file in "${files[@]}"; do echo "|$file|"; done |oneword| |two words| 

Используя конструкцию <<<"$expansion" вы также можете разделить переменные, содержащие новые строки, в массивы, например:

 $ string=$(dmesg) $ readarray -t lines <<<"$string" $ echo "${lines[0]}" [ 0.000000] Initializing cgroup subsys cpuset 

readarray был в Bash уже много лет, поэтому, вероятно, это должен быть канонический способ сделать это в Bash.

Пространства в значениях можно избежать, как простую для конструкции цикла

 for CHECK_STR in `ls -l /root/somedir` do echo "CHECKSTR $CHECK_STR" done 

ls -l root / somedir содержит мой файл с пробелами

Вывод над моим файлом с пробелами

чтобы избежать этого выхода, простое решение (обратите внимание на двойные кавычки)

 for CHECK_STR in "`ls -l /root/somedir`" do echo "CHECKSTR $CHECK_STR" done 

выводить мой файл с пробелами

пробовал бах