MyTetra Share
Делитесь знаниями!
Как сконструировать нужную команду с помощью xargs. Понимание xargs.
Время создания: 18.07.2017 14:30
Автор: Xintrea
Текстовые метки: linux, xargs, bash, команда, конструирование, параметры, понимание
Раздел: Компьютер - Linux - Bash - Команды и скрипты
Запись: xintrea/mytetra_syncro/master/base/1500377408mqrgx9a7le/text.html на raw.github.com

В Linux существует очень странная команда xargs, которую весьма любят гуру, но не спешат объяснять как она работает. Интернет завален рецептами "как пользоваться xargs", но ни в одном из них внятно не написано самого главного: что эта команда вообще делает.



Самое главное


В общих чертах везде написано одно и то же: команда xargs принимает входной поток (именно поэтому ее всегда предваряет какая-нибудь команда и символ перенаправления потока "|"), и каким-то волшебным синтаксисом выполняет указанную в ней команду.


На самом деле команда xargs делает вот что. (Попробую сформулировать предельно беспристрастно). Она разбивает поток символов, направляемых в нее, на куски. Для разбиения потока она использует символы-разделители. И для каждого выделенного куска выполняется команда, которая указана в правой части команды xargs, дополненная справа символами найденного куска.


Да, в этом определении дважды используется понятие "право". Подробности разжовываются чуть ниже. А пока лучше посмотреть на структуру команды xargs в виде картинки. Синтаксически команда xargs состоит как бы из двух частей - левой и правой:





Причем однозначного визуального разделения, где левая, а где правая часть, просто нет. Если вы пытаетесь понять написанную другим человеком команду xargs, эту "границу раздела" нужно уметь находить самостоятельно. Вот несколько примеров:




Полная команда

Левая часть

(xargs и ее аргумены)

Правая часть

(команда)

Примечание

xargs rm -rf

xargs

rm -rf

xargs -0 rm -rf

xargs -0

rm -rf

xargs -p -l gzip

xargs -p -l

gzip

xargs tar -zcf pl.tar.gz

xargs

tar -zcf pl.tar.gz

xargs -n2 fmv

xargs -n2

fmv

xargs -I file mv

xargs -I file

mv file

Да, тут нет ошибки

xargs chown temp

xargs

chown temp

xargs kill -9

xargs

kill -9

xargs -p vim

xargs -p

vim



То есть, здесь действует правило: если после xargs идут символы, предваряемые знаком минус "-", значит это опции команды xargs. Как только пошли символы без знака минус, значит это уже символы правой части. Но нужно учитывать, что некоторые опции xargs требуют после себя еще каких-то данных, которые не будут предваряться знаком минус (см. пример с опцией -I).


А теперь самое главное: какую же команду выполняет xargs? Куда она пихает пачку символов, которую она вычленила во входном потоке? Все просто: она кладет эти символы справа от команды, прописанной в правой части. Понимаю, тут два раза используется понятие "право". Тогда вот картинка, которая все расставляет на свои места:





Возьмем конкретный пример. В каталоге лежат файлы:


main.cpp

main.h

version.cpp

version.h

config.cpp

config.h

data.cpp

data.h


Внутри этого каталога выполняется команда:


$ find . -name "*.cpp" | xargs -n 1 rm -rf


Какие команды сгенерирует xargs? Чтобы ответить на это, нужно понять, что будет подано на ее вход. А на вход будет подан результат работы команды find:


./main.cpp

./version.cpp

./config.cpp

./data.cpp


Команда xargs считает разделителем пробел, табуляцию или перевод строки (и их непрерывные последовательности). Таким образом, в итоге будут выполнены четыре команды:


rm -rf ./main.cpp

rm -rf ./version.cpp

rm -rf ./config.cpp

rm -rf ./data.cpp



Очень важное замечание про волшебную опцию -n


Есть одно очень важное замечание. Если вы его не осознаете, то не сможете нормально работать с xargs, и уподобитесь авторам статей, которые думают, что понимают как работает xargs, а на самом деле пишут лютую чушь. В вышеприведенном примере не просто так прописана опция "-n 1".


Опция "-n 1" заставляет xargs выполнять команду для каждого очередного куска из входного потока. Да, понимаю, что это звучит бредово: ведь команда xargs и так должна делать именно это! В конце концов, в мануале написано следующее: "xargs reads items from the standard input, delimited by blanks (which can be protected with double or single quotes or a backslash) or newlines, and executes the command (default is /bin/echo) one or more times with any initial-arguments followed by items read from standard input." Проблема в том, что по-умолчанию, если не указать "-n 1", xargs воспринимает весь входящий поток, разбитый пробелами, табами, и переносами строк, как ОДИН аргумент. И по-сути, весь входящий поток просто подставляется в выполняемую команду. Вот так сюрприз от разработчиков!


Вопрос: А как же тогда срабатывают примеры, приводимые в статьях, типа


$ find . -name "*.cpp" | xargs rm -rf

$ find . -name "*.cpp" | xargs wc -l


А срабатывают они просто потому, что сами команды rm, wc и им подобные умеют работать с набором имен файлов. А пользователи ошибочно думают, что это xargs несколько раз вызывает данные команды для каждого имени файла. И чтобы в этом убедиться, можно воспользоваться опцией -t (печать команды, генерируемой xargs, перед ее выполнением). Но чтобы увидеть результат, нужно еще использовать конструкцию перенаправления вывода из потока ошибок 2>&1 (потому что использование опции -t даёт вывод в поток ошибок, а не в стандартную консоль). И вот что можно увидеть.


Если писать команду xargs без опции "-n 1", то произойдет следующее:


$ find . -name "*.cpp" | xargs -t rm -rf 2>&1

rm -rf ./main.cpp ./version.cpp ./config.cpp ./data.cpp


Видно, что вызвалась только одна команда rm, и ей передан список имен файлов. Просто результат её работы будет выглядет так, как будто она вызывалась отдельно для каждого файла.


Если же воспользоваться опцией "-n 1", то картина будет другая:


$ find . -name "*.cpp" | xargs -n 1 -t rm -rf 2>&1

rm -rf ./main.cpp

rm -rf ./version.cpp

rm -rf ./config.cpp

rm -rf ./data.cpp


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


Есть еще один тонкий момент. В xargs есть ограничение на длину входного потока. И если входной поток слишком большой, xargs таки разобъет его на два или больше куска, и для каждого куска все-таки вызовет отдельную команду, указанную в правой части. Чтобы таких непредвиденных ситуаций не было, пользуйтесь опцией "-n 1".



Волшебная опция -L


Помимо опции -n, в xargs есть не менее волшебная опция -L. Аргумент у обеих опций - целое число. Важно понимать, что делает опция -L и чем отличается от -n.


Входящий поток можно рассматривать как поток строк, которые состоят из слов. То есть:


Первая строка

Это, очевидно, вторая


Грубо говоря, опция -n 1 разбивает входящий поток на слова, т. е. на последовательность символов, ограниченных пробелом, а так же табуляцией и переводом строки. Если количество стоит 1, то в одной части будет одно слово. Значит, вышеприведенный поток будет разбит на следующие части: "Первая", "строка", "Это,", "очевидно,", "вторая". Именно такие части будут обрабатываться.


Опция -L 1 разбивает входящий поток на заданное количество строк, а не слов. То есть, на последовательность символов, ограниченных переводом строк. Значит, с такой опцией вышеприведенный поток будет разбит на две части: "Первая строка", "Это, очевидно, вторая".


Как уже стало понятно, в xargs нельзя одновременно использовать опции -n и -L, так как они задают разные правила обработки входных данных, и их одновременное использование вызывает конфликт.



Команда xargs без аргументов


Иногда можно встретить обескураживающую конструкцию, типа:


tr -dc A-Za-z0-9_ < /dev/urandom | head -c 10 | xargs


Данная команда генерирует случайный пароль длиной 10 символов. Но что значит команда xargs без аргументов в конце этой команды?


Ответ прост. Команда xargs без аргументов на самом деле считает, что в ее правой части стоит команда /bin/echo. И пропускает входящий поток через команду echo. Зачем это нужно? В данном примере это нужно просто для того, чтобы итоговый результат завершался символом перевода строки. Вот пример, демонстрирующий разницу между командой, в которой нет xargs и есть xargs:


[user@host home]> tr -dc A-Za-z0-9_ < /dev/urandom | head -c 10

7jk2qx4cX8[user@host home]>


[user@host home]> tr -dc A-Za-z0-9_ < /dev/urandom | head -c 10 | xargs

zSlr2HsbSa

[user@host home]>



Пробелы в именах файлов


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


Обычно, имена файлов на вход программы xargs подаются из результата работы команды find. И для решения этой проблемы у команды find есть опция "-print0". Она заменяет перенос строки на нуль-символ \x0. А у команды xargs есть опция "-0" (минус ноль), с помощью которой входной поток разбивается на части, разделенные символом \x0.


Предположим, в директории появился файл с именем "new file.cpp". Если не пользоваться опциями преобразования перевода строк в нуль-символ, произойдет следующее:


$ find . -name "*.cpp" | xargs -n 1 -t rm -rf 2>&1

rm -rf ./new

rm -rf file.cpp

...


и, естественно, файл "new file.cpp" не будет удален. Если же добавить вышеприведенные опции, то команда сработает правильно:


$ find . -name "*.cpp" -print0 | xargs -n 1 -t -0 rm -rf 2>&1

rm -rf ./new file.cpp

...


и файл будет удален.


Важная тонкость: надо понимать, что через опцию -0 в команде xargs задается не просто замена символа перевода строки на \0x0. Это задается символ, который разбивает входной поток на "неделимые части". Они подставляться в выполняемую команду как неделимый аргумент, и поэтому пробел в имени файла является частью имени файла.



А что будет, если не писать опцию "-n" ?


Надо обратить внимание, что в вышеприведенных командах используется опция "-n 1". А что будет, если ее не писать? В принципе, все сработает точно так же. Но вот как это работает, мало кто сможет объяснить, ибо визуально команды будут одни и те же, а результат разный. Вот пример.


Команда без опции "-n 1" и без опций преобразования нуль-символа:


$ find . -name "*.cpp" | xargs -t rm -rf 2>&1

rm -rf ./main.cpp ./data.cpp ./config.cpp ./version.cpp ./new file.cpp


В результате сконструирована команда "rm...", и она не удалит файл "new file.cpp".


А теперь команда без опции "-n 1", но с опциями преобразования нуль-символа:


$ find . -name "*.cpp" -print0 | xargs -t -0 rm -rf 2>&1

rm -rf ./main.cpp ./data.cpp ./config.cpp ./version.cpp ./new file.cpp


В результате сконструирована команда "rm...", внешне абсолютно идентичная предыдущей, с точностью до последнего символа. Но она удалит файл "new file.cpp"!


Как это работает объяснить сложно. Ведь опцию "-0" имеет команда xargs, а не команда rm. В man-странице команды rm нет никаких указаний на то, что в случае разделения имен файлов нулевыми символами пробельные символы в именах файлов будут обрабатываться как литералы, а не как разделители. Для автора статьи такое поведение остается загадкой, и пока не нашлось специалиста, который бы объяснил, что же на самом деле происходит.



Решение для перебора имен файлов с пробелами с опцией -L


Казалось бы, зачем использовать пару -print 0/-0, и добавлять -n, когда можно просто воспользоваться опцией -L? Ведь мы знаем, что команда find пишет имена файлов по одному на строку. Даже если имя файла будет содержать пробелы, через -L будет выделена одна строка - то есть, имя файла с пробелами. А ведь это то что нужно!


Ну что же надо попробовать. Будет использоваться команда изменения прав на файл:


find . | xargs -L 1 chmod 755

chmod: невозможно получить доступ к './new': Нет такого файла или каталога

chmod: невозможно получить доступ к 'file.cpp': Нет такого файла или каталога


Как видно, не сработало. Почему? Потому что в данном случае была сформирована команда:


chmod 755 ./new file.cpp


То есть, в команду была просто подставлена строка с именем файла в котором есть пробел. Подстановка произошла формально, и команда chmod увидела два имени файла - "./new" и "file.cpp".


Как видно, решение с -L просто так не работает.



Самый главный вопрос: как конструировать команды?


Как же конструировать команды, в которых нужно не просто добавить справа найденные последовательности символов? А если нужно и после подставленного справа значения еще что-то дописать? Как быть?



Устаревший ответ


А вот никак! Вот такой ответ. С помощью xargs невозможно сконструировать произвольную команду. Можно сконструировать только команду, состоящую из базовой (фиксированой) части и правой (подстановочной) части. И всё!


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



Парам-парам-пам! Всё!


* * *


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


Все, что было написано выше, касается поведения xargs по-умолчанию: утилита xargs подставляет получаемые данные в правую часть формируемой команды, после которой уже ничего невозможно добавить. Но это поведение не единственное возможное, потому что в xargs есть плейсхолдеры.


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



-I replace-str


Replace occurrences of replace-str in the initial-arguments with names read from standard input. Also, unquoted blanks do not terminate input items; instead the separator is the newline character. Implies -x and -L 1.



Примерный перевод:



Замена вхождения replace-str в начальных аргументах именами, считанными из стандартного ввода. Кроме того, пробелы без кавычек не завершают элементы ввода; вместо этого разделителем является символ новой строки. Подразумеваются -x и -L 1.



Вот такая белиберда и больше никаких подробностей.


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



Конструирование команд через плейсхолдеры в xargs


Итак, сразу надо показать команду, которая способна без проблем работать с именами файлов, в которых есть пробелы:


find . | xargs -I {} chmod 755 "{}"


В этой команде через опцию -I {} задаются символы, которые далее в этой команде используются как плейсхолдер. В данном случае плейсхолдер будет выглядеть так: {}. Там, где встретятся символы {}, будет подставлен очередной кусок, полученный из стандартного ввода. В самой формируемой команде плейсхолдер можно писать несколько раз, если по каким-то причинам необходимо повторить значение в тексте команды.



Важно: Когда используется опция -I, подразумевается, что в команде есть "неявные" опции -x и -L 1.



Опция -x будет прерывать выполнение команды, если размер блока будет слишком велик (больше 128Kib по-умолчанию, но размер можно увеличить через опцию -s).


Что делает опция -L 1, было уже объяснено ранее: поток будет разбиваться именно по строкам. И эта особенность хорошо сочетается с командой find, ведь find выводит по одному имени файла на строку (даже если имя файла с пробелами).


Вся вышеуказанная команда с плейсхолдером работает потому, что имя файла (там, где находится плейсхолдер) заключено в кавычки. Поэтому даже если попадется имя файла с пробелами, оно будет правильно отработано.


Таким образом, используя плейсхолдер, можно формировать любую команду, за небольшим ограничением. Это единственное ограничение состоит в том, что входящий поток, при использовании плейсхолдеров, разбивается только в режиме -L 1. А иногда ведь нужен и режим -n 1. Но его использовать нельзя, так как (как было сказано выше) эти опции взаимоисключающие.


* * *


Так неужели в *NIX невозможно сконструировать абсолютно произвольно нужную команду? Конечно, возможно. Для этого можно использовать команду awk и ее функцию system(). Как это делать, написано в статье: Как сконструировать нужную команду из переданных аргументов с помощью awk.



Так же в этом разделе:
 
MyTetra Share v.0.67
Яндекс индекс цитирования