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

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

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

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

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

153 голоса | спросил don_crissti 7 12016vEurope/Moscow11bEurope/MoscowMon, 07 Nov 2016 21:22:00 +0300 2016, 21:22:00

6 ответов


71

Проблема

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, а затем разделить + glob it (что означает сохранение этого вывода во второй раз в памяти), прежде чем начинать цикл над файлами.

Обратите внимание, что 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 - это либо pipe, либо /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 в комбинацию рекурсивного globbing с квалификаторами 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 в конечном итоге добавлена ​​поддержка **/ (хотя и не более продвинутых форм рекурсивного глобирования), но все же не glob которые делают использование ** очень ограниченным. Также помните, что bash до 4.3 следует символическим ссылкам при смене дерева каталогов.

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

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

Условия гонки

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

Даже при смене дерева каталогов необходимо следить за тем, чтобы не следовать символическим ссылкам и делать это без гонки TOCTOU. find (GNU find) делает это, открывая каталоги с помощью 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, но еще хуже, find, возможно, был заменен символической ссылкой на другое место (и окно гонки стало намного больше с помощью ./foo, где -exec {} + ожидает наличия достаточно файлов для вызова find).

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

С

-execdir

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

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

-execdir cmd -- {} \; также имеет смысл обойти некоторые проблемы, связанные со слишком глубокими деревьями каталогов.

В:

-execdir

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

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

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

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

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

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

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

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

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

find

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

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

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

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

    côté

    find . -name '????' -exec dash -c ' name=${1##*/}; echo "${#name}"' sh {} \; будет успешно найти файлы, имя которых состоит из 4 символов, закодированных в UTF-8, но find будет сообщать длины от 4 до 24.

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

    yash

    в локали UTF-8 будет сбой на нашем ISO-8859-1 find . -exec yash -c 'echo "$1"' sh {} \; из более раннего экземпляра.

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

  4. Те, что похожи на zsh FreeBSD (по крайней мере, 11) или sh, которые поддерживают несколько байтов, но только для UTF-8.

Примечания

1 Для полноты мы могли бы упомянуть хакерский путь в mksh -o utf8-mode, чтобы зацикливать файлы с использованием рекурсивного globbing, не сохраняя весь список в памяти:

zsh

process() { something with $REPLY false } : **/*(ND.m-1+process) является квалификатором glob, который вызывает +cmd (обычно это функция) с текущим файловым путем в cmd. Функция возвращает true или false, чтобы решить, следует ли выбрать файл (а также изменить $REPLY) или вернуть несколько файлов в массив $REPLY). Здесь мы обрабатываем эту функцию и возвращаем false, чтобы файл не был выбран.

ответил Stéphane Chazelas 8 22016vEurope/Moscow11bEurope/MoscowTue, 08 Nov 2016 02:12:47 +0300 2016, 02:12:47
178
  

Почему цикл с ошибкой find выводит неверную практику?

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

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

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


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

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

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

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


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

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


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

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

Однако для использования этой формы нет хорошей reason , потому что:

  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

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

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

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

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

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

      

    Если строка utility_name или содержит два символа "{}", но не только два символа "{}", это определяется реализацией, em> find заменяет эти двасимволов или использует строку без изменений.

         

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

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

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


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

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 "[email protected]"; do и обрабатывает каждый из позиционных параметров в свою очередь - другими словами, это использует каждый из найденных файлов find, независимо от каких-либо специальных символов в их именах.)


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

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

ответил Wildcard 8 22016vEurope/Moscow11bEurope/MoscowTue, 08 Nov 2016 01:53:59 +0300 2016, 01:53:59
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 и начальная задержка.

EDIT: удалены понятия «лучший» - трудно сказать, произойдет ли что-то лучше. ;)

ответил AnoE 8 22016vEurope/Moscow11bEurope/MoscowTue, 08 Nov 2016 03:23:43 +0300 2016, 03:23:43
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: -
$
ответил steve 7 12016vEurope/Moscow11bEurope/MoscowMon, 07 Nov 2016 21:28:50 +0300 2016, 21:28:50
2

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

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

Пример Bash:

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

То же самое в Fish:

for i in **
    echo «$i»
end

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

Рыба может выполнять итерацию вывода, ограниченного NUL. Таким образом, это действительно not bad:

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

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

ответил user2394284 12 62016vEurope/Moscow11bEurope/MoscowSat, 12 Nov 2016 17:17:50 +0300 2016, 17:17:50
1

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

tldr /cbf: find | parallel stuff

ответил Jan Kyu Peblik 11 52016vEurope/Moscow11bEurope/MoscowFri, 11 Nov 2016 12:45:42 +0300 2016, 12:45:42

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132