Проблема: команды, не читающие stdin
Большинство команд читают stdin (grep, sort, wc, awk, sed). Но многие — нет (rm, cp, mv, ls, mkdir). Они ждут аргументы в командной строке.
# Не работает — rm не читает stdin:
$ find /tmp -name '*.tmp' | rm
# Не работает — cp не читает stdin:
$ ls *.csv | cp /backup/
Решение — xargs. Он читает stdin, собирает токены в arguments командной строки следующей команды и запускает.
$ find /tmp -name '*.tmp' | xargs rm
# xargs читает stdin построчно (или по whitespace), строит:
# rm /tmp/file1.tmp /tmp/file2.tmp /tmp/file3.tmp
# и запускает
Базовый синтаксис
$ command_producing_args | xargs cmd_to_run [initial_args]
xargs читает stdin и добавляет токены после cmd_to_run [initial_args]:
$ echo "file1 file2 file3" | xargs ls -l
ls -l file1 file2 file3
$ echo "file1\nfile2\nfile3" | xargs touch
touch file1 file2 file3
xargs batches аргументы автоматически — если их слишком много (превышает ARG_MAX, обычно ~2 MB на Linux), xargs разделит на несколько вызовов:
# 100,000 файлов? Не проблема — xargs запустит rm batches:
$ find /tmp -name '*.tmp' | xargs rm
# xargs: rm /tmp/file1 ... /tmp/file9000
# xargs: rm /tmp/file9001 ... /tmp/file18000
# (batches на ~9000 файлов)
-I : placeholder
По default xargs добавляет аргументы в конец команды. Если нужно подставить в середину — используй -I:
$ ls /tmp/*.csv | xargs -I {} cp {} /backup/
# xargs запускает для каждого:
# cp /tmp/file1.csv /backup/
# cp /tmp/file2.csv /backup/
# ... (по одному вызову на файл, не batch)
$ ls *.tar | xargs -I {} tar -xf {} -C /tmp/extracted
-I {} означает «использовать {} как placeholder». Сам символ {} — convention, можно использовать любой:
$ ls *.csv | xargs -I FILE mv FILE archive/
Когда нужен -I:
- Файл нужен в середине команды
- Нужно несколько раз использовать аргумент:
xargs -I {} cp {} {}.bak - Нужны вокруг аргумента ещё параметры:
xargs -I {} tar -xf {} -C /dest
Минус -I: по одному вызову на token (нет batching). Медленнее для большого числа файлов. Без -I xargs batches optimally.
-0 / -print0: NUL-separator для safe-with-spaces
По default find выводит имена файлов через \n, а xargs читает по whitespace (space, tab, newline). Это ломается на файлах с пробелами или quotes:
$ touch "/tmp/file with spaces.csv"
$ find /tmp -name '*.csv' | xargs ls
ls: cannot access '/tmp/file': No such file or directory
ls: cannot access 'with': ...
ls: cannot access 'spaces.csv': ...
# xargs trato "file with spaces.csv" как ТРИ separate arguments
Решение — использовать NUL-character (\0) как separator:
$ find /tmp -name '*.csv' -print0 | xargs -0 ls
# -print0: find выводит имена разделённые \0 (не \n)
# -0: xargs читает \0-separated
# \0 не может быть в имени файла (Linux запрещает) — гарантия safety
-0 (или --null) — всегда используй в production-скриптах с find и xargs. Безопасно даже с самыми гнусными именами.
DE-сценарии xargs
1. Удалить старые файлы
$ find /var/log/etl -name '*.log.*' -mtime +30 -print0 | xargs -0 rm
# Удалить .log.* файлы старше 30 дней
2. Параллельная обработка
xargs -P N запускает N процессов одновременно:
$ ls *.csv | xargs -P 4 -I {} python3 process.py {}
# 4 параллельных Python-процесса
Это classical способ горизонтального scaling на одной машине. На 16-core CPU — xargs -P 16 использует все ядра.
# Compress all logs параллельно:
$ find /var/log -name '*.log' -mtime +1 -print0 | xargs -0 -P 8 gzip
# 8 параллельных gzip
Без -P 8 это последовательно (один gzip на файл). С -P 8 — параллельно, в 8 раз быстрее на multi-core.
3. Find + grep recursive
$ find /opt -name '*.py' -print0 | xargs -0 grep 'TODO'
# Эквивалент grep -r TODO /opt/*.py
# Но xargs allows extra grep flags, parallel, etc.
4. Удалить только если матчит pattern
$ ls /tmp/*.csv | xargs -I {} sh -c 'wc -l {} | awk "\$1 == 0 {print \$2}"' | xargs rm
# Удалить пустые CSV
Альтернативы xargs
find -exec
# Эквивалент xargs:
$ find . -name '*.tmp' -exec rm {} +
# Плюс в конце — batching как xargs (без +, отдельный rm на файл)
$ find . -name '*.tmp' -exec rm {} \;
# Точка-с-запятой — отдельный exec на каждый файл (медленно для multi)
# С действием в середине:
$ find . -name '*.csv' -exec cp {} /backup/ \;
-exec ... + ≈ xargs. -exec ... \; ≈ xargs -I {} (по одному вызову).
parallel (GNU parallel)
$ ls *.csv | parallel python3 process.py
$ ls *.csv | parallel -j 4 python3 process.py {}
GNU parallel — мощнее xargs (выводы interleave-free, более удобный syntax, лучшая отчётность), но не всегда установлен. Для cross-platform xargs -P достаточно.
Process substitution: <(...) и >(...)
Process substitution — bash-механизм, превращающий вывод/вход команды в файл-аналог (FIFO). Это то, что использует tee >(grep ...).
<(...) — output команды как input файл
# Команда требует "файл" аргументом, но у нас данные с stdin/команды:
$ diff <(sort file1) <(sort file2)
# diff видит два "файла" — каждый это вывод sort
# Не нужно создавать temp-файлы
$ comm -12 <(sort -u list1) <(sort -u list2)
# Intersection отсортированных уникальных списков
Под капотом bash:
- Создаёт FIFO
/dev/fd/63(или похожий). - Запускает
sort file1, направляет вывод в FIFO. - Передаёт
/dev/fd/63как аргумент diff. - diff читает «файл» = FIFO = вывод sort.
# Сравнить вывод двух команд:
$ diff <(ssh prod 'cat /etc/myapp.conf') <(cat /local/myapp.conf)
# Diff config production vs local
>(...) — input команды как output файл
Обратное: использует команду как destination для записи. Пример уже видели в уроке 04:
$ command | tee >(grep ERROR > err.log) > all.log
# tee пишет в "файл" /dev/fd/63 (FIFO), на другой стороне которого grep
DE-pipeline с process substitution
1. Сравнение dataset
SQL SELECT — как получить данные для bash-сравнения$ diff <(psql -c 'SELECT * FROM today_orders' | sort) <(psql -c 'SELECT * FROM yesterday_orders' | sort)
Diff между двумя SQL-запросами без temp-файлов.
2. Multiple inputs для команды
$ paste <(cut -d ',' -f 1 names.csv) <(cut -d ',' -f 3 ages.csv) > combined.tsv
Combine две колонки из разных CSV.
3. Lossless tee + transform
$ curl -s api.com/data \
| tee >(jq '.metadata' > metadata.json) \
| jq '.records[]'
Сохранить metadata в файл, продолжить pipeline с records.
Когда что использовать
Каждый инструмент в своей нише.
Попробуй сам
- Базовый xargs:
echo "file1 file2 file3" | xargs touch ls file* rm file* - Безопасный с -0:
touch "/tmp/a b c.txt" find /tmp -name '*.txt' -print0 | xargs -0 ls -l | head rm "/tmp/a b c.txt" - Параллелизация:
seq 1 10 | xargs -P 4 -I {} sh -c 'echo "Job {}"; sleep 0.5' # 10 jobs в 4 параллельных потоках - Process substitution с diff:
echo -e "a\nb\nc" > /tmp/f1 echo -e "a\nB\nc" > /tmp/f2 diff <(sort /tmp/f1) <(sort /tmp/f2) - comm с inline sort:
echo -e "apple\nbanana\ncherry" > /tmp/f1 echo -e "banana\ncherry\ndate" > /tmp/f2 comm -12 <(sort /tmp/f1) <(sort /tmp/f2) # Intersection: banana, cherry
macOS-различия
xargsидентичен на macOS и Linux для базовых флагов (-I,-0,-n).xargs -Pна macOS поддерживается (BSD xargs).find -print0поддерживается (BSD find).- Process substitution
<(...),>(...)— bash/zsh feature. Работает на macOS если shell — bash или zsh (default). - На pure
/bin/shилиdash(Debian default для скриптов) — process substitution не работает.
Главное
xargsпревращает stdin в arguments следующей команды. Без него —rm,cp,mvне работают с pipe.-I {}— placeholder для middle-of-command substitution. Минус: один вызов на token.-0(сfind -print0) — NUL-separated для safety с file names содержащими пробелы.-P N— N параллельных процессов. Простой horizontal scaling.find ... -exec ... +≈xargs(batching),find ... -exec ... \;≈xargs -I(one-at-a-time).- Process substitution
<(cmd)— команда как input file (для diff, comm, join, paste). - Process substitution
>(cmd)— команда как output file (для tee fan-out). - Process substitution — bash/zsh, не POSIX.