Этот вопрос вдохновлен
Почему использование цикла оболочки для обработки текста считается плохой практикой?
Я вижу эти конструкции
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
?
Ответы:
Проблема
сочетает в себе две несовместимые вещи.
find
печатает список путей к файлам, разделенных символами новой строки. В то время как оператор split + glob, который вызывается, когда вы оставляете его без$(find .)
кавычек в контексте этого списка, разделяет его на символы$IFS
(по умолчанию включает символ новой строки, но также пробел и табуляцию (и NUL вzsh
)) и выполняет глобализацию для каждого полученного слова (кроме вzsh
) (и даже в скобках в ksh93 или в производные pdksh!).Даже если вы сделаете это:
Это по-прежнему неправильно, так как символ новой строки так же действителен, как и любой в пути к файлу. Вывод
find -print
просто не может быть надежно постобработан (за исключением использования некоторой запутанной уловки, как показано здесь ).Это также означает, что оболочке необходимо полностью сохранить выходные данные
find
, а затем разбить их на разделенные + (что подразумевает сохранение этих выходных данных во второй раз в памяти) перед началом циклического перебора файлов.Обратите внимание, что
find . | xargs cmd
есть похожие проблемы (проблемы с пробелами, новой строкой, одинарными кавычками, двойными кавычками и обратной косой чертой (и с некоторымиxarg
реализациями байтов, не являющихся частью допустимых символов) являются проблемой)Более правильные альтернативы
Единственный способ использовать
for
цикл на выходеfind
будет использовать,zsh
который поддерживаетIFS=$'\0'
и:(заменить
-print0
на-exec printf '%s\0' {} +
дляfind
реализаций, которые не поддерживают нестандартные (но довольно распространенные в настоящее время)-print0
).Здесь правильным и переносимым способом является использование
-exec
:Или, если
something
может принимать более одного аргумента:Если вам нужен этот список файлов для обработки оболочкой:
(остерегайтесь, это может начаться больше чем один
sh
).В некоторых системах вы можете использовать:
хотя это имеет небольшое преимущество перед стандартным синтаксисом и означает,
something
чтоstdin
это либо труба, либо/dev/null
.Одной из причин, по которой вы можете захотеть использовать это, может быть использование
-P
опции GNUxargs
для параллельной обработки. Этуstdin
проблему также можно обойти с помощью GNUxargs
с-a
опцией оболочек, поддерживающих замену процессов:например, для запуска до 4 одновременных вызовов,
something
каждый из которых принимает 20 аргументов файла.С помощью
zsh
илиbash
, другой способ зацикливания на выходеfind -print0
с помощью:read -d ''
читает записи с разделителями NUL вместо строк с разделителями.bash-4.4
и выше также может хранить файлы, возвращаемыеfind -print0
в массиве с:zsh
Эквивалент (который имеет преимущество сохраненияfind
«ы статус выхода):С помощью
zsh
вы можете перевести большинствоfind
выражений в комбинацию рекурсивного сглаживания с квалификаторами glob. Например, зацикливаниеfind . -name '*.txt' -type f -mtime -1
будет:Или же
(остерегайтесь необходимости,
--
например**/*
, пути к файлам не начинаются./
, поэтому могут начинаться,-
например, с).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
chdir()
s в родительский каталог файла перед запускомcmd
. Вместо вызоваcmd -- ./foo/bar
он вызываетcmd -- ./bar
(cmd -- bar
с некоторыми реализациями, отсюда и--
), поэтому проблема с./foo
заменой символической ссылки исключается. Это делает использование таких команд, какrm
более безопасным (это может привести к удалению другого файла, но не файла в другом каталоге), но не позволяет использовать команды, которые могут изменять файлы, если они не предназначены для использования по символическим ссылкам.-execdir cmd -- {} +
иногда также работает, но с несколькими реализациями, включая некоторые версии GNUfind
, это эквивалентно-execdir cmd -- {} \;
.-execdir
также имеет преимущество работы с некоторыми проблемами, связанными со слишком глубокими деревьями каталогов.В:
размер указанного пути
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
реализациями (включая GNUfind
).наш
63 f4 74 e9 2e 74 78 74
файл выше не будет найден при вызове в локали UTF-8, поскольку*
(который соответствует 0 или более символам , а не байтам) не может соответствовать этим не символам.LC_ALL=C find...
будет работать вокруг этой проблемы, так как локаль C подразумевает один байт на символ и (как правило) гарантирует, что все байтовые значения отображаются на символ (хотя, возможно, и неопределенные для некоторых байтовых значений).Теперь, когда дело доходит до зацикливания этих имен файлов из оболочки, этот байт против символа также может стать проблемой. В этом отношении мы обычно видим 4 основных типа снарядов:
Те, которые еще не многобайтовые, знают как
dash
. Для них байт отображается на символ. Например, в UTF-8côté
это 4 символа, но 6 байтов. В локали, где UTF-8 является кодировкой, вfind
успешно найдет файлы, имя которых состоит из 4 символов, закодированных в UTF-8, ноdash
сообщит о длине в диапазоне от 4 до 24.yash
: противоположный. Это касается только персонажей . Все вводимые данные внутренне переводятся в символы. Это обеспечивает наиболее согласованную оболочку, но также означает, что она не может справиться с произвольными байтовыми последовательностями (теми, которые не переводятся в допустимые символы). Даже в локали C он не может справиться со значениями байтов выше 0x7f.например, в локали UTF-8 произойдет сбой на нашем ISO-8859-1
côté.txt
от более раннего.Те, кому нравится
bash
илиzsh
где многобайтовая поддержка была добавлена постепенно. Они вернутся к рассмотрению байтов, которые не могут быть сопоставлены с символами, как если бы они были символами. У них все еще есть несколько ошибок, особенно с менее распространенными многобайтовыми кодировками, такими как GBK или BIG5-HKSCS (которые являются довольно неприятными, поскольку многие из их многобайтовых символов содержат байты в диапазоне 0-127 (например, символы ASCII) ).Те, что во
sh
FreeBSD (по крайней мере 11) илиmksh -o utf8-mode
которые поддерживают многобайтовые, но только для UTF-8.Примечания
1 Для полноты изложения можно упомянуть хакерский способ
zsh
циклического перебора файлов с использованием рекурсивного сглаживания без сохранения всего списка в памяти:+cmd
является Глоб классификатором , который вызываетcmd
(обычно функция) с текущим путем к файлу в$REPLY
. Функция возвращает true или false, чтобы решить, должен ли файл быть выбран (и также может изменить$REPLY
или вернуть несколько файлов в$reply
массиве). Здесь мы выполняем обработку в этой функции и возвращаем false, чтобы файл не был выбран.источник
find
себя вести себя безопасно. По умолчанию глобализация безопасна, а поиск по умолчанию небезопасен.Простой ответ:
Потому что имена файлов могут содержать любой символ.
Следовательно, нет печатного символа, который можно надежно использовать для разделения имен файлов.
Символы новой строки часто используются (неправильно) для разграничения имен файлов, потому что необычно включать символы новой строки в имена файлов.
Однако, если вы строите свое программное обеспечение на основе произвольных предположений, вы в лучшем случае просто не справляетесь с необычными случаями, а в худшем случае открываете себя для злонамеренных эксплойтов, которые передают контроль над вашей системой. Так что это вопрос надежности и безопасности.
Если вы можете писать программное обеспечение двумя различными способами, и один из них правильно обрабатывает крайние случаи (необычные входные данные), но другой легче читать, вы можете поспорить, что есть компромисс. (Я бы не стал. Я предпочитаю правильный код.)
Однако, если правильная и надежная версия кода также легко читается, не существует оправдания для написания кода, который не выполняется в крайних случаях. Это как раз тот случай,
find
когда нужно выполнить команду для каждого найденного файла.Давайте будем более конкретными: в системе UNIX или Linux имена файлов могут содержать любые символы, кроме символа
/
(который используется в качестве разделителя компонентов пути), и они могут не содержать нулевой байт.Следовательно, нулевой байт является единственным правильным способом разграничения имен файлов.
Так как GNU
find
включает-print0
первичный, который будет использовать нулевой байт для разграничения имен файлов, которые он печатает, GNUfind
может безопасно использоваться с GNUxargs
и его-0
флагом (и-r
флагом) для обработки выводаfind
:Тем не менее, нет веской причины использовать эту форму, потому что:
find
будет разработан , чтобы иметь возможность запускать команды на файлы , которые он находит.Кроме того, GNU
xargs
требует-0
и-r
, тогда как FreeBSDxargs
требует только-0
(и не имеет-r
опций), а некоторыеxargs
вообще не поддерживают-0
. Поэтому лучше просто придерживаться функций POSIXfind
(см. Следующий раздел) и пропуститьxargs
.Что касается пункта 2
find
- способности запускать команды для файлов, которые он находит, - я думаю, что Майк Лоукидес сказал это лучше всего:POSIX указанное использование
find
Чтобы запустить одну команду для каждого найденного файла, используйте:
Чтобы запустить несколько команд в последовательности для каждого найденного файла, где вторая команда должна выполняться только в случае успеха первой команды, используйте:
Чтобы запустить одну команду для нескольких файлов одновременно:
find
в комбинации сsh
Если вам нужно использовать в команде функции оболочки , такие как перенаправление вывода или удаление расширения из имени файла или чего-то подобного, вы можете использовать
sh -c
конструкцию. Вы должны знать несколько вещей об этом:Никогда не вставляйте
{}
прямо вsh
код. Это позволяет выполнять произвольный код из злонамеренно созданных имен файлов. Кроме того, в POSIX даже не указано, что он будет работать вообще. (См. Следующий пункт.)Не используйте
{}
несколько раз или используйте его как часть более длинного аргумента. Это не портативно. Например, не делайте этого:find ... -exec cp {} somedir/{}.bak \;
Чтобы процитировать спецификации POSIX для
find
:Аргументы, следующие за командной строкой оболочки, переданной
-c
параметру, устанавливаются в позиционные параметры оболочки, начиная с$0
. Не начиная с$1
.По этой причине целесообразно добавить «фиктивное»
$0
значение, напримерfind-sh
, которое будет использоваться для создания отчетов об ошибках из порожденной оболочки. Кроме того, это позволяет использовать конструкции, например,"$@"
при передаче нескольких файлов в оболочку, тогда как пропуск значения$0
означает, что первый переданный файл будет установлен$0
и, следовательно, не включен в"$@"
.Чтобы запустить одну команду оболочки для файла, используйте:
Тем не менее, это обычно дает лучшую производительность для обработки файлов в цикле оболочки, так что вы не создаете оболочку для каждого найденного файла:
(Обратите внимание, что
for f do
это эквивалентноfor f in "$@"; do
и обрабатывает каждый из позиционных параметров по очереди - другими словами, он использует каждый из найденных файловfind
, независимо от каких-либо специальных символов в их именах.)Дополнительные примеры правильного
find
использования:(Примечание: не стесняйтесь расширять этот список.)
источник
find
выводу парсинга - когда вам нужно запускать команды в текущей оболочке (например, потому что вы хотите установить переменные) для каждого файла. В данном случаеwhile IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)
это лучшая идиома, которую я знаю. Примечания:<( )
не переносимо - используйте bash или zsh. Кроме того,-u3
и3<
есть в случае, если что-то внутри цикла пытается прочитать стандартный ввод.find ... -exec
вызов. Или просто используйте оболочку оболочки, если она будет обрабатывать ваш вариант использования.filelist=(); while ... do filelist+=("$file"); done ...
).find
выводимые данные или, что еще хуже, использоватьls
. Я делаю это ежедневно без проблем. Я знаю о параметрах -print0, --null, -z или -0 всех видов инструментов. Но я бы не стал тратить время на их использование в моей интерактивной командной оболочке, если в этом нет особой необходимости. Это также может быть отмечено в вашем ответе.Этот ответ предназначен для очень больших наборов результатов и касается в основном производительности, например, при получении списка файлов по медленной сети. Для небольшого количества файлов (скажем, несколько 100 или, может быть, даже 1000 на локальном диске) большинство из них спорные.
Параллельность и использование памяти
Помимо других ответов, связанных с проблемами разделения и так далее, существует еще одна проблема с
Часть внутри обратных кавычек должна быть сначала полностью оценена, а затем разделена на разрывы строк. Это означает, что, если вы получаете огромное количество файлов, он может либо подавиться любыми ограничениями размера в различных компонентах; у вас может не хватить памяти, если нет ограничений; и в любом случае вам придется подождать, пока весь список не будет выведен,
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
процессов и начальной задержкой.РЕДАКТИРОВАТЬ: удалены понятия «лучший» - трудно сказать, появится ли что-то лучшее. ;)
источник
find ... -exec smth {} +
это решение.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
.Одна из причин заключается в том, что пробелы в работе дают ключ, в результате чего файл 'foo bar' оценивается как 'foo' и 'bar'.
Работает нормально, если вместо этого используется -exec
источник
find
когда есть возможность выполнить команду для каждого файла, это просто лучший вариант.-exec ... {} \;
против-exec ... {} +
for file in "$(find . -type f)"
аecho "${file}"
затем он работает даже с пробелами, другие специальные символы, я думаю, доставляют больше хлопотfor file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";done
который (по вашему мнению) должен напечатать каждое имя файла в отдельной строке, перед которой стоитname:
. Это не так.Поскольку выходные данные любой команды представляют собой одну строку, но ваш цикл нуждается в массиве строк для зацикливания. Причина, по которой он «работает», заключается в том, что снаряды предательски разбивают вам строку на пустом месте.
Во-вторых, если вам не нужна определенная особенность
find
, имейте в виду, что ваша оболочка, скорее всего, уже может самостоятельно развернуть рекурсивный шаблон глобуса, и, что важно, она будет расширена до нужного массива.Пример Bash:
То же самое в рыбе:
Если вам нужны функции
find
, убедитесь, что разделены только на NUL (например,find -print0 | xargs -r0
идиома).Рыба может повторять вывод с разделением NUL. Так что это один на самом деле не плохо:
Как последний маленький уловок, во многих оболочках (конечно, не в Fish) цикл вывода команды сделает тело цикла подоболочником (то есть вы не можете установить переменную любым способом, который будет виден после завершения цикла), что никогда, что вы хотите.
источник
zsh
начале 90-х годов (хотя вам это понадобится**/*
).fish
как и в более ранних реализациях эквивалентной функции bash, все же следует символические ссылки при спуске по дереву каталогов. См . Результат ls *, ls ** и ls *** для различий между реализациями.Циклический вывод результатов поиска не является плохой практикой - то, что является плохой практикой (в этой и во всех ситуациях), предполагает, что ваши входные данные представляют собой определенный формат, а не знают (тестируют и подтверждают), что это особый формат.
tldr / CBF:
find | parallel stuff
источник