Почему зацикливание на выводе find плохо работает?

170

Этот вопрос вдохновлен

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

Я вижу эти конструкции

for file in `find . -type f -name ...`; do smth with ${file}; done

а также

for dir in $(find . -type d -name ...); do smth with ${dir}; done

используется здесь почти ежедневно, даже если некоторые люди не торопятся комментировать эти посты, объясняя, почему такого рода вещи следует избегать ...
Просмотр количества таких постов (и тот факт, что иногда эти комментарии просто игнорируются) Я подумал, что с тем же успехом могу задать вопрос:

Почему циклический findвывод неэффективен и как правильно выполнить одну или несколько команд для каждого имени файла / пути, возвращаемого find?

don_crissti
источник
12
Я думаю, это что-то вроде "Никогда не анализируйте вывод ls!" - Вы, конечно, можете сделать что-то одно на индивидуальной основе, но это скорее быстрый взлом, чем качество продукции. Или, вообще говоря, определенно никогда не будьте догматичными.
Брюс Эдигер
Это должно быть превращено в канонический ответ
Зайд
6
Потому что суть поиска заключается в том, чтобы перебрать то, что он находит.
OrangeDog
2
Один вспомогательный момент - вы можете захотеть отправить вывод в файл, а затем обработать его позже в скрипте. Таким образом, список файлов доступен для просмотра, если вам нужно отладить скрипт.
user117529 11.11.16

Ответы:

87

Проблема

for f in $(find .)

сочетает в себе две несовместимые вещи.

findпечатает список путей к файлам, разделенных символами новой строки. В то время как оператор split + glob, который вызывается, когда вы оставляете его без $(find .)кавычек в контексте этого списка, разделяет его на символы $IFS(по умолчанию включает символ новой строки, но также пробел и табуляцию (и NUL в zsh)) и выполняет глобализацию для каждого полученного слова (кроме в zsh) (и даже в скобках в ksh93 или в производные pdksh!).

Даже если вы сделаете это:

IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
              # but not ksh93)
for f in $(find .) # invoke split+glob

Это по-прежнему неправильно, так как символ новой строки так же действителен, как и любой в пути к файлу. Вывод find -printпросто не может быть надежно постобработан (за исключением использования некоторой запутанной уловки, как показано здесь ).

Это также означает, что оболочке необходимо полностью сохранить выходные данные find, а затем разбить их на разделенные + (что подразумевает сохранение этих выходных данных во второй раз в памяти) перед началом циклического перебора файлов.

Обратите внимание, что find . | xargs cmdесть похожие проблемы (проблемы с пробелами, новой строкой, одинарными кавычками, двойными кавычками и обратной косой чертой (и с некоторыми xargреализациями байтов, не являющихся частью допустимых символов) являются проблемой)

Более правильные альтернативы

Единственный способ использовать forцикл на выходе findбудет использовать, zshкоторый поддерживает IFS=$'\0'и:

IFS=$'\0'
for f in $(find . -print0)

(заменить -print0на -exec printf '%s\0' {} +для findреализаций, которые не поддерживают нестандартные (но довольно распространенные в настоящее время) -print0).

Здесь правильным и переносимым способом является использование -exec:

find . -exec something with {} \;

Или, если somethingможет принимать более одного аргумента:

find . -exec something with {} +

Если вам нужен этот список файлов для обработки оболочкой:

find . -exec sh -c '
  for file do
    something < "$file"
  done' find-sh {} +

(остерегайтесь, это может начаться больше чем один sh).

В некоторых системах вы можете использовать:

find . -print0 | xargs -r0 something with

хотя это имеет небольшое преимущество перед стандартным синтаксисом и означает, somethingчто stdinэто либо труба, либо /dev/null.

Одной из причин, по которой вы можете захотеть использовать это, может быть использование -Pопции GNU xargsдля параллельной обработки. Эту stdinпроблему также можно обойти с помощью GNU xargsс -aопцией оболочек, поддерживающих замену процессов:

xargs -r0n 20 -P 4 -a <(find . -print0) something

например, для запуска до 4 одновременных вызовов, somethingкаждый из которых принимает 20 аргументов файла.

С помощью zshили bash, другой способ зацикливания на выходе find -print0с помощью:

while IFS= read -rd '' file <&3; do
  something "$file" 3<&-
done 3< <(find . -print0)

read -d '' читает записи с разделителями NUL вместо строк с разделителями.

bash-4.4и выше также может хранить файлы, возвращаемые find -print0в массиве с:

readarray -td '' files < <(find . -print0)

zshЭквивалент (который имеет преимущество сохранения find«ы статус выхода):

files=(${(0)"$(find . -print0)"})

С помощью zshвы можете перевести большинство findвыражений в комбинацию рекурсивного сглаживания с квалификаторами glob. Например, зацикливание find . -name '*.txt' -type f -mtime -1будет:

for file (./**/*.txt(ND.m-1)) cmd $file

Или же

for file (**/*.txt(ND.m-1)) cmd -- $file

(остерегайтесь необходимости, --например **/*, пути к файлам не начинаются ./, поэтому могут начинаться, -например, с).

ksh93и в bashконечном итоге добавили поддержку **/(хотя не более продвинутые формы рекурсивного сглаживания), но все же не классификаторы сгущения, что делает использование там **очень ограниченным. Также помните, что bashдо 4.3 следует символические ссылки при спуске дерева каталогов.

Как и для зацикливания $(find .), это также означает сохранение всего списка файлов в памяти 1 . Это может быть желательно, хотя в некоторых случаях, когда вы не хотите, чтобы ваши действия над файлами влияли на поиск файлов (например, когда вы добавляете больше файлов, которые могут в конечном итоге оказаться самими собой).

Другие соображения надежности / безопасности

Расовые условия

Теперь, если мы говорим о надежности, мы должны упомянуть условия гонки между временем find/ zshнайденным файлом и проверкой его соответствия критериям и временем его использования ( гонка TOCTOU ).

Даже спускаясь по дереву каталогов, нужно следить за тем, чтобы не следовать символическим ссылкам, и делать это без гонки TOCTOU. find(По findкрайней мере, GNU ) делает это, открывая каталоги, используя openat()правильные O_NOFOLLOWфлаги (если они поддерживаются) и сохраняя файловый дескриптор открытым для каждого каталога, zsh/ bash/ kshне делайте этого. Таким образом, перед лицом злоумышленника, который может заменить каталог символической ссылкой в ​​нужное время, вы можете в конечном итоге спуститься не в тот каталог.

Даже если findдействительно спускаемся каталог должным образом, с -exec cmd {} \;и тем более с -exec cmd {} +после того , как cmdбудет выполнен, например , как cmd ./foo/barи cmd ./foo/bar ./foo/bar/baz, к тому времени cmdиспользует ./foo/bar, атрибуты barмогут больше не удовлетворяют критериям подбираются find, но еще хуже, ./fooможет быть заменяется символической ссылкой на какое-то другое место (и окно гонки становится намного больше, -exec {} +где findожидает, когда будет достаточно файлов для вызова cmd).

У некоторых findреализаций есть (нестандартный) -execdirпредикат, чтобы облегчить вторую проблему.

С участием:

find . -execdir cmd -- {} \;

find chdir()s в родительский каталог файла перед запуском cmd. Вместо вызова cmd -- ./foo/barон вызывает cmd -- ./bar( cmd -- barс некоторыми реализациями, отсюда и --), поэтому проблема с ./fooзаменой символической ссылки исключается. Это делает использование таких команд, как rmболее безопасным (это может привести к удалению другого файла, но не файла в другом каталоге), но не позволяет использовать команды, которые могут изменять файлы, если они не предназначены для использования по символическим ссылкам.

-execdir cmd -- {} +иногда также работает, но с несколькими реализациями, включая некоторые версии GNU find, это эквивалентно -execdir cmd -- {} \;.

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

В:

find . -exec cmd {} \;

размер указанного пути cmdбудет увеличиваться с глубиной директории, в которой находится файл. Если этот размер становится больше, чем PATH_MAX(что-то вроде 4k в Linux), то любой системный вызов, cmdвыполняющий этот путь, завершится с ENAMETOOLONGошибкой.

С -execdir, только имя файла (возможно с префиксом ./) передается cmd. Сами имена файлов в большинстве файловых систем имеют гораздо более низкий предел ( NAME_MAX), чем PATH_MAX, поэтому ENAMETOOLONGвероятность возникновения ошибки меньше.

Байт против символов

Кроме того, часто упускается из виду при рассмотрении вопросов безопасности findи, в более общем смысле, при обработке имен файлов в целом, является тот факт, что в большинстве Unix-подобных систем имена файлов представляют собой последовательности байтов (любое значение байта, кроме 0 в пути к файлу, и в большинстве систем ( Основанные на ASCII, мы пока проигнорируем редкие основанные на EBCDIC) 0x2f - разделитель пути).

Приложения сами решают, хотят ли они считать эти байты текстовыми. Как правило, это так, но обычно перевод из байтов в символы выполняется в зависимости от локали пользователя и среды.

Это означает, что данное имя файла может иметь различное текстовое представление в зависимости от локали. Например, последовательность байтов 63 f4 74 e9 2e 74 78 74была бы côté.txtдля приложения, интерпретирующего это имя файла в локали, где набор символов - ISO-8859-1, и cєtщ.txtв локали, где вместо кодировки IS0-8859-5.

Хуже. В локали, где кодировка UTF-8 (в настоящее время норма), 63 f4 74 e9 2e 74 78 74 просто не могут быть сопоставлены с символами!

findявляется одним из таких приложений, которое рассматривает имена файлов как текст для своих предикатов -name/ -path(и более, например, -inameили -regexс некоторыми реализациями).

Это означает, что, например, с несколькими findреализациями (включая GNU find).

find . -name '*.txt'

наш 63 f4 74 e9 2e 74 78 74файл выше не будет найден при вызове в локали UTF-8, поскольку *(который соответствует 0 или более символам , а не байтам) не может соответствовать этим не символам.

LC_ALL=C find... будет работать вокруг этой проблемы, так как локаль C подразумевает один байт на символ и (как правило) гарантирует, что все байтовые значения отображаются на символ (хотя, возможно, и неопределенные для некоторых байтовых значений).

Теперь, когда дело доходит до зацикливания этих имен файлов из оболочки, этот байт против символа также может стать проблемой. В этом отношении мы обычно видим 4 основных типа снарядов:

  1. Те, которые еще не многобайтовые, знают как dash. Для них байт отображается на символ. Например, в UTF-8 côtéэто 4 символа, но 6 байтов. В локали, где UTF-8 является кодировкой, в

    find . -name '????' -exec dash -c '
      name=${1##*/}; echo "${#name}"' sh {} \;
    

    findуспешно найдет файлы, имя которых состоит из 4 символов, закодированных в UTF-8, но dashсообщит о длине в диапазоне от 4 до 24.

  2. yash: противоположный. Это касается только персонажей . Все вводимые данные внутренне переводятся в символы. Это обеспечивает наиболее согласованную оболочку, но также означает, что она не может справиться с произвольными байтовыми последовательностями (теми, которые не переводятся в допустимые символы). Даже в локали C он не может справиться со значениями байтов выше 0x7f.

    find . -exec yash -c 'echo "$1"' sh {} \;
    

    например, в локали UTF-8 произойдет сбой на нашем ISO-8859-1 côté.txtот более раннего.

  3. Те, кому нравится bashили zshгде многобайтовая поддержка была добавлена ​​постепенно. Они вернутся к рассмотрению байтов, которые не могут быть сопоставлены с символами, как если бы они были символами. У них все еще есть несколько ошибок, особенно с менее распространенными многобайтовыми кодировками, такими как GBK или BIG5-HKSCS (которые являются довольно неприятными, поскольку многие из их многобайтовых символов содержат байты в диапазоне 0-127 (например, символы ASCII) ).

  4. Те, что во shFreeBSD (по крайней мере 11) или mksh -o utf8-modeкоторые поддерживают многобайтовые, но только для UTF-8.

Примечания

1 Для полноты изложения можно упомянуть хакерский способ zshциклического перебора файлов с использованием рекурсивного сглаживания без сохранения всего списка в памяти:

process() {
  something with $REPLY
  false
}
: **/*(ND.m-1+process)

+cmdявляется Глоб классификатором , который вызывает cmd(обычно функция) с текущим путем к файлу в $REPLY. Функция возвращает true или false, чтобы решить, должен ли файл быть выбран (и также может изменить $REPLYили вернуть несколько файлов в $replyмассиве). Здесь мы выполняем обработку в этой функции и возвращаем false, чтобы файл не был выбран.

Стефан Шазелас
источник
Если zsh и bash доступны, вам, возможно, будет лучше просто использовать globbing и shell-конструкции вместо того, чтобы пытаться заставить findсебя вести себя безопасно. По умолчанию глобализация безопасна, а поиск по умолчанию небезопасен.
Кевин
@Kevin, см. Редактировать.
Стефан Шазелас
182

Почему зацикливание на findвыходе плохая практика?

Простой ответ:

Потому что имена файлов могут содержать любой символ.

Следовательно, нет печатного символа, который можно надежно использовать для разделения имен файлов.


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

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

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

Однако, если правильная и надежная версия кода также легко читается, не существует оправдания для написания кода, который не выполняется в крайних случаях. Это как раз тот случай, findкогда нужно выполнить команду для каждого найденного файла.


Давайте будем более конкретными: в системе UNIX или Linux имена файлов могут содержать любые символы, кроме символа /(который используется в качестве разделителя компонентов пути), и они могут не содержать нулевой байт.

Следовательно, нулевой байт является единственным правильным способом разграничения имен файлов.


Так как GNU findвключает -print0первичный, который будет использовать нулевой байт для разграничения имен файлов, которые он печатает, GNU find может безопасно использоваться с GNU xargsи его -0флагом (и -rфлагом) для обработки вывода find:

find ... -print0 | xargs -r0 ...

Тем не менее, нет веской причины использовать эту форму, потому что:

  1. Он добавляет зависимость от GNU Findutils, который не должен быть там, и
  2. findбудет разработан , чтобы иметь возможность запускать команды на файлы , которые он находит.

Кроме того, GNU xargsтребует -0и -r, тогда как FreeBSD xargsтребует только -0(и не имеет -rопций), а некоторые xargsвообще не поддерживают -0. Поэтому лучше просто придерживаться функций POSIX find(см. Следующий раздел) и пропустить xargs.

Что касается пункта 2 find- способности запускать команды для файлов, которые он находит, - я думаю, что Майк Лоукидес сказал это лучше всего:

findБизнес оценивает выражения, а не находит файлы. Да, findконечно, находит файлы; но это действительно просто побочный эффект.

--Unix Электроинструменты


POSIX указанное использование find

Как правильно запустить одну или несколько команд для каждого из findрезультатов?

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

find dirname ... -exec somecommand {} \;

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

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

Чтобы запустить одну команду для нескольких файлов одновременно:

find dirname ... -exec somecommand {} +

find в комбинации с sh

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

  • Никогда не вставляйте {}прямо в shкод. Это позволяет выполнять произвольный код из злонамеренно созданных имен файлов. Кроме того, в POSIX даже не указано, что он будет работать вообще. (См. Следующий пункт.)

  • Не используйте {}несколько раз или используйте его как часть более длинного аргумента. Это не портативно. Например, не делайте этого:

    find ... -exec cp {} somedir/{}.bak \;

    Чтобы процитировать спецификации POSIX дляfind :

    Если имя_программы или строка аргумента содержит два символа «{}», а не только два символа «{}», то определяется реализацией, заменяет ли find эти два символа или использует строку без изменений.

    ... Если присутствует более одного аргумента, содержащего два символа "{}", поведение не определено.

  • Аргументы, следующие за командной строкой оболочки, переданной -cпараметру, устанавливаются в позиционные параметры оболочки, начиная с$0 . Не начиная с $1.

    По этой причине целесообразно добавить «фиктивное» $0значение, например find-sh, которое будет использоваться для создания отчетов об ошибках из порожденной оболочки. Кроме того, это позволяет использовать конструкции, например, "$@"при передаче нескольких файлов в оболочку, тогда как пропуск значения $0означает, что первый переданный файл будет установлен $0и, следовательно, не включен в "$@".


Чтобы запустить одну команду оболочки для файла, используйте:

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

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

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(Обратите внимание, что for f doэто эквивалентно for f in "$@"; doи обрабатывает каждый из позиционных параметров по очереди - другими словами, он использует каждый из найденных файлов find, независимо от каких-либо специальных символов в их именах.)


Дополнительные примеры правильного findиспользования:

(Примечание: не стесняйтесь расширять этот список.)

Wildcard
источник
5
В одном случае я не знаю альтернативы findвыводу парсинга - когда вам нужно запускать команды в текущей оболочке (например, потому что вы хотите установить переменные) для каждого файла. В данном случае while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)это лучшая идиома, которую я знаю. Примечания: <( )не переносимо - используйте bash или zsh. Кроме того, -u3и 3<есть в случае, если что-то внутри цикла пытается прочитать стандартный ввод.
Гордон Дэвиссон
1
@GordonDavisson, возможно, но то , что вам нужно установить эти переменные для ? Я считаю , что все , что он должен быть обработан внутри на find ... -execвызов. Или просто используйте оболочку оболочки, если она будет обрабатывать ваш вариант использования.
Wildcard
1
Я часто хочу распечатать резюме после обработки файлов («2 преобразовано, 3 пропущено, в следующих файлах были ошибки: ...»), и эти количества / списки должны быть собраны в переменных оболочки. Кроме того, существуют ситуации, когда я хочу создать массив имен файлов, чтобы я мог делать более сложные вещи, чем повторять по порядку (в этом случае это так filelist=(); while ... do filelist+=("$file"); done ...).
Гордон Дэвиссон
3
Ваш ответ правильный. Однако я не люблю догму. Несмотря на то, что я знаю лучше, есть много (особенно интерактивных) сценариев использования, в которых безопасно и просто печатать циклически findвыводимые данные или, что еще хуже, использовать ls. Я делаю это ежедневно без проблем. Я знаю о параметрах -print0, --null, -z или -0 всех видов инструментов. Но я бы не стал тратить время на их использование в моей интерактивной командной оболочке, если в этом нет особой необходимости. Это также может быть отмечено в вашем ответе.
rudimeier
16
@rudimeier, спор о догме и лучших практиках уже был сделан до смерти . Не интересует. Если вы используете его в интерактивном режиме, и он работает, хорошо, хорошо для вас - но я не собираюсь продвигать это. Процент авторов сценариев, которые пытаются узнать, что такое надежный код, а затем делают это только при написании производственных сценариев, вместо того, чтобы делать то, что они привыкли делать в интерактивном режиме, крайне минимален. Обработка должна продвигать лучшие методы все время. Люди должны понять, что есть правильный способ делать вещи.
Wildcard
10

Этот ответ предназначен для очень больших наборов результатов и касается в основном производительности, например, при получении списка файлов по медленной сети. Для небольшого количества файлов (скажем, несколько 100 или, может быть, даже 1000 на локальном диске) большинство из них спорные.

Параллельность и использование памяти

Помимо других ответов, связанных с проблемами разделения и так далее, существует еще одна проблема с

for file in `find . -type f -name ...`; do smth with ${file}; done

Часть внутри обратных кавычек должна быть сначала полностью оценена, а затем разделена на разрывы строк. Это означает, что, если вы получаете огромное количество файлов, он может либо подавиться любыми ограничениями размера в различных компонентах; у вас может не хватить памяти, если нет ограничений; и в любом случае вам придется подождать, пока весь список не будет выведен, findа затем проанализирован, forпрежде чем запускать ваш первый smth.

Предпочтительным способом Unix является работа с конвейерами, которые по своей сути работают параллельно, и которые также не нуждаются в сколь угодно больших буферах в целом. Это означает: вы бы предпочли, findчтобы программа работала параллельно с вашей smth, и сохраняла текущее имя файла в ОЗУ, пока оно передает это smth.

Одно, по крайней мере, частично OKish решение для этого является вышеупомянутым find -exec smth. Это избавляет от необходимости хранить все имена файлов в памяти и прекрасно работает параллельно. К сожалению, он также запускает один smthпроцесс на файл. Если smthможно работать только с одним файлом, то так оно и должно быть.

Если это вообще возможно, оптимальное решение было бы find -print0 | smth, с smthвозможностью обрабатывать имена файлов на его STDIN. Тогда у вас есть только один smthпроцесс, независимо от количества файлов, и вам нужно буферизовать только небольшое количество байтов (независимо от того, происходит ли внутренняя конвейерная буферизация) между этими двумя процессами. Конечно, это довольно нереально, если smthэто стандартная команда Unix / POSIX, но может быть подходом, если вы пишете ее самостоятельно.

Если это невозможно, то find -print0 | xargs -0 smthэто, вероятно, одно из лучших решений. Как упомянуто в комментариях @ dave_thompson_085, xargsон разделяет аргументы по нескольким прогонам smthпри достижении системных ограничений (по умолчанию в диапазоне от 128 КБ или любого другого ограничения, налагаемого execсистемой) и имеет параметры, влияющие на то, сколько файлы передаются одному вызову smth, что позволяет найти баланс между числом smthпроцессов и начальной задержкой.

РЕДАКТИРОВАТЬ: удалены понятия «лучший» - трудно сказать, появится ли что-то лучшее. ;)

Anoe
источник
find ... -exec smth {} +это решение.
Wildcard
find -print0 | xargs smthвообще не работает, но find -print0 | xargs -0 smth(примечание -0) или find | xargs smthесли в именах файлов нет кавычек, или обратная косая черта запускается smthс таким количеством имен файлов, которое доступно и помещается в один список аргументов ; если вы превысите maxargs, он будет запускаться smthстолько раз, сколько необходимо для обработки всех заданных аргументов (без ограничений). Вы можете установить меньшие куски (таким образом, несколько более ранний параллелизм) с помощью -L/--max-lines -n/--max-args -s/--max-chars.
dave_thompson_085
4

Одна из причин заключается в том, что пробелы в работе дают ключ, в результате чего файл 'foo bar' оценивается как 'foo' и 'bar'.

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

Работает нормально, если вместо этого используется -exec

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$
Стив
источник
Особенно в случае, findкогда есть возможность выполнить команду для каждого файла, это просто лучший вариант.
Сентиман
1
Также рассмотреть -exec ... {} \;против-exec ... {} +
три
1
если вы используете, for file in "$(find . -type f)" а echo "${file}"затем он работает даже с пробелами, другие специальные символы, я думаю, доставляют больше хлопот
mazs
9
@mazs - нет, цитирование не делает то, что вы думаете. В каталоге с несколькими файлами попробуйте, for file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";doneкоторый (по вашему мнению) должен напечатать каждое имя файла в отдельной строке, перед которой стоит name:. Это не так.
don_crissti
2

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

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

Пример Bash:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

То же самое в рыбе:

for i in **
    echo «$i»
end

Если вам нужны функции find, убедитесь, что разделены только на NUL (например, find -print0 | xargs -r0идиома).

Рыба может повторять вывод с разделением NUL. Так что это один на самом деле не плохо:

find -print0 | while read -z i
    echo «$i»
end

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

user2394284
источник
@don_crissti Точно. Это не обычно работает. Я пытался быть саркастичным, говоря, что это «работает» (с кавычками).
user2394284
Обратите внимание, что рекурсивное сглаживание возникло в zshначале 90-х годов (хотя вам это понадобится **/*). fishкак и в более ранних реализациях эквивалентной функции bash, все же следует символические ссылки при спуске по дереву каталогов. См . Результат ls *, ls ** и ls *** для различий между реализациями.
Стефан Шазелас
1

Циклический вывод результатов поиска не является плохой практикой - то, что является плохой практикой (в этой и во всех ситуациях), предполагает, что ваши входные данные представляют собой определенный формат, а не знают (тестируют и подтверждают), что это особый формат.

tldr / CBF: find | parallel stuff

Ян Кью Пеблик
источник