Зачем не просто loop в bash
Допустим хочешь сжать все .log старше 7 дней. На bash напрямую:
for f in $(find /var/log -name '*.log' -mtime +7); do
gzip "$f"
done
Проблемы:
- Word splitting на пробелах - файл
my app.logраспадётся на два аргумента. - Команда форкается отдельно для каждого файла - медленно при тысячах.
- Bash может не справиться с длинной командной строкой при огромном выводе find.
find -exec и xargs решают всё это - они корректно работают с пробелами,
батчат вызовы и грамотно обходят ARG_MAX.
find -exec - два варианта
\; - один-в-один
find /var/log -name '*.log' -mtime +7 -exec gzip {} \;Запускает gzip отдельно для каждого файла. {} подставляется на путь.
\; - литеральная точка с запятой (надо экранировать от bash).
Минус: 1000 файлов = 1000 fork'ов = медленно.
+ - батч
find /var/log -name '*.log' -mtime +7 -exec gzip {} +Накапливает аргументы и вызывает gzip один раз с пачкой файлов
(или несколько раз если упёрлось в ARG_MAX). На порядок быстрее.
Используй + всегда когда команда поддерживает множество файлов
(gzip, rm, chmod, chown, cp). Используй \; только когда нужен один файл
за раз (например shell-конструкция с условием).
xargs - pipe-вариант
find /var/log -name '*.log' -mtime +7 | xargs gzip
Эквивалент find ... -exec gzip {} +. По умолчанию xargs батчит и сам
следит за ARG_MAX.
Зачем оба способа существуют: xargs берёт ввод из любого pipe'а
(не только find), и имеет больше опций (параллельность, подстановка
в произвольное место). find -exec короче для простых случаев.
ВАЖНО: -print0 / -0 для безопасности
Дефолтный разделитель xargs - whitespace. Файл foo bar.log он разрежет
пополам и попытается обработать foo и bar.log как два пути.
Кавычки тоже трактуются специально.
Решение - null-byte как разделитель:
find /var/log -name '*.log' -print0 | xargs -0 gzip
find -print0- печатает имена через\0вместо\n.xargs -0- читает разделённое\0.
Имя файла не может содержать \0 (это терминатор C-строк), поэтому
такой пайп всегда корректен.
Правило: если ты не уверен на 100% что в именах нет пробелов/кавычек -
всегда -print0 | xargs -0. Или find -exec ... +.
xargs - практичные опции
| Флаг | Что делает |
|---|---|
-0 | разделитель - \0 (для find -print0) |
-I {} | подставлять {} в произвольное место в команде |
-n N | по N аргументов на вызов |
-P N | до N параллельных процессов |
-r / --no-run-if-empty | не запускать команду если ввод пустой |
-t | trace - печатать команду перед выполнением |
--max-args=1 | то же что -n 1 |
-d $'\n' | свой разделитель (например только newline) |
Подстановка в середину - -I
ls *.tar.gz | xargs -I {} mv {} /backup/{}.bak# или
cat hosts.txt | xargs -I HOST ssh HOST 'uptime'
По умолчанию xargs кладёт аргументы в конец команды. С -I подставит
туда где написал плейсхолдер. Подходит когда нужен файл и до и после
(например mv X /dst/X.bak).
Цена: -I неявно ставит -n 1 - вернулись к одному вызову на аргумент.
Параллельность - -P
find . -name '*.log' -print0 | xargs -0 -n1 -P4 gzip
▸4 параллельных gzip'а
Полезно когда задача CPU-bound и есть несколько ядер. Для I/O-bound
можно ставить -P больше числа ядер.
Внимание: при -P > 1 порядок вывода непредсказуем - несколько
процессов пишут в stdout вперемешку.
Batch size - -n
echo {1..100} | xargs -n 5 echo▸echo 1 2 3 4 5
▸echo 6 7 8 9 10
▸...
Полезно когда команда принимает ограниченное число аргументов или ты хочешь видеть прогресс по группам.
Типичные паттерны
Удалить старые логи
find /var/log -name '*.log.gz' -mtime +30 -delete
# или
find /var/log -name '*.log.gz' -mtime +30 -print0 | xargs -0 rm
-delete встроен в find и быстрее xargs (не форкает rm). Используй его
когда нужно просто удалить.
Поиск содержимого только в свежих файлах
find /var/log -name '*.log' -mtime -1 -print0 | xargs -0 grep -l 'ERROR'
grep -l - печатать только имена файлов где найдено.
Перенумеровать или ssh-команда на список хостов
cat servers.txt | xargs -I {} -P 10 ssh {} 'systemctl restart myapp'▸10 параллельных SSH-сессий
Убить процессы по паттерну
ps aux | grep '[m]yapp' | awk '{print $2}' | xargs killБез [m] сам grep попадёт в свой grep - старый трюк.
Лучше: pkill myapp - атомарно и не форкает 4 процесса.
Изменить mode/owner на множестве файлов
find /srv/upload -type f -print0 | xargs -0 chmod 644
find /srv/upload -type d -print0 | xargs -0 chmod 755
Конвертировать кучу картинок (параллельно)
find . -name '*.png' -print0 | xargs -0 -n1 -P$(nproc) -I IMG \
convert IMG -resize 50% IMG.thumb.png
$(nproc) - число ядер; -n1 -I обязательно если нужен placeholder.
Подвохи и ошибки
-
xargsбез-rзапустит команду даже на пустом вводе:bashfind . -name '*.tmp' | xargs rm # если find ничего не нашёл - rm запустится без аргументов
На GNU xargs (Linux) это окей (rm без аргументов скажет «missing operand»), но на BSD/macOS поведение может быть другим. Безопасно:
xargs -r rm. Или используйfind -exec ... +- он не запускает команду на пустом вводе. -
Кавычки и
xargs:'"для xargs специальные. Без-0строки"foo bar"парсятся как одна. Это не то что хочешь когда читаешь файл с путями. Всегда-0или-d $'\n'. -
find -exec ...vsfind ... | xargs: для одной команды разницы по результату нет, ноfind -execкороче и всегда null-safe (find сам передаёт пути argv'ом). Используй его если не нужен pipe-входной поток. -
{}в shell-конструкции: так не работает:bashfind . -name '*.log' -exec sh -c 'echo Processing {}' \;▸Processing {} (без подстановки!)
В shell-команде нужно явно передать через позиционный аргумент:
bashfind . -name '*.log' -exec sh -c 'echo "Processing $1"' _ {} \;_- фиктивный$0,{}идёт как$1.
Когда не использовать xargs
-
Сложная shell-логика на каждый файл - пиши обычный
while read-цикл (см. bash-scripting):bashfind . -type f -name '*.log' -print0 | while IFS= read -r -d '' f; do
if [[ -s "$f" ]]; then
echo "Non-empty: $f"
fi
done
-
Нужны массивы / map'ы - переписывай на Python.
-
Параллельность с обработкой результатов - лучше GNU
parallel(надмножество xargs с UI прогресса и логов).