# xargs и find -exec - массовые операции _Команды · LinuxLab Knowledge Base_ **TL;DR:** Два способа применить команду к набору файлов: `find ... -exec cmd {} +` (внутри find) и `... | xargs cmd` (через pipe). Для безопасности с пробелами/спецсимволами - связка `find -print0 | xargs -0`. ## Зачем не просто loop в bash Допустим хочешь сжать все `.log` старше 7 дней. На bash напрямую: ```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 - два варианта ### `\;` - один-в-один ```bash find /var/log -name '*.log' -mtime +7 -exec gzip {} \; ``` Запускает `gzip` **отдельно** для каждого файла. `{}` подставляется на путь. `\;` - литеральная точка с запятой (надо экранировать от bash). Минус: 1000 файлов = 1000 fork'ов = медленно. ### `+` - батч ```bash find /var/log -name '*.log' -mtime +7 -exec gzip {} + ``` Накапливает аргументы и вызывает `gzip` **один раз** с пачкой файлов (или несколько раз если упёрлось в ARG_MAX). На порядок быстрее. **Используй `+` всегда когда команда поддерживает множество файлов** (gzip, rm, chmod, chown, cp). Используй `\;` только когда нужен один файл за раз (например shell-конструкция с условием). ## xargs - pipe-вариант ```bash 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 как разделитель: ```bash 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` ```bash 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` ```bash find . -name '*.log' -print0 | xargs -0 -n1 -P4 gzip # → 4 параллельных gzip'а ``` Полезно когда задача CPU-bound и есть несколько ядер. Для I/O-bound можно ставить `-P` больше числа ядер. Внимание: при `-P > 1` **порядок вывода непредсказуем** - несколько процессов пишут в stdout вперемешку. ### Batch size - `-n` ```bash echo {1..100} | xargs -n 5 echo # → echo 1 2 3 4 5 # → echo 6 7 8 9 10 # → ... ``` Полезно когда команда принимает ограниченное число аргументов или ты хочешь видеть прогресс по группам. ## Типичные паттерны ### Удалить старые логи ```bash 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). Используй его когда нужно просто удалить. ### Поиск содержимого только в свежих файлах ```bash find /var/log -name '*.log' -mtime -1 -print0 | xargs -0 grep -l 'ERROR' ``` `grep -l` - печатать только имена файлов где найдено. ### Перенумеровать или ssh-команда на список хостов ```bash cat servers.txt | xargs -I {} -P 10 ssh {} 'systemctl restart myapp' # → 10 параллельных SSH-сессий ``` ### Убить процессы по паттерну ```bash ps aux | grep '[m]yapp' | awk '{print $2}' | xargs kill ``` Без `[m]` сам grep попадёт в свой grep - старый трюк. Лучше: `pkill myapp` - атомарно и не форкает 4 процесса. ### Изменить mode/owner на множестве файлов ```bash find /srv/upload -type f -print0 | xargs -0 chmod 644 find /srv/upload -type d -print0 | xargs -0 chmod 755 ``` ### Конвертировать кучу картинок (параллельно) ```bash find . -name '*.png' -print0 | xargs -0 -n1 -P$(nproc) -I IMG \ convert IMG -resize 50% IMG.thumb.png ``` `$(nproc)` - число ядер; `-n1 -I` обязательно если нужен placeholder. ## Подвохи и ошибки 1. **`xargs` без `-r` запустит команду даже на пустом вводе:** ```bash find . -name '*.tmp' | xargs rm # если find ничего не нашёл - rm запустится без аргументов ``` На GNU xargs (Linux) это окей (rm без аргументов скажет «missing operand»), но на BSD/macOS поведение может быть другим. Безопасно: `xargs -r rm`. Или используй `find -exec ... +` - он не запускает команду на пустом вводе. 2. **Кавычки и `xargs`:** `'` `"` для xargs **специальные**. Без `-0` строки `"foo bar"` парсятся как одна. Это не то что хочешь когда читаешь файл с путями. Всегда `-0` или `-d $'\n'`. 3. **`find -exec ...` vs `find ... | xargs`:** для одной команды разницы по результату нет, но `find -exec` короче и всегда null-safe (find сам передаёт пути argv'ом). Используй его если не нужен pipe-входной поток. 4. **`{}` в shell-конструкции:** так не работает: ```bash find . -name '*.log' -exec sh -c 'echo Processing {}' \; # → Processing {} (без подстановки!) ``` В shell-команде нужно явно передать через позиционный аргумент: ```bash find . -name '*.log' -exec sh -c 'echo "Processing $1"' _ {} \; ``` `_` - фиктивный `$0`, `{}` идёт как `$1`. ## Когда не использовать xargs - **Сложная shell-логика на каждый файл** - пиши обычный `while read`-цикл (см. [bash-scripting](/kb/bash-scripting.md)): ```bash find . -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 прогресса и логов). ## Команды ```bash find /var/log -name '*.log' -mtime +7 -exec gzip {} + ``` Сжать все .log старше 7 дней. + вместо \; - батчем, на порядок быстрее ```bash find . -type f -print0 | xargs -0 grep -l 'ERROR' ``` Безопасный grep по всем файлам - null-разделитель не ломается на пробелах ```bash cat hosts.txt | xargs -I {} -P 10 ssh {} 'uptime' ``` Параллельный SSH на список хостов - 10 одновременных сессий ```bash find . -name '*.tmp' -delete ``` Встроенное удаление в find - быстрее xargs rm и атомарно ```bash ls *.png | xargs -n1 -P$(nproc) -I IMG convert IMG -resize 50% IMG.small.png ``` Параллельный resize по числу ядер - заполнить все CPU работой ## См. также - [find - поиск файлов по предикатам](/kb/cmd-find.md) - [grep - поиск строк по шаблону](/kb/cmd-grep.md) - [sed - потоковый редактор текста](/kb/cmd-sed.md) - [awk - обработка структурированного текста по полям](/kb/cmd-awk.md) - [bash-скрипты - основы и идиомы](/kb/bash-scripting.md) - [rsync - инкрементальная синхронизация файлов](/kb/cmd-rsync.md)