пятница, 29 января 2010 г.

Просто о xargs

Долгими зимними вечерами я сидел и думал "вот придёт светлое время, я сяду и как следует разберусь с этой загадочной утилиткой xarg". Ну вот время похоже и пришло - я сел разбираться. Первое, что мне бросилось в глаза, это то, что man к ней довольно загадочный, и с первого раза не просветляет. Статья на википедии понимания тоже не добавила, а скорее даже запутала, поэтому я решил провести своё собственное расследование, и написать по этому поводу небольшой мануальчик. Как известно, пока объясняешь и сам поймёшь :)

Итак, xargs.

xargs это такая утилита командной строки, позволяющая вызвать любую команду с аргументами, взятыми из стандартного входа. Причём аргументы можно передать все сразу, а можно группировать по несколько штук. Изучать мы будем xargs версии 4.4.0, при чём по рекомендации man-а будем использовать только новые аргументы, не помеченные как deprecated(лучше сразу привыкать работать правильно).

Итак первое, что стоит понять, это то, как xargs обрабатывает входящий поток и делит его на аргументы. Есть несколько режимов, зависящих от опций:

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

2. Обычный, с группировкой. Режим, включающийся параметром -L. Практически идентичен предыдущему, за исключением того, что xargs запоминает, какой аргумент на какой строке находится. Более того, если строка оканчивается пробелом или табуляцией, следующая строка считается продолжением текущей.

3. По строкам. Включается при использовании опции -I или -0. При этом вся строка считается одним целым аргументом, несмотря на пробелы и табуляции внутри. Для -I концом строки является символ '\n' а для -0 символ '\0'

Проведём пару испытаний, что бы лучше понять всё это. Создадим файл test с следующим содержимым(== в файл заносить не надо):
==
arg1
arg2 space
"arg3 quoted"
arg4\ escaped
arg5 with
continue
==
(После 'arg5 with' должен быть пробел)
А так-же напишем небольшой скрипт tp, который будет выводить свои аргументы разделяя их символом ':' и количество:
==
#!/bin/bash
echo -n "@$#"
while [[ $1 != "" ]]; do echo -n ":$1"; shift; done
echo
==

Обычный режим(выделение аргументов по пробельным символам):
x $ cat test | xargs ./tp
@8:arg1:arg2:space:arg3 quoted:arg4 escaped:arg5:with:continue
Файл был разбит на аргументы по пробельным символам, но строки взятые в кавычки и экранированные символом '\' остались целыми.

Обычный режим с группировкой по строкам не отличается от предыдущего на этом этапе.

Разбиение по строкам. Создадим второй тестовый файл следующей командой:
x $ cp test testz && printf "\0arg6" >> testz
Проверим
x $ cat testz | xargs -0 ./tp
@2:arg1
arg2 space
"arg3 quoted"
arg4\ escaped
arg5 with
continue
:arg6

Как можно видеть аргумента всего 2. Первый длинный, сохранивший переводы строк, кавычки и \, а второй arg6. В файле они разделены нулевым символом.

По поводу разделения параметров можно ещё сказать о опции -d, которая указывает новый разделитель. Например попробуем использовать '3' как разделитель.
x $ cat test | xargs -d 3 ./tp
@2:arg1
arg2 space
"arg: quoted"
arg4\ escaped
arg5 with
continue
Произошло разделение файла на 2 части на месте символа '3'. Что примечательно, таким образом можно эмулировать опцию -0
x $ cat testz | xargs -d "\x00" ./tp
@2:arg1
arg2 space
"arg3 quoted"
arg4\ escaped
arg5 with
continue
:arg6

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

Итак, xarg после своих опций ждёт команду, которую и будет выполнять. Все входящие аргументы разбиваются на группы, после чего заданная команда вызывается для каждой группы, и в неё передаются все аргументы из этой группы.

Теперь рассмотрим, как формируются группы.

1. Если опций нет, то группа одна, в неё попадают все аргументы из потока ввода. Группа бесконечного размера, так сказать :)

2. Опция -L n задаёт группировку по строкам. В команду передаются аргументы находящиеся на n строках. Продемонстрирую на примерах.
Группировка по 1 строке:
x $ cat test | xargs -L 1 ./tp
@1:arg1
@2:arg2:space
@1:arg3 quoted
@1:arg4 escaped
@3:arg5:with:continue
Видно, что вторая строка содержит 2 аргумента, потому как они оба на одной строке. А а последняя вообще 3, так как предпоследняя строка "удлинняется" за счёт пробела в конце.

Теперь группировка по 2 строки. В команду попадают строки 1 и 2; 3 и 4; и сиротка 5-ая:
x $ cat test | xargs -L 2 ./tp
@3:arg1:arg2:space
@2:arg3 quoted:arg4 escaped
@3:arg5:with:continue

3. Группировка по аргументам, задаваемая опцией -n x. Тут всё прозрачно: аргументы группируются по x штук и передаются в команду.
По одному аргументу:
x $ cat test | xargs -n 1 ./tp
@1:arg1
@1:arg2
@1:space
@1:arg3 quoted
@1:arg4 escaped
@1:arg5
@1:with
@1:continue
По 2 аргумента:
x $ cat test | xargs -n 2 ./tp
@2:arg1:arg2
@2:space:arg3 quoted
@2:arg4 escaped:arg5
@2:with:continue

3. Режим с подстановкой - опция -I. Для начала надо напомнить, что в данном режиме аргументы из потока ввода разбираются по особому. Каждая строка это один целый аргумент, склеивание строк не производится. Во вторых, у опции -I имеется параметр - строка, которая заменяется в команде на аргумент:
x $ echo -e "A B\nC D" | xargs -I _ ./tp =_+_=
@1:=A B+A B=
@1:=C D+C D=
Легко заметить, что символ _ задан как строка подстановки аргумента, которая используется в команде 2 раза. Так же видно, что аргументы выделяются целыми строками, и пробел не влияет на разбор. Команда вызывается для каждого аргумента.

С подстановкой всё. Рассмотрим оставшиеся важные опции
-r - не выполнять команду, если нет аргументов:
x $ cat /dev/null | xargs ./tp
@0
x $ cat /dev/null | xargs -r ./tp
x $
Как видим, во втором случае команда не выполнилась.

-p - xargs будет запрашивать подтверждение на выполнение каждой команды.

На этом небольшой мануал завершён. Он оказался не совсем кратеньким, зато надеюсь понятненьким ;)

12 комментариев:

Анонимный комментирует...

Ну не скажу что объяснение простое и логичное, но все равно большое спасибо, было интересно.

vlak комментирует...

а по-моему все супер понятно, огромное спасибо :-)

Анонимный комментирует...

спасибо, не супер написано. но посравнению с вики или man хargs просто божественно.
80% юзеров интересует только 20% возможностей проги

кстати былоб очень полезно (вместо подробного описания группировки) описать совместную работу хargs и sed, egrep или awk. Так как типичное использывание хargs именно что то вроде получить список из нескольких столбцев, выделить из столбца нужную инфу, передать ее с помощью хargs нужной проге.

спасибо

Анонимный комментирует...

Да уж… Тут как люди раньше говорили: Азбуку учат — во всю избу кричат :)

Отличный сайт! Все хорошо сделано.

Анонимный комментирует...

1. Если опций нет, то группа одна, в неё попадают все аргументы из потока ввода. Группа бесконечного размера, так сказать :)

На самом деле группа не бесконечного размера. Есть параметры --max-сhars=max-chars или -s max-chars, которые задают максимальный размер командной строки (вызываемая команда + все передаваемые аргументы + всякие пробелы и терминальные символы).

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

Классический пример косяка, который в данном случае обходит xargs - выполнение ls * в директории с парой сотен тысяч файлов с длинными именами. * раскрывается башем и он не может запустить ls с таким большим количеством параметров (и такой общей длинной).

Владимир Попов комментирует...

Отличная работа. Спасибо!

Виктор комментирует...

Все было доходчиво, спасибо! :)

Dexel комментирует...

Спасибо Вам огромное. Чётко разжОвано.
Мэрси!

Dexel комментирует...

Спасибо Вам огромное. Чётко разжОвано.
Мэрси!

Unknown комментирует...

весь инет облазил, а ответа на свой вопрос так и не нашел.

есть контроллер, которому передается список команд из файла (РНР-скрипты).

xargs -t -n 1 -P 3 -L 1 /usr/bin/php < _command.txt;

нужно сделать чтобы вывод КАЖДОЙ команды направлялся в СВОЙ лог.

сделать в один общий - нет проблем.

если в файле команд написать что-то типа
aaa.php >> aaa.log
то ругается - нет такого файла - "aaa.php >> aaa.log"

можно конечно из РНР выводить не в STDOUT, а писать в свой собственный лог-файл, но тогда ошибки (и такое случается) пишутся куда-то в общий ЛОГ и понять кто ее сгенерил не всегда тривиальная задача, тем более если она возникает достаточно редко при каком-то стечении обстоятельств.

Unknown комментирует...

есть, конечно, достаточно банальное решение - в файле команд прописать вызов SH - для каждого РНР свой, а уже в нем прописать вывод в свой лог.
но это будет увеличение числа файлов в 2 раза - а оно нужно? :-)

Unknown комментирует...

получается что-то типа этого

xargs -t -n 1 -P 3 -L 1 -I{} sh -c "/usr/bin/php '{}' >> 'logs/`date +%Y-%m-%d`_{}.log'" < _parallel_command.txt >> "logs/`date +%Y-%m-%d`_parallel.log" 2>&1 ;


пишет ЛОГи каждого скрипта в свой файл.

правда не совсем туда куда хотелось бы - а в один каталог кучей.
и с не такими именами - к именам логов просто добавляются какие-то префиксы и суффиксы.