Вопрос или проблема
find . -exec cmd {} +
find . -print0 | xargs -0 cmd
Оба варианта являются надежными способами выполнения команды над файлами, найденными с помощью find.
Что предпочтительнее? Что более портативно, надежно, эффективно, универсально и почему?
Кратко
Нет четкого победителя. Моя рекомендация — использовать:
find . -exec cmd {} +
Где бы это ни было достаточным, так как это более портативно, требует меньше ресурсов и имеет меньше проблем, и один из:
xargs -r0 -other-options -a <(find ... -print0 | ...) cmd
find . -print0 | ... | xargs -r0 -other-options cmd
Когда вам нужны дополнительные функции xargs
или необходимо постобработать вывод find
другими инструментами, и вы знаете, что находитесь на системе, которая поддерживает эти нестандартные опции и ограничения не применяются и/или могут быть проигнорированы.
История
find
имел -exec cmd {} ';'
, вариант, который выполняет одно вызов cmd
для каждого файла и также выступает в качестве предиката условия, с момента его реализации с текущим интерфейсом в Unix V5 в середине 70-х, но форма -exec cmd {} +
, которая передает несколько файлов в cmd
, как можно больше, появилась гораздо позже. Она была написана Дэвидом Корном и впервые выпущена в System V Release 4 в 1988 году (см. lynx news://news.gmane.io/gmane.comp.standards.posix.austin.general/2192
), хотя не документировалась до SVR4.2 (1992).
Она была добавлена в издание стандарта POSIX 2001, и некоторые реализации find
добавили это гораздо позже (4.2.12 в 2005 году для GNU find
, в 2002 году для FreeBSD, в 2006 году для NetBSD, в 2015 году для busybox)
xargs
сам по себе появился в PWB Unix в конце 70-х. У него был (и по-прежнему есть) очень плохой интерфейс с странными и ненужными функциями и ограничениями, понимая уникальную форму кавычек (хотя, справедливости ради, близким к тому, что понимает оболочка PWB Unix, которая не сохранилась). Хотя он изначально должен был работать с выводом find
, он не мог делать это надежно.
Опция -0
была добавлена в GNU xargs
вместе с новой опцией -print0
для GNU find
в 1990 году. Достаточно безопасно предположить, что авторы GNU find
не знали о SysV's -exec {} +
, когда добавляли это. Некоторые опции -0
/--null
/-z
/--zero
постепенно были добавлены к другим утилитам GNU после этого, чтобы обрабатывать этот формат обмена с разделителем NUL, который может содержать произвольные пути к файлам и более общие произвольные строки C или аргументы командной строки.
Опция -d
для xargs
, чтобы разрешить любую однобайтовую запись-разделитель, сделав -0
избыточной, так как тогда она будет просто той же, что -d '\0'
, была добавлена гораздо позже в GNU xargs (в 4.2.26, выпущенной в конце 2005 года), но по сей день и, насколько мне известно, поддерживается только в GNU xargs
.
Без этих -0
или -d
(и -r
, см. ниже) xargs
почти не пригоден для использования (надежно).
-print0
/-0
были добавлены с тех пор в несколько других реализаций, даже на некоторых коммерческих Unix-системах на основе SysV, таких как Solaris 11. Это также поддерживается встроенной командой find
в оболочке bosh
.
Они не являются стандартными, но могут стать таковыми (вместе с опцией -d ''
для утилиты read
) в следующей версии стандарта POSIX (редактирование 2024: теперь указано в издании стандарта POSIX 2024 (find
, xargs
, read
)).
-exec cmd {} + по сравнению с xargs -0 cmd
-exec cmd {} +
является стандартным и теперь довольно портативным. Его поддержка все еще является опциональной в busybox, поэтому вы можете столкнуться с системами на базе Linux, где он недоступен. Вариант -ok cmd {} +
, который предлагает пользователю подтвердить выполнение cmd
, не является стандартным и не портативным (и это не было бы удобно, поскольку команды могут оказаться объемными).
-print0
/xargs -0
не является стандартом, но сейчас часто встречается на BSD и в реализациях find
/xargs
, которые обычно встречаются на системах на базе Linux, включая GNU, busybox и toybox. Он все еще не поддерживается на AIX и HP/UX.
За пределами систем GNU также по-прежнему редко встречаются другие реализации стандартных утилит (sort
, sed
, cut
, awk
и др.), которые поддерживают записи с разделителем NUL.
find
с -print0
может быть реализован стандартным образом с помощью -exec printf '%s\0' {} +
, но стандартного эквивалента для xargs -0
или sort
/sed
/grep
… -z нет, и более того, NUL не может обрабатываться POSIX текстовыми утилитами (как и пути к файлам, так как они не гарантированно являются текстом).
За исключением некоторых BSD, find . -print0 | xargs -0 cmd
все равно выполнит cmd
один раз без аргументов, если файл не будет найден, что обычно нежелательно. Реализация GNU xargs
добавила опцию -r
, чтобы избежать этого, но она не так портативна, как -0
.
В find . -exec cmd {} +
cmd
наследует стандартный ввод find
, поэтому cmd
все еще может взаимодействовать с пользователем, если эта команда запускается, например, из терминала.
В то время как в find . -print0 | xargs -r0 cmd
, в зависимости от реализации xargs
, стандартный ввод cmd
будет либо /dev/null (как в случае с GNU xargs
), либо хуже, будет наследовать стандартный ввод xargs
, который здесь является трубопроводом от find
, так что если он когда-либо будет читать из своего стандартного ввода, это приведет к хаосу. С реализацией GNU xargs
это можно обойти, используя -a
и подстановку процессов:
xargs -r0a <(find . -print0) cmd # Синтаксис Korn
xargs -r0a <{find . -print0} cmd # Синтаксис rc
xargs -r0a /dev/fd/3 3<(find . -print0) cmd # Синтаксис yash
xargs -r0a (find . -print0|psub) cmd # Синтаксис fish (хотя и не параллельный)
Но это гораздо менее портативно.
В отношении надежности, в find . -print0 | xargs -0 cmd
, если find
аварийно завершится или будет убит досрочно (например, потому что он достиг предела ресурсов), это может иметь драматические последствия. Это происходит потому, что find
записывает свой вывод блоками, которые не гарантируется, что завершатся на разделителе NUL (например, с find /var/tmp -name '*.tmp'
, блок может завершиться на /var
), и xargs
все равно создаст аргумент для cmd
для неделимого записа. Так, например, здесь мог бы вызвать cmd
(как rm -rf
) с /var
в качестве аргумента в нашем примере, если find
был убит после того, как он вывел блок, заканчивающийся на /var
..
Эта проблема не затрагивает -exec cmd {} +
.
С find . -exec cmd {} +
статус выхода отражает как ошибки find
, так и cmd
, в то время как с find | xargs
, в большинстве оболочек вы получите только статус выхода xargs
, так что можете пропустить тот факт, что не все файлы могли быть найдены. Многие оболочки имеют команду pipefail
, чтобы смягчить это, а также есть $pipestatus
в zsh или $PIPESTATUS
в bash, что в конечном итоге дает большую гибкость.
(не стандартный) вариант -execdir cmd -- {} +
(обратите внимание на необходимый --
в реализациях find
, которые не добавляют префикс ./
к именам файлов) обходит некоторые проблемы безопасности с -exec cmd {} +
или xargs
, не может быть выполнен с помощью xargs
.
В отношении производительности find | xargs
подразумевает больше работы (по крайней мере, один дополнительный процесс и передача этих данных через трубопровод), и, поскольку некоторые из этих действий выполняются параллельно (find
и xargs
работают одновременно), это может привести к конкуренции, так как как find
, так и cmd
конкурируют за доступ к I/O, что, вероятно, будет использовать значительно больше ресурсов в целом. Благодаря этой параллельности, в некоторых ситуациях, однако, он может выполнить задачу быстрее, так как find
может продолжить поиск дополнительных файлов, в то время как cmd
занят выполнением вычислительно интенсивных задач с предыдущей пачкой (по крайней мере, пока трубопровод и внутренний выходной буфер find
оба не будут заполнены).
С find . -exec cmd {} +
cmd
легче прервать весь поиск. Например, с:
find . -exec sh -c 'if some-condition; then kill -s PIPE "$PPID"; exit 1; fi' sh {} +
С find . -print0 | xargs -0 cmd
cmd
может сделать exit 255
, чтобы прервать xargs
, но find
не завершится после этого, пока не попытается записать следующий блок в трубопровод.
xargs -0 cmd по сравнению с -exec cmd {} +
Основной аргумент в пользу этого заключается в том, что в общем смысле это более общее, гибкое и универсальное решение.
Вывод find
с -print0
представляет собой обрабатываемое после вывода представление списка файлов, которое может быть использовано чем угодно, не только xargs -0
. Например, вы можете сделать:
find . -print0 |
grep -z foo |
sort -z
И все равно получить обрабатываемый после вывода, отфильтрованный и отсортированный список путей к файлам.
Аналогично xargs -0
можно использовать для тех списков с разделителем NUL, независимо от того, являются ли они выходом из find
или чем-либо еще, независимо от того, представляют ли они пути к файлам или что-либо еще.
В этом смысле find . -exec cmd {} +
предназначен только для узкого специального случая использования (даже если это один из самых распространенных).
С xargs
вы можете использовать опцию -n
или -s
, чтобы ограничить количество аргументов, передаваемых в cmd
. С GNU xargs
также посмотрите опцию -P
, чтобы запустить несколько экземпляров cmd
параллельно, или xargs -0 -J {} mv {} /dest/
некоторых BSD, чтобы разрешить дополнительные аргументы после списка файлов.
Вы можете сохранить вывод find . -print0
в файл и обработать его позже (например, только если find
был полностью успешен) с помощью xargs -0 cmd < file
, избегая вмешательства вывода cmd
в результат find
, включая такие вещи, как (с GNU xargs
):
xargs -r0a =(find . -print0) cmd # zsh
xargs -r0a (find . -print0|psub -f) cmd # fish
Не существует эквивалента специальной обработки exit 255
для xargs
с -exec cmd {} +
(хотя см. выше о kill "$PPID"
).
С find | xargs
вы можете легче запускать find
и xargs cmd
в разных локалях или, более широко, в разных средах (включая переменные, ограничения, umask…)
Например, часто необходимо запускать find
в локали C, чтобы обойти проблемы с не текстовыми именами файлов, но при этом часто хочется, чтобы cmd
выполнялся в локали пользователя.
LC_ALL=C find . -exec cmd {} +
Запускает как find
, так и cmd
в локали C. А также:
LC_ALL=C find . -exec env -u LC_ALL cmd {} +
является сначала нестандартным, но также может не восстановить оригинальную локаль для cmd
, если LC_ALL
была определена заранее.
LC_ALL=C find . -print0 | xargs -r0 cmd
Меняет локаль на C только для find
.
В качестве особого случая:
find . -exec sudo cmd {} +
Часто не удается избежать ограничения на размер args+env, так как sudo
устанавливает переменную окружения SUDO_COMMAND
, что приводит к дублированию списка аргументов.
find . -print0 | sudo xargs -r0 cmd
Не имеет этой проблемы, так как в этом случае $SUDO_COMMAND
содержит только xargs -0 cmd
(не используйте find . -print0 | xargs -r0 sudo cmd
).
Смотрите также:
sudo find . -print0 | xargs -r0 cmd
Где список файлов обнаруживается root
, но cmd
запускается от имени оригинального пользователя. Или
find . -print0 | USERNAME=some-user xargs -r0 cmd
В оболочках, таких как zsh, которые поддерживают встроенную смену (e)uid, (e)gids.
find . -print0 | xargs -r0 -- "${cmd[@]}"
Работает независимо от того, что содержит массив $cmd
, в то время как
find . -exec "${cmd[@]}" {} +
Не удается, если какой-либо из элементов массива $cmd
является ;
или если там есть {}
, +
подряд.
Что более портативно, надежно, эффективно, универсально и почему?
Вот сравнительный тест с 1,249,367
файлами в /
с использованием XFS на Gen4 NVME
# 0m1.537s
time find / -exec echo -- \{} +
# 0m0.889s
time find / -print0 | xargs -0
Лично я предпочитаю вывод через трубопровод из-за читаемости.
Разница в производительности намного меньше и едва уловима при обработке файлов в реальных сценариях.
Более того, помимо технических различий, на которые указал @Stéphane Chazelas, вы скорее увидите значительную разницу в повседневном использовании с новыми инструментами, такими как fd (Rust)
# 0m0.577s
cd /
fd
.
Ответ или решение
Сравнение методов find . -exec cmd {} + и find . -print0 | xargs -0 cmd
Вопрос о сравнении команд find . -exec cmd {} +
и find . -print0 | xargs -0 cmd
является актуальным среди специалистов в области информационных технологий. Обе конструкции предназначены для выполнения команд над файлами, найденными утилитой find
, но имеют свои особенности, касающиеся надежности, портативности, эффективности и универсальности.
Портативность
Команда find . -exec cmd {} +
является стандартной и поддерживается большей частью UNIX-подобных систем, включая Linux и BSD. Её простота позволяет широко использовать этот вариант без дополнительной настройки. Напротив, конструкция find . -print0 | xargs -0 cmd
зависит от конкретных реализаций утилит find
и xargs
, что может вызвать проблемы на некоторых системах, таких как AIX и HP/UX. В большинстве случаев -print0
и -0
встречаются только в GNU и некоторых BSD-реализациях.
Надежность
Использование find . -exec cmd {} +
обеспечивает большую надежность в работе. Если find
завершится с ошибкой, команда cmd
не будет вызвана с неправильными или недостаточными аргументами. Важно отметить, что данная команда вызовет cmd
лишь однажды с большим количеством найденных файлов, что минимизирует риск ситуации, когда команда вызывает ошибку во время обработки.
С другой стороны, при использовании find . -print0 | xargs -0 cmd
существуют риски, связанные с случайным завершением find
. Если find
завершится неожиданно, это может гарантировать, что xargs
получит некорректный аргумент, что в свою очередь может привести к выполнению команды с неправильными данными.
Эффективность
С точки зрения производительности, использование xargs
может быть более быстрым в некоторых сценариях. Утилита xargs
будет вызывать команду cmd
, как только она будет готова, что позволяет параллельно обрабатывать файлы и выполнять команды. Однако, за это платится цена в виде дополнительных ресурсов, так как задействуется больше процессов и использование системы ввода-вывода.
Сравнительные тесты показывают, что в больших файловых системах с множеством файлов, команда find . -print0 | xargs -0 cmd
может работать быстрее, чем find . -exec cmd {} +
, благодаря тому, что искать файлы и запускать команды возможно одновременно. Однако разница в скорости может быть незначительной в реальных условиях, что делает выбор варианта более субъективным.
Универсальность
Универсальность конструкций тоже является важным аспектом. xargs
предоставляет больше возможностей для работы со списками аргументов. Например, можно легко изменять количество передаваемых аргументов, использовать дополнительные опции для параллельной обработки или фильтрации результатов. Это делает конструкцию более гибкой в сравнении с фиксированным подходом -exec
.
К тому же, xargs
позволяет применять функции на основе выходных данных других команд, что предоставляет более широкий набор возможностей для манипуляции с данными. Например, комбинация find
и grep
, а затем использование xargs
для передачи отфильтрованных файлов на выполнение следующей команды.
Заключение
В заключение, каждая из команд имеет свои достоинства и недостатки, и выбор между ними зависит от конкретного контекста использования. В общем, рекомендуется использовать find . -exec cmd {} +
, когда необходима портативность и надежность, а find . -print0 | xargs -0 cmd
— когда требуется больше гибкости и возможности обработки больших объемов данных. Обращение внимания на функциональные особенности каждой опции позволит более эффективно решать задачи в области управления файлами и ресурсами системы.