Пользователь
87,0
рейтинг
8 октября 2012 в 03:25

Администрирование → Функциональное программирование в шелле на примере xargs tutorial

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

Немного SEO-оптимизации: карринг, лямбда-функция, композиция функций, map, фильтрация списка, работа с множествами в шелле.

Пример



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

Это не реальная «задача», это учебный пример, решая который (в решении будет однострочник) я расскажу про очень необычный и мощный инструмент системного администрирования — линейное функциональное программирование. Линейное оно, потому что использование пайпа "|" это линейное программирование, а использование xargs позволяет превратить сложную программу с вложенными циклами в однострочник функционального вида. Целью статьи будет не показать «как найти размер библиотек» и не пересказать аргументы xargs, а объяснить дух решения, пояснить стоящую за ним философию.

Лирика


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

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

Даже словами видно, что второй вариант короче.

С учётом выразительных средств языков программирования конструкции получаются ёмкие и лишённые проблем обычных циклов — зацикливаний, неверного выхода из цикла и т.д.

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

Данные для решения


Для решения задачи нам нужно получить список запущенных процессов. Это проще, чем кажется — все процессы находятся в /proc/[0-9]+. Далее, нам нужны пути к бинарнику. И это тоже просто: /proc/PID/exe для всех процессов, кроме ядерных, указывает на путь к процессу. Следующая задача: нам нужен список библиотек для файла. Это делает команда ldd, которая ожидает путь к файлу и выводит (в хитром формате) список библиотек. Дальше вопрос техники и хождения по симлинкам — нам нужно пройти по симлинкам библиотек до самого упора, а потом посчитать размер каждого из файлов.

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

Заметим, в процессе список исполняемых файлов у нас будет использоваться два раза — один раз «сам по себе», второй раз — для получения списка библиотек каждого файла.

Императивное решение


(утрирую и опускаю детали)
get_exe_list(){
  for a in `ls /proc/*;
     do
         readlink -f $a/exe;
     done
}

get_lib(){
  for a in `cat `;
  do
       ldd $a
  done |awk '{print $3}'
}

calc(){
   sum=0
   sum=$(( $sum + `for a in $(cat);   do      du -b $a|awk '{print $1}';   done` ))
}

exe_list=`get_exe_list|sort -u`
lib_list=`for a in $exe_list; do get_lib $a;done|sort -u`
size=$(( calc_size $exe_list + calc_size $lib_list))
echo $size



Отвратительно, правда? Это, заметим, без регэкспов на правильную фильтрацию pid'ов (нам не нужно пытаться прочитать не существующий /proc/mdstat/exe) и без обработки многочисленных случаев ошибок.

Списки


Осознаем задачу. Поскольку у нас входные данные — это однородные файлы, то мы можем просто представить их как список, и обрабатывать так же. Капсом я буду писать «не написанные» участки кода.

Часть первая: Двойная обработка списка



Мы немного смухлюем, и используем stderr для дупликации списка.

(EXE_LIST |tee /dev/stderr|LIB_LIST) 2>&1 | CALC


Что этот код делает? Ненаписанная пока часть EXE_LIST генерирует список exe в системе. tee берёт этот список, пишет на stderr (поток №2) и пишет в stdout (поток №1). Поток №1 передаётся в LIB_LIST. Дальше мы объединяем вывод всех трёх команд (скобки) с stderr и выпихиваем в stdout единым списком и передаём в CALC.

Теперь нам надо сделать EXE_LIST

Часть вторая: фильтрация списков



(прим. чтобы видеть _все_ процессы в системе, нужно быть рутом).

Мы пойдём немного необычным путём, и вместо ls в цикле, используем find. В принципе, можно и ls, но там будет больше проблем с обработкой симлинков.

Итак: find /proc/ -maxdepth 2 -name "exe" -ls
maxdepth нам нужен, чтобы игнорировать треды, мы получаем вывод, аналогичный выводу ls. Нам надо его отфильтровать.

Итак, улучшаем EXE_LIST:

find /proc/ -name "exe" -ls 2>/dev/null|awk '{print $13}'

Наблюдение: мы будем использовать stderr для передачи данных, так что флуд find насчёт проблем с всякого рода ядерными тредами нам не нужен.

Часть третья: LIB_LIST



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

xargs -n 32 -P 4 ldd 2>/dev/null|grep "/"|awk '{print $3}'


Что тут интересного? Мы лимитируем ldd максимум 32 файлами за раз, и запускаем 4 очереди ldd в параллель (да, такой у нас доморощенный хадуп получается). Опция -P позволяет распараллелить исполнение ldd, что даст нам некоторый прирост скорости на многоядерных машинах и хороших дисках (пижонство это, в данном случае, но если мы выполняем что-то, более тормознутое, чем ldd, то параллелизм может быть спасением...).

Часть четвёртая: CALC



На входе файлы, на выходе надо отдать цифру суммарного размера всех файлов. Размер будем определять… Впрочем, стоп. Кто сказал, что симлинки указывают на файлы? Разумеется, симлинки указывают на симлинки, которые указывают на симлинки или на файлы. А те симлинки… Ну выпоняли.

Добавляем readlink. А он, зараза, хочет один параметр за раз. Зато есть опция -f, которая нам сэкономит кучу усилий — она покажет имя файла вне зависимости от того, симлинк это или просто файл.

|xargs -n 1 -P 16 readlink -f|sort -u

… так вот, размер будем определять с помощью du. Заметим, мы могли бы тут просто использовать опцию -С, которая просуммирует цифры и даст ответ, но в учебном курсе мы простых путей не ищем. Значит, без мухлежа.

|xargs -n 32 -P 4 du -b|awk '{sum+=$1}END{printf "%i\n", sum}'

Зачем нам sort -u? Дело в том, что у нас будет очень много повторов. Нам надо из списка выбрать уникальные значения, то есть превратить список во множество (set). Делается это наивным методом: мы сортируем список и говорим, выкинуть при сортировке повторяющиеся строки.

Однострочник, ужасающий



Выписываем всё вместе:

(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}'

(я не поставил тега сырца в этом ужасе, чтобы строка автоматически переносилась, чтобы не порвать вам ленты rss-ридеров, любите меня.)

Разумеется, ЭТО ничем не лучше того, что было приведено в начале. Страх и ужас, одним словом. Хотя, если вы гуру 98 левела и качаете 99ый, то такие однострочники могут быть обычным стилем работы…

Впрочем, назад, к штурму 10ого левела.

Приличный вид


Нам хочется порезать это. На читаемые и отлаживаемые куски кода. Вменяемые. Комментируемые.

Итак, вернёмся к начальной форме записи: (EXE_LIST |tee /dev/stderr|LIB_LIST) 2>&1 | CALC

Читаемо? Ну, наверное, да.

Осталось придумать, как правильно записать EXE_LIST

Вариант первый: с использованием функций:

EXE_LIST (){
    find /proc/ -name "exe" -ls 2>/dev/null|awk '{print $13}'
}
LIB_LIST (){
    xargs -n 32 -P 4 ldd 2>/dev/null|grep /|awk '{print $3}'
}
CALC (){
   sort -u|xargs -n 1 -P 16 readlink -f|xargs -n 32 -P 4 du -b|awk '{sum+=$1}END{print  sum}'
}
(EXE_LIST |tee /dev/stderr|LIB_LIST) 2>&1 | CALC


И чуть-чуть фунционального благородства:

EXE_LIST () ( find /proc/ -name "exe" -ls 2>/dev/null|awk '{print $13}' )
LIB_LIST ()  ( xargs -n 32 -P 4 ldd 2>/dev/null|grep / |awk '{print $3}' )
CALC()  ( sort -u|xargs -n 1 -P 16 readlink -f|xargs -n 32 -P 4 du -b|awk '{sum+=$1}END{print  sum}' )
(EXE_LIST |tee /dev/stderr|LIB_LIST) 2>&1 | CALC


Разумеется, пуристы ФЯПов скажут, что тут нет ни выведения типов, ни их контроля, нет ленивых вычислений, и вообще, считать это list processing в функциональном стиле — безумие и порнография.

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

Основное, что я хотел показать: функциональный подход к обработке последовательностей в шелле даёт более читаемый и менее громоздкий код, чем прямая итерация. И работает быстрее, кстати (за счёт параллелизма xargs).

scalabilty


Дальнейшим развитием «хадупа на шелле» является утилита gnu parallels, позволяющая выполнять код на нескольких серверах в параллель.
Георгий Шуклин @amarao
карма
268,0
рейтинг 87,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Администрирование

Комментарии (45)

  • +1
    да…
  • +1
    в целом, статья любопытна, но с трудом представляю где это может быть полезно. с «более читаемый» я бы поспорил + не стоит забывать об области применения тех или иных технологий
    • +6
      Я вообще большой противник всякого рода мета-программирования и прочей формы интеллектуального просирания ресурсов в никуда. И к использованию xargs я пришёл через длительный опыт разного рода скриптования, в т.ч. «на ходу». Я точно могу сказать, что xargs — одна из весьма полезных утилит, а списковый подход при обработке — это гигантская экономия кнопкодавления и уменьшение числа ошибок. Причём речь идёт не о написании программ, а именно о коньюктурной работе.

      Вот, например, как выглядит типовой однострочник для XCP:

      xe sr-list type=lvmoiscsi --minimal|xargs -d, -n 1 -I U xe pbd-list sr-uuid=U --minimal|xargs --verbose -d, -n 1 -P 16 -I U xe pbd-unplug uuid=U

      Взять все SR с типом lvmoiscsi, взять все pbd с sr-uuid из списка, для каждого (с параллельностью 16) сделать unplug.

      Сравнить с:

      for a `xe sr-list type=lvmoiscsi --minimal|tr, ' '`;do for b in `xe pbd-list sr-uuid=$a --minimal|tr, ' '`;do echo $b;xe pbd-unplug uuid=$b;done;done

      Заметим, второе — не паралелльное, жутко путанное и длиннее. Даже не столько по буквам, сколько по числу осмысленных конструкций, требующих внимания при написании.
      • 0
        Спасибо за ёмкий пример, его бы во введение. Не против я вашего подхода, да и проблем не вижу пока. Но на практике: в простых вещах извращаться противопоказано, а в сложных — будет уже другой/не shell.
        * вы так выгодно для себя вложенные циклы пишете в одну строку, что использование циклов в принципе не очень привлекательно. ИМХО, (по статье) то что можно одной строкой написать — вы пишете несколькими, а то что несколькими оформляется — пишете в одну
      • +1
        Если еще не успели, то присмотритесь к GNU Parallel, выполняет те же функции, что и xargs, но лишен многих детских болезней, дает больше контроля над процессом и обладает большим функционалом.
      • +3
        Вообще говоря, вся суть метапрограммирования и заключается в основном в экономии кнопкодавления, а так же ликвидации просирания ресурсов мозга на разбор бессмысленно навороченных конструкций. Если решение с метапрограммированием сложнее наивного — это бездарно примененное метапрограммирование :)
        • 0
          Дело в том, по какой метрике оценивается «экономия». Человеку, тащущему метапрограммирование, хочется нетривиальной задачи и потом элементарное добавление новых фич/запросов. Человеку, который фичи/запросы даёт, интересует время реализации оных.

          И получается, что «два месяца на метапрограммирование, потом фича за пол-часа» вместо «три дня унылого кодения на каждую фичу» для программиста — круто и хорошо, а для менеджера проекта — нет, потому что фич всего десять, а всё остальное за горизонтом планирования.
          • 0
            Мы, возможно, говорим о чём-то разном. В моем любимом Ruby переписывание нетривиального куска, повторенного 3-4 раза, с использованием метапрограммирования займет в итоге меньше строк и меньше времени, чем репликаиция куска еще столько же раз и вылавливание в нем какого-нибудь бага.

            (Это, конечно, если использовать его к месту, а не как в текущем проекте: замечательный файлик в двести строк, состоящий из запутанных конкатенаций, который генерирует эквивалент примерно десяти строк рукописного кода.)
            • 0
              Мы говорим о разном и о разном масштабе. То, что вы говорите, это мелочь и ерунда. Я ж в примере указал сроки, о которых речь (можно ещё умножить на несколько человек в команде).
              • 0
                Один раз — мелочь и ерунда, сто раз — фреймворк. Можно увидеть характерный пример кода?
                • 0
                  Нет, потому что это наша работа. Я-то говорю, взаимодействуя с программистами, де-факто, в режиме менеджера.
  • +1
    Единственное непонятно — нафига использовать stderr не по назначению, когда размножение строк можно было точно так же и через stdout сделать: tee /dev/stdout
    • 0
      Вас не затруднит привести пример, как stdout продублировать на два различных stdin? Давно уже ищу элегантное решение, но пока кроме tee /dev/stderr да волшебства в /dev/fd ничего не попадалось.
      • 0
        А, сорри, не разглядел сразу что последующее объединение происходит не сразу, а через вызов.

        Если придумаю что-то — напишу сюда :)
      • 0
        Привожу пример:
        echo "a" | xargs -I % sh -c 'echo "%/1"; echo "%/2"'
        Применительно к данной статье:
        echo "/usr/bin/gnote"| xargs -I % sh -c 'echo "%"; ldd "%"|cut -d" " -f3|grep "\S"'
        • 0
          Это правда не будет работать если в строках будут кавычки "".
          Но для данной задачи — сойдет. По крайней мере лучше чем хак через stderr
        • 0
          Занятно, спасибо, Но мне не подходит 8( Я ищу способ обрабатывать потоки в миллионы строк.
          • 0
            Именно однострочником надо?
            Потому что через именованные пайпы можно безо всякого оверхеда это делать, но там отдельными командами нужно создать/удалить пайп
            • 0
              mkfifo /tmp/fifo1; echo "a" | tee /tmp/fifo1 | (cat /tmp/fifo1 | (cmd1); cat - |(cmd2)); r m -f /tmp/fifo1

              Вместо cmd1 и cmd2 подставить соответственно команды для каждой ветки
            • 0
              У именованных пайпов есть очень большой недостаток — если команду вынесли, то пайпы останутся на диске. Соответственно написать хоть какую обёртку на этот случай уже не получется красиво — остаётся захламлённый уголок. В идеале я бы хотел получить некий способ взять поток данных и разделить его на N независимых потоков. Можно даже полное копирование и дальнейшую фильтрацию awkом.
              PS Кстати, обратную задачу я уже решил использовав github.com/vi/fdlinecombine/ 8)
              • 0
                Я думаю это решаемо (даже без именованных пайпов).
                Просто возможно будет громоздко.
                Чуть позже напишу решение.
                • +2
                  В bash это можно решить через Process Substitution:
                  $ echo test | tee >(cat) | cat
                  test
                  test
                  $
                  


                  Соответственно, пример из поста (на /proc не хватает прав):
                  #!/bin/bash
                  _list() {
                      find /usr/bin -type f -perm 755
                  }
                  _ldd() {
                      xargs -n32 ldd | awk '/=>  \(/ {next;} !/=>/ {print $1;next;} {print $3;}'
                  }
                  _calc() {
                      xargs -n1 readlink -f | sort -u | xargs stat -c '%s' | awk '{sum+=$1;} END {print sum;}'
                  }
                  
                  _list | tee >(_ldd) | _calc
                  
                  • 0
                    Круто.
                    Получается что >(cat) это и есть тот «именованный» пайп к команде внутри () автоматически удаляемый при завершении родительского процесса, о чем просили выше в комментах.
                    «Именованный», потому что у него есть имя (/dev/fd/XXX), которое можно передать в ком.строке, но физически файла нет.

                    Спасибо
                  • 0
                    А вот это да, круто. Ветвление pipe'ов.
              • 0
                В общем такое решение:
                echo "a b" | perl -e 'open($OUT[$_], "|$ARGV[$_]") or die "$!:$ARGV[$_]\n"for 0..$#ARGV; while (my $l = <STDIN>) { for (0..$#ARGV) { my $OUT = $OUT[$_]; print $OUT $l}} close($_) for @OUT' "cat -" "cat -"
                


                В перл передается поток через stdin, там открываются локальные пайпы для stdin каждого переданного аргумента (являющегося произвольной командой допустимой в шелле) и в каждый такой пайп выводится копия потока.
                А уже задача каждой команды как то обработать или просто вывести свою копию потока в stdout.

                В данном примере указаны команды «cat -» и «cat -», т.е. создаются 2 копии потока и просто выводятся в stdout.

                Естественно без отладки из головы каждый раз такую команду не наберешь, поэтому для практического применения вероятно нужно оформить в виде алиаса или отдельного скрипта (это легко сделать, т.к. тело команды не надо менять для разного числа аргументов).
    • 0
      Нельзя. Если на /dev/stdout вывести, то вывод уползёт в ldd, а не окажется «за» ldd (что делает вывод в /dev/stderr и 2>&1).
      • 0
        Да, был неправ
  • 0
    Вместо awk '{print $3}' можно использовать cut -f3 (не всегда — cut, например, не может использовать понятие «обобщенный пробел» в качестве разделителя). Для продвинутой обработки также весьма годится perl -e '' (скормить stdout программе) и, более интересный вариант, perl -ne '' (скормить stdout программе, которая перед этим обернется в цикл по строкам stdout)
    • 0
      Простите, парсер съел содержимое кавычек
      perl -e '[program]'
      и
      perl -e '[cycle_body]'
    • 0
      А есть ещё «perl -pne», который вдобавок выводит на печать построчно
    • 0
      cut -d\ -f3 (два пробела после \)
      И только если разделитель — одиночный пробел.
      Но если условия подходят — это короче и проще, чем awk.
      • 0
        cut -d " " -f 3 читается лучше. ещё лучше — через awk, сам понимаешь.
  • +3
    На мой вкус, ничего функционального в таком подходе нет: xargs — возможность вызывать команды, которые сами не умеют работать с аргументами с stdin'а. По хорошему, просто обёртка над циклом в духе «while read var; do $OMG_FUNCTION $var & done;».
    Я ничуть не умаляю достоинства xargs, штука удобная… Но вроде, это такой же императивный подход, как и обычно. И да, не используя xargs, я всё равно могу выделить те же куски конвеера и так же обернуть их в функции. (Хотя лаконичней от этого код не станет).
    Also, можно было бы использовать tag code и переносы строк ) И rss целы, и читатели довольны.
    • +1
      Ну и на ФЯЗ это было бы сделано рекурсивно, скорее всего. На хаскеле так точно. А тут цикл-циклом.
      Но фиг знает, я не великий знаток ФП ;)
  • +2
    Очень годно! Действительно шелл велик!

    Два замечания

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

    2) Размер файла лучше читать через stat, а не du.
    • 0
      Ну и в ту же коробку:
      for a in `ls /proc/*`;
      vs
      for a in /proc/*;
      • 0
        Не досмотрел, оно схематическое, и вообще не используется.
  • +1
    По поводу сырца в ужасе — а как же экранирование переводов строк для длинных команд? По-моему, удобно :)
  • 0
    Не нашёл манула обещанного.
    • 0
      Я же написал, что это не мануал. man xargs. Ключевые интересные опции: -d, -I, -n, -P, --verbose.
      • 0
        Нет, я про то, что обещан был не мануал, а манул Ж)
  • 0
    А можно пару слов про значения аргументов -n -P для xargs?
    C -n вроде понятно, а вот откуда такие -P и как они зависят от -n?
    • 0
      -P — указывает число потоков на выполнение.
      -n — максимальное число аргументов, передаваемых за раз. Много утилит понимает только один аргумент, т.е. -n 1.
  • 0
    xargs хорошая штука хотя бы потому, что передает допустимой длины argv[] и самостоятельно выполняет программу нужное количество раз.
    Мой любимый пример — сделать что-нибудь с миллионом файлов.
    Можно еще делать command $(generate-list-of-arguments), но легко натолкнуться на Argument list too long, сложнее решить проблемы с пробелами в именах файлов и хуже согласуется с естественным языком
  • 0
    Я думаю у вас не к месту использован термин «линейное программирование».
    Как-то так получилось что линейное программирование не связано никак с программированием.

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