Проблема
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) ).
Те, что во sh
FreeBSD (по крайней мере 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, чтобы файл не был выбран.