Проблема
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 основных типа снарядов:
Те, которые еще не многобайтовые, знают как 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.
yash: противоположный. Это касается только персонажей . Все вводимые данные внутренне переводятся в символы. Это обеспечивает наиболее согласованную оболочку, но также означает, что она не может справиться с произвольными байтовыми последовательностями (теми, которые не переводятся в допустимые символы). Даже в локали C он не может справиться со значениями байтов выше 0x7f.
find . -exec yash -c 'echo "$1"' sh {} \;
например, в локали UTF-8 произойдет сбой на нашем ISO-8859-1 côté.txtот более раннего.
Те, кому нравится bashили zshгде многобайтовая поддержка была добавлена постепенно. Они вернутся к рассмотрению байтов, которые не могут быть сопоставлены с символами, как если бы они были символами. У них все еще есть несколько ошибок, особенно с менее распространенными многобайтовыми кодировками, такими как GBK или BIG5-HKSCS (которые являются довольно неприятными, поскольку многие из их многобайтовых символов содержат байты в диапазоне 0-127 (например, символы ASCII) ).
Те, что во 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, чтобы файл не был выбран.