xargs и find -exec: массовые операции над файлами

find отбирает файлы по условиям, а -exec и xargs выполняют команду над каждым найденным — это массовое редактирование файловой системы без ручного цикла.

find рекурсивно обходит дерево каталогов и отбирает файлы по предикатам (имя, тип, размер, время); xargs превращает поток имён из stdin в аргументы команды. Вместе они выполняют операции над тысячами файлов одной строкой.

«Удалить все .tmp старше недели», «сжать все логи», «заменить строку во всех .conf», «найти крупные файлы» — это повседневные задачи администрирования. Делать их циклом в скрипте можно, но find выражает условия декларативно и работает быстрее, а xargs ещё и распараллеливает.

find: отбор по условиям

Синтаксис: find ГДЕ УСЛОВИЯ ДЕЙСТВИЕ. Условия комбинируются и по умолчанию соединяются логическим И.

ПредикатСмысл
-name '*.log'по имени (шаблон в кавычках!)
-inameимя без учёта регистра
-type f / dобычный файл / каталог
-mtime +7изменён более 7 суток назад
-size +100Mбольше 100 мегабайт
-maxdepth 2ограничить глубину обхода
# все .log в /var/log и глубже
find /var/log -type f -name '*.log'

# каталоги node_modules в текущем дереве
find . -type d -name node_modules

# файлы крупнее 100 МБ
find / -type f -size +100M 2>/dev/null

# изменённые за последние сутки
find . -type f -mtime -1

# объединение по ИЛИ: .jpg или .png
find . -type f \( -name '*.jpg' -o -name '*.png' \)

Шаблон в -name обязательно берут в кавычки: иначе *.log раскроет оболочка по файлам текущего каталога ещё до запуска find.

Действие прямо в find: -exec

Опция -exec запускает команду для каждого найденного файла. Плейсхолдер {} подставляет имя файла, а команда завершается \; (точка с запятой, экранированная от оболочки).

# показать детали каждого найденного файла
find . -name '*.conf' -exec ls -l {} \;

# удалить старые временные файлы
find /tmp -name '*.tmp' -mtime +7 -exec rm {} \;

# заменить строку во всех .conf через sed -i
find . -name '*.conf' -exec sed -i.bak 's/DEBUG/INFO/' {} \;

Различие \; и + в конце важно для производительности:

# \;  — запуск команды ОТДЕЛЬНО для каждого файла (медленно, тысячи процессов)
find . -name '*.log' -exec gzip {} \;

# +   — собрать МНОГО файлов в один вызов команды (как xargs, быстро)
find . -name '*.txt' -exec grep -l 'TODO' {} +

Вариант с + подставляет сразу пачку имён в один запуск команды — это в разы быстрее на больших объёмах, но команда должна уметь принимать много аргументов (как grep, rm, gzip).

xargs: имена из stdin в аргументы

Альтернатива — отдать список имён в xargs, который соберёт их в аргументы команды. Это даёт гибкость конвейера: можно фильтровать список любыми утилитами между find и xargs.

# удалить найденные файлы
find . -name '*.bak' | xargs rm

# посчитать строки во всех .py
find . -name '*.py' | xargs wc -l

# -n1 — по одному аргументу на вызов; -I{} — подставить в произвольное место
find . -name '*.log' | xargs -I{} mv {} {}.old

Главная ловушка: пробелы в именах

По умолчанию xargs разбивает вход по пробелам и переносам строк. Имя «my file.txt» превратится в два аргумента — «my» и «file.txt», и команда сломается или удалит не то. Это классический источник опасных багов.

Решение — нулевой разделитель: find -print0 разделяет имена байтом \0 (который в путях встречаться не может), а xargs -0 его понимает:

# ОПАСНО при пробелах в именах
find . -name '*.txt' | xargs rm

# БЕЗОПАСНО: разделитель — нулевой байт
find . -name '*.txt' -print0 | xargs -0 rm

# то же с подстановкой места аргумента
find . -name '*.mp3' -print0 | xargs -0 -I{} cp {} /backup/

Правило: find в xargs — всегда через -print0 | xargs -0. Это страхует от пробелов, переносов и спецсимволов в именах.

Параллелизм: xargs -P

Опция -P N запускает до N команд параллельно. На многоядерной машине это кратно ускоряет независимые операции — конвертацию картинок, сжатие, обработку файлов.

# сжать все .log в 4 параллельных потока
find . -name '*.log' -print0 | xargs -0 -P4 -n1 gzip

# по числу ядер: -P$(nproc)
find . -name '*.png' -print0 | xargs -0 -P$(nproc) -n1 optipng

Флаг -n1 здесь говорит «по одному файлу на процесс», чтобы было что распараллеливать. Без -P xargs работает последовательно.

Как это работает под капотом

find обходит дерево каталогов в глубину, для каждого узла последовательно вычисляя предикаты слева направо с коротким замыканием логики И/ИЛИ: как только условие провалилось, остальные не проверяются (поэтому дешёвые проверки вроде -name ставят раньше дорогих вроде -exec). Различие -exec ... \; и -exec ... + — это число порождённых процессов: точка с запятой делает fork+exec на каждый файл, а + накапливает имена в буфер и запускает команду пачками, упираясь в системный лимит длины командной строки ARG_MAX (обычно ~2 МБ). Ровно ту же логику пачек реализует xargs: он читает stdin, набивает аргументы до ARG_MAX и запускает команду, повторяя цикл. Поэтому find | xargs и find -exec + близки по скорости. Опция -print0/-0 существует, потому что единственный символ, запрещённый в путях Unix, — это \0; только он может служить надёжным разделителем. Параллелизм -P реализован просто: xargs держит до N дочерних процессов одновременно, запуская новый, как только освободился слот.

Частые ошибки

  • Пробелы в именах. find | xargs без -print0/-0 ломается на именах с пробелами — может удалить не те файлы. Всегда нулевой разделитель.
  • Незакавыченный шаблон. find . -name *.log без кавычек раскрывается оболочкой и даёт ошибку либо неверный результат.
  • Забытый -type f. Операция, рассчитанная на файлы, цепляет каталоги — например chmod или rm срабатывает не так, как ожидалось.
  • Медленный \; на больших объёмах. Тысячи отдельных запусков команды тормозят; используйте + или xargs, если команда принимает много аргументов.
  • find -delete без проверки. Сначала запустите find БЕЗ действия и посмотрите список, и только потом добавляйте -delete или -exec rm.

Итоги

  • find отбирает файлы по предикатам: -name, -type, -mtime, -size, -maxdepth; условия соединяются И, ИЛИ через -o.
  • -exec cmd {} \; запускает команду на каждый файл; -exec cmd {} + — пачками (быстрее).
  • xargs превращает stdin в аргументы; -I{} подставляет имя в нужное место, -n1 — по одному.
  • Против пробелов в именах — всегда find -print0 | xargs -0.
  • xargs -P N распараллеливает независимые операции по N процессов.
  • Перед удаляющими действиями сначала смотрите список без действия.
Проверьте себя
1. Зачем использовать find -print0 в паре с xargs -0?
Aчтобы ускорить обход дерева каталогов
Bчтобы корректно обработать имена файлов с пробелами и спецсимволами
Cчтобы find выводил абсолютные пути
Dчтобы включить параллельную обработку
2. Чем -exec cmd {} + отличается от -exec cmd {} \; ?
A+ запускает команду параллельно, \; последовательно
B+ передаёт много файлов в один вызов команды, \; запускает её на каждый файл отдельно
Cразницы нет, это синонимы
D+ работает только с rm