Xargs: многообразие вариантов использования

    xargs

    Об утилите xargs написано очень много — что можно написать еще? Но если, что называется, копнуть поглубже, то выясняется, что во многих публикациях излагаются лишь самые основы, но нет главного: не объясняется, как можно применять xargs в реальной практике. Статей с разбором сложных и нетривиальных вариантов применения этого весьма полезного для системного администратора инструмента, к сожалению, очень мало. Именно поэтому мы написали свою статью и постарались включить в нее как можно больше примеров использования xargs для решения различных проблем.

    Сначала мы рассмотрим принцип работы xargs и разберем примеры попроще, а затем перейдем к разбору сложных и интересных кейсов.


    Вспоминаем основы



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

    В общем виде синтаксис команды xargs можно представить так:

    [команда_генератор_списка] | xargs [опции_xargs] [команда]
    

    Рассмотрим, как все это работает, на материале простых и хрестоматийных примеров.

    Удаление файлов



    Одна из самых частых ситуаций, в которых используется xargs — удаление файлов, найденных при помощи команды find.

    Представим себе следующую ситуацию: имеется директория, в которой хранится большое количество файлов. Из нее нужно удалить файлы определенного типа (в нашем примере — файлы с расширением *.sh). Чтобы осуществить эту операцию, нужно передать xargs вывод команды find, и к файлам с указанным расширением будет применена команда -rm:

    $ ls
    one.sh one.py two.sh two.py
    
    $ find . -name "*.sh"| xargs rm -rf
    
    $ ls
    one.py two.py
    


    Отметим, что операцию удаления файлов можно осуществить и без xargs, а с помощью команды

    $ find . -name "*.sh" -exec rm -rf '{}' \
    


    Описанный способ не сработает, если в имени одного из удаляемых файлов содержится пробел. Имя, состоящее из двух слов, разделенных пробелом, не будет воспринято как единое целое.

    Проиллюстрируем это следующим примером:

    $ ls
    new file.sh one.sh one.py two.sh two.py
    
    $ find . -name "*.sh"| xargs rm -rf
    
    $ ls
    new file.sh one.py two.py
    


    Как видим, файл, в имени которого имеется пробел, не был удалён.

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

    $ find . -name "*.sh" -print0 | xargs -0 rm -rf
    


    Xargs может также помочь, например, быстро удалить все временные файлы, имеющие расширение tmp:

    $ find /tmp -name "*.tmp"| xargs rm
    


    Сжатие файлов



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

    $ ls | xargs -p -l gzip
    


    Рассмотрим еще один пример: сжатие с помощью tar всех файлов с расширением *.pl:

    $ find . -name "*.pl" | xargs tar -zcf pl.tar.gz
    


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



    С помощью xargs можно осуществлять массовое переименование файлов. Представим себе, что у нас есть группа файлов с расширением *.txt, и нам нужно заменить это расширение на *.sql. Это можно сделать при помощи xargs и потокового текстового редактора sed:

    $ ls | sed -e "p;s/.txt$/.sql/" | xargs -n2 fmv
    


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

    С помощью xargs можно также добавлять к дополнительные элементы к именам файлов (например, дату):

    $ ls | xargs -I FILE mv {} <...>-{}
    


    Вместо <..> можно подставить всё, что угодно.
    Фигурные скобки {} в этом примере означают «текущий аргумент» (т.е. текущее имя файла).

    Изменение прав для папок и файлов



    С помощью xargs можно также ускорить процесс смены прав на файлы и папки для определенного пользователя или группы. Предположим, нам нужно найти все папки пользователя root и заменить их владельца на temp. Эта операция осуществляется при помощи команды:

    $ find . -group root -print | xargs chown temp
    


    Чтобы найти все папки группы root и заменить группу на temp, используется команда:

    $ find . -group root -print | xargs chgrp temp
    


    Xargs и find: сложные операции



    С помощью команд find и xargs можно выполнять и более сложные операции. Вот так, например, можно удалить временные файлы, созданные более 7 дней назад:

    $ find /tmp -type f -name '*' -mtime +7 -print0 | xargs -0 rm -f
    


    А вот так — принудительно остановить процессы, которые уже работают больше 7 дней:

    $ find /proc -user myuser -maxdepth 1 -type d -mtime +7 -exec basename {} \; | xargs kill -9
    


    Xargs и сut



    Xargs довольно часто используется в сочетании с командой cut, позволяющей вырезать строки из текстовых файлов. Рассмотрим некоторые практические примеры. С помощью приведённой ниже команды на консоль будет выведен список всех пользователей системы:

    $ cut -d: -f1 < /etc/passwd | sort | xargs echo
    


    А команда вида

    file * | grep ASCII | cut -d":" -f1 | xargs -p vim
    

    будет последовательно открывать файлы для редактирования в vim.
    Обратим внимание на опцию -p. Благодаря ей команда будет выполняться в интерактивном режиме: перед открытием каждого файла будет запрашиваться подтверждение (y/n).

    В заключение приведём ещё один сложный и интересный пример — рекурсивный поиск файлов самого большого размера в некоторой директории:

    $ find . -type f -printf '%20s %p\n' | sort -n | cut -b22- | tr '\n' '\000' | xargs -0 ls -laSr
    


    Параллельный запуск процессов



    Xargs часто используется для параллельного запуска нескольких процессов. Вот так, например, можно одновременно cжать несколько директорий в tar.gz:

    $ echo dir1 dir2 dir3 | xargs -P 3 -I NAME tar czf NAME.tar.gz NAME 
    


    В приведенном примере используется ключ -P. Он указывает максимальное количество процессов, которые будут выполняться одновременно. Предположим, что у нас на входе имеется 10 аргументов. Если мы введём команду xargs с ключoм -P 3, то будет запущено 3 экземпляра команды, следующей после xargs, с каждым из этих аргументов.

    С помощью xargs можно также параллельно загружать из Интернета множество файлов:

    $ wget -nv <ссылка> | egrep -o "http://[^[:space:]]*.jpg" | xargs -P 10 -n 1 wget -nv 
    


    В приведенном примере с указанного адреса будут скачаны все графические файлы с расширением jpg; ключ -P указывает, что требуется скачивать по 10 файлов одновременно.

    Предварительные итоги



    Подведём предварительные итоги и сформулируем несколько правил работы с xargs.
    1. Xargs не работает с файлами, в имени которых присутствует пробел. Для решения этой проблемы с командой xargs используется опция −0. Пробел в имени файла можно обойти также следующим образом:
      $ xargs -I FILE my_command “FILE”
      

    2. Команда xargs принимает команды из со стандартного ввода, разделенные пробелом или переводом строки. Чтобы группировать эти команды, можно использовать двойные или одинарные кавычки. Можно также указать разделитель с помощью опции -d;
    3. Если команде xargs не передать вообще никаких аргументов, то по умолчанию будет выполнена команда /bin/echo;
    4. Во многих случаях команду xargs можно заменить циклом for. Например, команда
      $ find . -type f -and -iname "*.deb" | xargs -n 1 dpkg -I
      

      полностью эквивалента циклу
      $ for file in `find . -type f -and -iname "*.deb"`; do dpkg -I "$file"; done
      



    Нетривиальные примеры



    Основы мы вспомнили, типичные варианты использования рассмотрели… Перейдем теперь к более сложным и нетривиальным примерам. До некоторых из них мы додумались самостоятельно, работая над повседневными задачами, а некоторые — почерпнули с сайта http://www.commandlinefu.com (всем желающим научиться тонкостям работы с командной строкой очень рекомендуем время от времени его посещать — там порой можно найти очень полезные советы).

    Баним IP-адреса из списка



    Чтобы забанить IP-адреса из списка, нужно их добавить в IP tables c правилом DROP. Эта операция осуществляется при помощи команды:

    $ cat bad_ip_list | xargs -I IP iptables -A INPUT -s IP -j DROP
    

    Можно проделать и более сложную операцию и забанить все адреса по AS:

    $ /usr/bin/whois -H -h whois.ripe.net -T route -i origin AS<номер>|egrep "^route"|awk '{print $2}' |xargs -I NET iptables -A INPUT -s NET -j DROP 
    


    Изменяем формат URL



    Преобразовать URL вида «http%3A%2F%2Fwww.google.com» в «www,google.com» можно при помощи команды:

    echo "http%3A%2F%2Fwww.google.com" | sed -e's/%\([0-9A-F][0-9A-F]\)/\\\\\x\1/g' | xargs echo -e
    


    Генерируем пароль из 10 символов



    Сгенерировать надежный пароль можно при помощи команды вида:

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


    Генерировать пароли можно и без помощи xargs: для этого cуществует специализированная утилита pwgen. Некоторые другие способы генерации паролей описаны также здесь.

    Ищем бинарные файлы, установленные без использования dpkg



    Такая операция может потребоваться в случае, если, например, машина стала жертвой хакерской атаки и на ней было установлено вредоносное программное обеспечение. Выявить, что за программы поставили злоумышленники, поможет следующая команда (она ищет запущенные «бинарники», установленные без использования менеджера пакетов dpkg):

    $ сat /var/lib/dpkg/info/*.list > /tmp/listin ; ls /proc/*/exe |xargs -l readlink | grep -xvFf /tmp/listin; rm /tmp/listin
    


    Удаляем устаревшие пакеты ядра



    $ dpkg -l linux-* | awk '/^ii/{ print $2}' | grep -v -e `uname -r | cut -f1,2 -d"-"` | grep -e [0-9] | xargs sudo apt-get -y purge
    


    Проблема удаления старых ядер уже обсуждалась на Хабре — см. здесь (по этой же ссылке можно найти любопытные примеры команд).

    Преобразуем скрипт в строку



    Иногда возникает необходимость преобразовать большой скрипт в одну строку. Сделать это можно так:

    $ (sed 's/#.*//g'|sed '/^ *$/d'|tr '\n' ';'|xargs echo) < script.sh
    


    Заключение



    Как видно из проделанного обзора, возможности xargs гораздо шире, чем это может показаться на первый взгляд. Надеемся, что приведённые в статье примеры окажутся для вас полезными. Если вам известны другие интересные варианты использования xargs — добро пожаловать в комментарии. Читателей, которые по тем или иным причинам не могут оставлять комментарии здесь, приглашаем в наш блог.
    Selectel
    115,00
    ИТ-инфраструктура для бизнеса
    Поделиться публикацией

    Комментарии 44

      +4
      Перешёл с xargs на GNU Parallel — сильно быстрее во многих случаях, особенно на сложных операциях над файлами.
        +1
        > Как видим, файл, в имени которого имеется пробел, не был удалён.

        ~/Tmp$> touch "new file.sh" new_file.sh
        ~/Tmp$> find . -name "new*.sh"
        ./new file.sh
        ./new_file.sh
        ~/Tmp$> find . -name "new*.sh" -exec rm -f '{}' \;
        ~/Tmp$> find . -name "new*.sh"
        ~/Tmp$>
          0
          Эта строка относится к примеру с xargs, и дальше пояснение как этого избежать.
            +4
            А я понял, что к find:

              0
              А вы ниже прочитайте на абзац.
              Ну а так, то у find есть ключ -delete.
                +3
                И зачем тут тогда вообще нужен xargs? :)
                  +3
                  У автора спросите )) Думаю, что просто приведено как пример.
                    0
                    -delete есть в FreeBSD- и GNU-версии find, наверное где-то еще.
                    Но кроме GNU существует и масса других реализаций find, без -delete. В SunOS например этого нет.
                      0
                      В SunOS есть -exec (он, емнип, везде есть). А с ним через rm все прекрасно работает.
                        0
                        exec будет вызываться для каждого файла отдельно
                        а xargs может скормить группу аргументов тому же rm. это работает еще «прекраснее» :)
                0
                Я не знаю, где вы это прочитали, но УМВР.
                 $ touch "test test.sh" "test.sh"
                 $ ls -l
                total 0
                -rw------- 1 user group 0 Jan 21 01:16 test.sh
                -rw------- 1 user group 0 Jan 21 01:16 test test.sh
                 $ find . -name "t*.sh" -exec rm -vf '{}' \; 
                removed ‘./test test.sh’
                removed ‘./test.sh’
                  0
                  Картинка — скрин из статьи (см. мой коммент выше по ветке).
                    0
                    мистред
                +1
                вашу find конструкцию можно существенно ускорить заменив всего лишь один символ:
                find . -name "new*.sh" -exec rm -f '{}' \+
                
                оно отработает существенно быстрее, т.к. не будет на каждый файл отдельно вызывать rm. на паре-тройке файлов это незаметно, но когда счёт идёт на даже на сотни это уже видно невооруженным глазом. и \+ даже не гнутое расширение, а posix. и впилили его уже не один год назад. да (:
                  0
                  О, а вот за это — спасибо, не знал о такой штуке (да, я иногда невнимательно читаю длинные мануалы :().
                    0
                    А насколько быстро отработает эта команда на списке из 27 миллионов файлов? Точно ли, если использовать \+, rm не выдаст заветное «too long list of arguments»?
                    Имхо, с find лучше всего использовать ключ -delete, который просто возьмет и удалит все найденные файлы вне зависимости от их количества.
                      0
                      «too long list of arguments» вроде же оболочка выдаёт, до rm дело не доходит, а если бы дошло, то, он бы не знал о слишком длинном списке. Вообще xargs умный, если длина команды будет близка к максимальной, то команда вызовется, а накопленный список очистится. "+" не значит, что вызов будет один, он значит, что команда может принимать много аргументов и xargs должен стараться передать как можно больше за раз.
                        0
                        > до rm дело не доходит, а если бы дошло, то, он бы не знал о слишком длинном списке

                        У меня был случай, когда rm -rf dir/* ругнулось, а rm -rf dir — отработало.
                          0
                          Всё верно. dir/* раскрывает список файлов, поэтому оболочка ругается на огромный список. dir ничего не раскрывает, поэтому ругаться не на что.
                            0
                            А каков максимальный размер списка? Сходу ничего не нагуглилось.
                              0
                              «sysconf(_SC_ARG_MAX)» >= 4096. На современных Linux-based системах обычно 2 MiB.
                        +1
                        в случае с удалением пример немного специфический. вам просто повезло, что у find'a есть -delete. а если вам всё это счастье скопировать нужно? -copy у find'а нет, поэтому либо xargs, либо
                        find . -options -exec cp -t target {} \+
                        
                        0
                        А для cp нельзя прикрутить, чтобы найденные файлы в указанный каталог перекинуть одним вызовом?
                        + обязательно после {} должен идти?
                          0
                          Можно. Если почитать man cp, то можно наткнуться на любопытную опцию -t:
                                 -t, --target-directory=DIRECTORY
                                        copy all SOURCE arguments into DIRECTORY

                          Т.е. выглядит оно примерно так:
                          find . -type f -exec cp -t TARGET {} \+

                          Аналогичный трюк можно провернуть с mv.

                          upd. Что-то я сразу не заметил, ведь именно так и написано в моём предыдущем комментарии. Т.е. оно всё что найдёт и перекинет в указанную директорию. Ну или я вопрос неправильно понял.
                            0
                            Я буду читать все комментарии!
                            Спасибо :)
                      +3
                      Переименование файлов

                      $ ls | sed -e «p;s/.txt$/.sql/» | xargs -n2 fmv


                      А можно еще проще:
                      rename 's/\.txt$/.sql' *.txt
                      


                      Команда rename широко распространена, в debian/ubuntu стоит по умолчанию
                        –2
                        А можно еще проще:
                        mv *.txt *.sql
                        
                          +2
                          WAT??
                            0
                            Извините, я перепутал
                          +1
                          Замечу, что есть разные rename. В генте правильная rename это dev-perl/rename и вызывается как perl-rename
                          +7
                          У find есть ключик -delete, работает быстрее чем с использованием xargs и exec.
                          Смотреть:
                          Тут(GNU.org)
                            0
                            (find /proc/ -name exe -ls 2>/dev/null|awk '{print $13}'|tee /dev/stderr| xargs -n 32 -P 4 ldd 2>/dev/null|grep /|awk '{print $3}') 2>&1|sort -u|xargs -n 1 -P 16 readlink -f|xargs -n 32 -P 4 du -b|awk '{sum+=$1}END{print sum}'

                            Функциональное программирование в шелле на примере xargs: habrahabr.ru/post/153785/
                              +8
                              Некоторые примеры мягко говоря не совсем подходящие:
                              * find умеет делать -delete сам
                              * для упаковки файлов можно использовать обычный пайп: find. -type f | tar czf arc.tgz
                              * $ cut -d: -f1 < /etc/passwd | sort | xargs echo < — зачем этот echo? sort и так отлично возвращает вывод в stdout

                              Как-то так.
                              Сам я чаще всего использую exec внутри find, вместо xargs. В остальных случаях циклы покрывают 99% потребностей. Для работы с именами в которых есть пробелы проще всего экранировать «маркер». Например так:
                              bash-4.2# ls
                              a b c d
                              bash-4.2# touch "1 2 3 4"
                              bash-4.2# ls | xargs -I{} mv "{}" "{}".sh
                              bash-4.2# ls
                              1 2 3 4.sh  a b c d.sh
                              bash-4.2#
                              
                                +1
                                Всегда думал — «Как Тираны обходились такими маленькими ручками?»
                                Теперь я все понял! Спасибо :)

                                ЗЫ *nix советы тоже интересны и поучительны, но картинка… Это нечто.
                                  +2
                                  А давайте усугубим пример с find… | xargs rm. Для этого сначала почти бесправный пользователь bob делает:
                                  mkdir "~bob/user-controlled/space /etc"
                                  touch "~bob/user-controlled/space /etc/passwd"
                                  

                                  Потом всевластный root в скрипте по крону пишет:
                                  find ~bob -name "*" | xargs rm -f
                                  

                                  И в итоге некоторые файлы не то, чтобы совсем не удаляются, но удаляются не те, что нужно.

                                  Место для хождения по граблям в статье выделено совершенно правильно, да и аргументом xargs может быть не только rm.
                                    0
                                    Завтра не поленюсь попробую
                                    0
                                    # самое простое
                                    find -name ... -delete
                                    # удалять файлы в 4 потока по 8 штук за раз
                                    find -type f -name ... -print0 | xargs -0 -P$(nproc) -n8 rm
                                    

                                    Насчёт параллельного запуска да и самого xargs можно было и побольше примеров придумать:

                                    # нехитрый способ конвертировать все mp3 в ogg
                                    find -name '*.mp3' -print0 | xargs -0 -n1 -P$(nproc) sh -c 'ffmpeg -v quiet -y -i "$0" -vn -codec vorbis -aq 2 "${0%mp3}ogg"'
                                    # поиск несмердженных веток Git
                                    git branch -r --no-merged | grep -v HEAD | xargs -L1 git --no-pager log --pretty=tformat:'%Cgreen%d%Creset - %h by %an (%Cblue%ar%Creset)' -1
                                    # wget в 8 потоков (список ссылок для загрузки лежит в файле .list)
                                    < .list xargs -I{} -P8 sh -c 'echo {}; wget -q {}'
                                    # универсальный скрипт - функция _worker выполнится для каждого подкаталога в текущем каталоге (в несколько потоков, разумеется)
                                    _worker() {
                                        cd "$1"
                                        # ...что-то делаем
                                    }
                                    export -f _worker
                                    find . -maxdepth 1 -mindepth 1 -type d | xargs -P$(nproc) -I{} bash -c '_worker {}'
                                    

                                    find, xargs и nproc выручают меня постоянно, особенно когда речь идёт о серверах с кучей ядер. В статье как-то скудновато описано.
                                      +1
                                      мне как-то нужно было скачать первую страницу всех сайтов рунета.

                                      сначала через fping определи все домены которые резолвятся и сохранил их в отдельный файл, а потом

                                      cat unqiq.domains.txt | xargs -P 800 -I _URL_ ./crawler.sh _URL_
                                      


                                      где crawler.sh это просто wget c нужными настройками (таймаутами). За 6 часов все скачалось.
                                        0
                                        нужно было скачать первую страницу всех сайтов рунета

                                        Так вот как ты выглядишь, движок Яндекса.

                                        Пробовал я как-то запустить резолвер на несколько тыс. доменов в многопоточном режиме — роутер повесился намертво.
                                      0
                                      Я может чего не понял, xargs иногда путает, но вот это:

                                      «С помощью xargs можно также добавлять к дополнительные элементы к именам файлов (например, дату):

                                      $ ls | xargs -I FILE mv {} <… >-{}»

                                      — не будет работать. Тогда уж так:
                                      $ ls | xargs -I FILE mv FILE <… >-FILE

                                      Ну и в других местах…
                                        +1
                                        какую задачу выполняет xargs в комманде?

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


                                        PS: Ждем след статью на тему «как использовать grep и echo»
                                        tr -dc A-Za-z0-9_ < /dev/urandom | head -c 10 | grep ''
                                        tr -dc A-Za-z0-9_ < /dev/urandom | head -c 10 && echo
                                        


                                          0
                                          По iptables — не стоит применять длинные линейные списки там, где можно применить ipset.
                                            +1
                                            Не смотря на то, что возможности действительно широкие, думаю стоит в тексте написать предостережение от использования xargs для работы с файлами.

                                            Например, вот что greybot на фринодовском канале #bash нам сообщает:

                                            !xargs
                                            xargs(1) is dangerous (broken, exploitable, etc.) when reading non-NUL-delimited input. If you're working with filenames, use find's -exec [command] { } + instead. If you can get NUL-delimited output, use xargs -0. Otherwise, you probably want a while read or for loop instead.

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

                                            Самое читаемое