July 28

xargs: команда, которая делает всё остальное компонуемым

Это перевод оригинальной статьи xargs: The command that makes everything else composable.

Подписывайтесь на телеграм-канал usr_bin, где я публикую много полезного по Linux, в том числе ссылки на статьи в этом блоге.

Вы видели её в ответах на Stack Overflow. Вы копировали и вставляли её один или два раза. Вы примерно знаете, что она как-то связана с конвейерами.

Но если бы вас попросили написать команду xargs с нуля, вы, скорее всего, задумались бы.

В этом и заключается пробел в понимании xargs. Большинство разработчиков знают, что она существует. Почти никто не использует её уверенно. Эта статья исправит это.

Фотография Фото Tommy Diner на Unsplash.

Проблема, которую она решает

Конвейер Unix | — мощный инструмент. Он берет вывод одной команды и передает его в качестве входных данных следующей команде.

cat files.txt | grep "error"

Это работает, потому что grep предназначена для чтения из stdin. Но многие команды устроены иначе. Они ожидают аргументы, а не stdin.

Попробуйте это:

echo "file.txt" | rm

Это не сработает. rm не читает из stdin. Она ожидает аргументы: rm file.txt.

Именно этот пробел и заполняет xargs. Она читает данные из stdin и преобразует этот ввод в аргументы для другой команды

echo "file.txt" | xargs rm

Теперь это работает. xargs получил file.txt из stdin и передала его в rm как аргумент.

В этом и заключается вся модель понимания. xargs преобразует stdin в аргументы.

Базовый синтаксис

command1 | xargs command2

command1 формирует вывод. xargs берет этот вывод и передает его в качестве аргументов для command2.

По умолчанию xargs разбивает ввод по пробельным символам и символам новой строки, а затем за один вызов передает все элементы как аргументы.

echo "a b c" | xargs echo
# Output: a b c

Всё просто. Теперь давайте сделаем это полезным.

Реальное использование, шаг за шагом

Удаление списка файлов

У вас есть файл, содержащий пути к файлам, которые нужно удалить:

cat to_delete.txt | xargs rm

Или найти и удалить напрямую:

find . -name "*.log" | xargs rm "*.log" | xargs rm

Это наиболее распространенный шаблон использования xargs. find формирует список путей, а xargs передает их все в rm.

Без xargs вам пришлось бы использовать цикл:

# Without xargs — more verbose
for f in $(find . -name "*.log"); do
  rm "$f"
done

xargs позволяет записать это в одну строку.

Флаг -I: размещение аргумента именно там, где нужно

По умолчанию xargs добавляет аргументы в конец команды. Но иногда аргумент нужно разместить в определенной позиции.

cat branches.txt | xargs -I {} git checkout {}

-I {} определяет заполнитель. Везде, где вы помещаете {} в команде, xargs подставляет текущий элемент. В качестве заполнителя можно использовать любую строку. {} — это просто соглашение.

Реальный пример: перемещение файлов в каталог резервных копий.

find . -name "*.bak" | xargs -I {} mv {} ./backup/{}"*.bak" | xargs -I {} mv {} ./backup/{}

Это перемещает каждый файл .bak в каталог ./backup/, сохраняя имя файла. Без -I выразить это было бы неудобно.

-n: обрабатывать по одному элементу за раз

По умолчанию xargs объединяет все аргументы в один вызов команды. Иногда требуется один вызов на каждый элемент.

cat servers.txt | xargs -n 1 ping -c 1

-n 1 указывает xargs передавать один аргумент на каждый вызов команды. Поэтому, если в servers.txt содержится пять серверов, команда ping -c 1 будет выполнена пять раз — по одному разу для каждого сервера.

-n 2 будет передавать по два аргумента за один вызов и так далее.

-P: Выполнять параллельно

Именно здесь xargs становится по-настоящему мощной.

cat urls.txt | xargs -n 1 -P 4 curl -O

-P 4 запускает до 4 процессов параллельно. В сочетании с -n 1 (один URL на вызов) это позволяет загружать все URL по четыре одновременно, а не последовательно.

Тот же шаблон подходит для любой медленной задачи, которую можно распараллелить: обработка файлов, запуск тестов, обращения к API:

find . -name "*.csv" | xargs -n 1 -P 8 python process.py

Восемь файлов обрабатываются одновременно. Без очередей задач, без дополнительных инструментов. Только xargs.

Шаблон -print0 и -0

В большинстве примеров с xargs есть одна неочевидная проблема. Имена файлов могут содержать пробелы. А поскольку по умолчанию xargs разбивает ввод по пробельным символам, файл с именем my report.csv будет воспринят как два аргумента: my и report.csv.

Решение:

find . -name "*.csv" -print0 | xargs -0 rm"*.csv" -print0 | xargs -0 rm

-print0 указывает find разделять результаты нулевым символом вместо символа новой строки. -0 указывает xargs разбивать ввод по нулевым символам вместо пробельных. Теперь имена файлов с пробелами обрабатываются корректно.

Возьмите это за привычку всякий раз, когда объединяете find и xargs. Это защитный подход, который ничего не стоит.

# Fragile -> breaks on filenames with spaces
find . -name "*.log" | xargs rm

# Robust -> handles all filenames correctly
find . -name "*.log" -print0 | xargs -0 rm

args как связующее звено между командами

Как только вы поймете xargs, вы начнете замечать её повсюду — не как команду, которую нужно искать в справочнике, а как связующее звено, которое делает остальные команды комбинируемыми.

Несколько шаблонов, которые стоит запомнить:

Поиск с помощью grep среди отфильтрованного набора файлов:

find . -name "*.java" -print0 | xargs -0 grep -l "NullPointerException""*.java" -print0 | xargs -0 grep -l "NullPointerException"

Найти все Java-файлы, содержащие NullPointerException. -l указывает grep выводить только имена файлов, а не совпавшие строки.

Перезапуск списка сервисов:

echo -e "nginx\npostgres\nredis" | xargs -n 1 systemctl restart-e "nginx\npostgres\nredis" | xargs -n 1 systemctl restart

Массовое переименование файлов:

find . -name "*.jpeg" -print0 | xargs -0 -I {} bash -c 'mv "$1" "${1%.jpeg}.jpg"' _ {}"*.jpeg" -print0 | xargs -0 -I {} bash -c 'mv "$1" "${1%.jpeg}.jpg"' _ {}

Немного сложнее → используется -I {} и вложенная оболочка для выполнения преобразования строки, но при этом массовое переименование выражается одной строкой.

Подсчет количества строк в нескольких файлах:

find . -name "*.log" -print0 | xargs -0 wc -l"*.log" -print0 | xargs -0 wc -l

xargs или цикл for

Частый вопрос: когда следует использовать xargs, а когда цикл for?

Я бы использовал xargs, когда:

  • у вас есть список элементов из stdin, и вы хотите передать их команде;
  • вам нужно параллельное выполнение с помощью -P;
  • вам нужны лаконичные, легко комбинируемые однострочники.

И использовал бы цикл for, когда:

  • для каждого элемента нужна условная логика;
  • важна обработка ошибок для каждого элемента;
  • операция достаточно сложная, и читаемость важнее краткости.

xargs оптимизирована для производительности и комбинируемости. Циклы for оптимизированы для управления.

xargs делает только одну вещь: она преобразует stdin в аргументы.

Всё остальное — -I для размещения, -n для пакетной обработки, -P для параллелизма, -0 для безопасности — лишь управляет тем, как происходит это преобразование.

Как только это станет понятно, вы перестанете копировать xargs из Stack Overflow и начнете использовать её естественным образом. Она станет тем самым связующим звеном, которого вам не хватало между командами, формирующими вывод, и командами, ожидающими аргументы.

Именно тогда терминал начинает ощущаться по-настоящему управляемым.

На этом все! Спасибо за внимание! Если статья была интересна, подпишитесь на телеграм-канал usr_bin, где будет еще больше полезной информации.