Как перестать беспокоиться и полюбить командную строку, часть 2

This article is protected by the Open Publication License, V1.0 or later. Copyright © 2005 by Red Hat, Inc.
Original article: http://www.redhat.com/magazine/005mar05/features/perl/

Перевод: © Иван Песин

Введение

В первой части этой статьи мы рассмотрели некоторые мощные подходы и команды, позволяющие нестандартно использовать командную строку с целью быстрой манипуляции и извлечения необходимой нам информации. Это было неплохим началом и показало, что с помощью относительно простых утилит можно получить множество ценной информации. Однако это была всего лишь закуска перед нашим главным блюдом — Perl.

Perl называют армейским ножом в множестве контекстов, включая системное администрирование, и это вполне соответствует истине. В целом, Perl чрезвычайно разносторонний язык, который, в отличие от многих других языков, рос и развивался больше для удовлетворения практических потребностей, нежели академического интереса. В частности, он изначально предназначался системным администраторам, как более гибкая и мощная замена утилит Sed и Awk. Такой вариант развития имеет свои плюсы и минусы. С одной стороны, вы получаете очень мощный и гибкий язык, с другой — он иногда пытается удовлетворить потребности слишком широкой аудитории и ему не достает строгости более традиционных языков.

Но именно эта гибкость, которую мы собираемся здесь использовать, делает Perl отличным выбором. Более того, используемые в статье концепции, пригодны и к целому классу других языков. И хотя скорее всего вы не станете использовать Java в хитростях командной строки (удачи вам, если вы решите попробовать), некоторые другие языки очень хорошо для этого подходят. Например, Ruby, относительно молодой язык, который даже подражает некоторым ключам командной строки Perl. Но в этой статье мы сконцентрируемся на Perl.

Подготовка

В заключении предыдущей части, мы продемонстрировали вызов Perl из командной строки, но не объяснили его. Вот эта команда:

find /tmp/library -print0 | xargs -0 perl -p -i -e 's/XFree86/x.org/g'

Команды find и xargs уже нам известны, как и ключ -e в вызове perl. Мы оставили без объяснений ключи -p и -i. Давайте начнем с -p. Его присутствие в командной строке говорит Perl что он должен выполнить заданные команды (будь они в скрипте, или, что чаще используется, в выражении, заданном после ключа -e) для каждой строки ввода, после чего вывести переменную $_. Давайте рассмотрим более простой пример:

seq 1 10 | perl -p -e '$_ = "prefix: $_"'

Команда seq просто выводит в стандартный вывод последовательность чисел от 1 до 10. Эти данные направляются на ввод к Perl, который начинает считывать по одной строке и выполнять над полученными данными команды, заданные после ключа -e. В нашем примере, мы переопределяем значение переменной $_ новым значением "prefix: $_" — иначе говоря, вставляем слово "prefix: " перед старым значением. В результате мы получим:

prefix: 1
prefix: 2
prefix: 3
...
prefix: 10

Все просто, но важно понимать механизм работы, потому давайте разберем еще один пример, возможно, немного более практичный. Нужно считать файл /etc/passwd и удалить все данные в строке, начиная с первого символа "двоеточие":

perl -p -e 's/:.*//' /etc/passwd

Указанная команда выведет имена всех пользователей вашей системы. Здесь мы применили в качестве команды подстановку по регулярному выражению — 's/:.*//'. В Perl, переменная $_ является магической. Ее используют многие операторы и функции, если не указано что-либо иное. В этом случае, при использовании ключа -p, она содержит текущую строку ввода.

При использовании отдельно, ключ -p удобен для преобразования потоков данных. В комбинации же с ключом -i, мы получим удивительно мощную команду, которая может занять достойное место в любом арсенале инструментов. Ключ -i указывает на необходимость выполнения изменений непосредственно в источнике данных. Таким образом, вместо обычного вывода измененных ключом -p данных в стандартный вывод, они непосредственно записываются в исходный файл. Этот механизм является основой большого количества особенно мощных приемов.

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

find /tmp/library -print0 | xargs -0 perl -p -i -e 's/XFree86/x.org/g'

Мы знаем, что комбинация find/xargs добавляет каждый найденный файл к команде perl -p -i -e 's/XFree86/x.org/g'. Мы также знаем, что ключи -p и -i задают обработку каждой входящей строки и ее замену на вычисленный результат. Выражение, в данном примере, заменяет строку XFree86 на x.org. Конечный же результат всего примера — замена во всех файлах каталога /tmp/library, и его подкаталогов, строки XFree86 на x.org.

У ключа -p есть брат, который выполняет те же функции, за исключением вывода переменной после выполнения. Это ключ -n. И хотя есть задачи, при решении которых применение его одного оправдано, использование в комбинации с ключами, которые мы вскоре изучим, сильно увеличивает его эффективность. Кроме того, его можно использовать и с уже знакомыми нам ключами:

perl -n -e '@fields = split /:/; print "$fields[0]\n"' /etc/passwd

Эта команда, как и другая, рассмотренная нами ранее, выводит список всех пользователей из файла /etc/passwd. Если задуматься, то вы вспомните, что при работе с Linux часто встречаются файлы, в частности /etc/passwd, поля которых разделены каким-либо символом-разделителем, в нашем случае — двоеточием. Причем встречаются настолько часто, что в Perl встроены средства автоматизации их обработки. Рассмотрим простейший случай: файл с полями, разделенными пробелом. Одним из таких файлов является журнал веб-сервера Apache — access_log:

172.31.29.101 - - [04/Jan/2005:21:56:44 -0500] "GET /favicon.ico HTTP/1.1" 404 291 "-" "Mozilla/5.0"

Здесь мы видим много информации. Иногда нам нужна лишь некоторая ее часть. Положим мы хотим получить список запрошенных URL. Посчитав поля обнаруживаем, что седьмое поле содержит интересующие нас данные. Соединив в командной строке ключ -n с ключом -a, который говорит Perl автоматически разбивать строку на поля, мы получим массив полей. Теперь в выражении можно обращаться не только к целой строке ввода, посредством переменной $_, но и к отдельным полям, с помощью массива @F (неинтуитивно, я знаю). В нашем случае, массив @F будет содержать:

$F[0] = '172.31.29.101'
$F[1] = '-'
$F[2] = '-'
$F[3] = '[04/Jan/2005:21:56:44'
$F[4] = '-0500]'
$F[5] = '"GET'
$F[6] = '/favicon.ico'
$F[7] = 'HTTP/1.1"'
$F[8] = '404'
$F[9] = '291'
$F[10] = '"-"'
$F[11] = '"Mozilla/5.0"'

Таким образом, чтобы получить список всех URL, которые были запрошены, выполните:

perl -l -a -n -e 'print $F[6]' /var/log/httpd/access_log

Ключи -l и -e нам уже известны. Нового здесь только комбинация ключей -a и -n.

Практическая магия

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

Проблема

Какие страницы моего веб-сервера самые популярные?

Решение
cat /var/log/httpd/access_log | perl -l -a -n -e 'print $F[6]' | sort | uniq -c | sort -n | tail -10
Пояснение

Итак, это развитие предыдущего примера; здесь мы добавили последовательную обработку результатов командами sort, uniq и tail. Первая команда sort просто готовит данные для uniq (uniq ожидает на вводе уже отсортированные данные). Однако, сейчас мы просим команду uniq, не просто убрать дубликаты строк, но и посчитать их количество, для чего вызываем ее с ключом -c. В этом случае формат вывода команды uniq следующий:

18 /robots.txt
37 /favicon.ico

Первое число — это количество повторов, далее следует непосредственно строка. Такой вывод мы опять отправляем команде sort, но на этот раз, сортируем в числовом порядке. Наконец, с помощью tail мы выводим последние десять строк отсортированного списка (что, в нашем случае, отвечает десяти наиболее популярным страницам).

Проблема

Кто атакует мой веб-сервер?

Решение
cat /var/log/httpd/access_log | perl -l -a -n -e 'print $F[0]' | sort | uniq -c | sort -n | tail -10
Пояснение

Этот пример совпадает один в один с предыдущим, за исключением того, что мы анализируем нулевое поле, которое содержит IP-адрес клиента.


Проблема

Вам нужно внести изменения в большое количество файлов.

Решение
find -type f -name '*.txt' | xargs perl -p -i -e 's/PLACEHOLDER/new_value/g'
Пояснение

Основная идея этого примера заключается в использовании утилиты find для создания списка файлов и xargs для передачи его к Perl. Это просто развитие наших предыдущих примеров с использованием xargs, но теперь мы комбинируем ее с Perl, чтобы внести много изменений, гораздо больше чем может поместиться в одной командной строке. Другим способом решения этой задачи может быть запуск вашего любимого редактора (конечно, Emacs), вероятно создание макроса и выполнение его над каждым файлом... времени, наверно, на это у вас не будет, если требуется откорректировать сотни файлов, не говоря уже о том, что вероятность ошибок в этом случае гораздо выше.


Проблема

Вы подозреваете, что в файле /etc/passwd нескольким пользователям принадлежит один и тот же uid.

Решение
perl -F: -l -a -n -e 'print $F[2]' /etc/passwd | sort | uniq -d
Пояснение

В этом примере мы применили новый параметр "-F:". Он переопределяет символ-разделитель полей на двоеточие (вообще говоря, на любую строку или регулярное выражение, следующее непосредственно за ключом -F). В файле /etc/passwd пользовательский идентификатор хранится в третьем поле (т.е. $F[2]. Помните, инженеры начинают считать с нуля!). После этого мы выполняем sort, чтобы приготовить данные для команды uniq, которую вызываем с ключом -d (чтобы вывести только дублирующиеся данные; по-умолчанию, uniq работает наоборот: выводит только уникальные строки).


Проблема

Вы хотите узнать, какой объем данных был загружен с вашего веб-сервера за сегодня.

Решение
cat /var/log/httpd/access_log | perl -l -a -n -e '$n += $F[9]; } END { print $n'
Пояснение

Становится все интереснее. Все примененные ключи нам известны, но само выражение явно необычно. Честно говоря, это выражение практически нечитаемо, пока вы не знаете как оно работает. А работает оно, весьма чудно. Ключ -n говорит Perl, что нужно выполнить для вас следующее:

while (<>) {
# разделение строки $_ в массив @F

# код, указанный после ключа -e,
# вставляется сюда
}

Другими словами, Perl просто помещает код, указанный после ключа -e в цикл while. Давайте попробуем для нашего примера выполнить постановку вручную:

while (<>) {
# разделение строки $_ в массив @F

$n += $F[9]; } END { print $n
}

Теперь отформатируем и получим ...

while (<>) {
# разделение строки $_ в массив @F

$n += $F[9];
}

END {
print $n
}

Ага, теперь это становится понятнее. В Perl, блок END { ... } исполняется при завершении работы интерпретатора. Таким образом, для каждой строки ввода, Perl прибавляет значение десятого поля к переменной $n. При завершении работы интерпретатора, выводится значение данной переменной.

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

Проблема

В вашем журнальном файле дата и время указаны в формате epoch, но ваш мозг не умеет преобразовывать его. Ниже приведен пример формата Unix epoch:

1104903117 0.3
Решение
cat /tmp/weirdlog | perl -l -a -n -e 'print scalar(localtime $F[0]), " @F[1..$#F]"'
Пояснение

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

Первая конструкция превращает $F[0], где содержится число, например 1104903117, в формат, доступный для чтения человеком. Это выполняется функцией localtime. localtime возвращает либо массив значений, содержащий различные части даты, такие как месяц и год, либо строку представляющую дату в текстовом виде. Мы задаем текстовый формат вывода данных с помощью функции scalar.

Следующая конструкция — это " @F[1..$#F]". Данная конструкция эквивалентна "@F" за тем лишь исключением, что мы получаем список элементов с 1 по $#F. $#F на языке Perl означает: последний допустимый индекс массива @F. А вот другой способ выполнения той же задачи:

cat /tmp/weirdlog | perl -l -a -n -e '$F[0] = scalar localtime $F[0]; print "@F"'

Все та же идея, но мы изменяем массив @F в теле программы, после чего выводим его. Важно заметить, что мы не используем ключ -i и потому файл /tmp/weirdlog изменен не будет (хотя в этом конкретном случае, использование cat не позволит внести изменения в файл; даже при использовании ключа -i, Perl может изменять файлы только если они были указаны непосредственно в команде вызова интерпретатора, а не переданы на стандартный ввод).

Заключение

Надеюсь, вы почувствовали вкус к магическим приемам, которые можно выполнять при находчивом использовании Perl и bash. Данные статьи знакомят вас с набором строительных блоков, которые вы можете творчески использовать при решении собственных задач. В следующий раз, когда вы будете редактировать файл, внося большое количество одинаковых исправлений, или комбинировать сложным образом grepsed и другие утилиты командной строки, задумайтесь над тем, как можно решить данную проблему с привлечением Perl.

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

Одним из достоинств использования таких инструментов, как Perl это то, что все его возможности можно использовать как в больших, более формальных, скриптах, так и в командной строке. Таким образом, вы удваиваете использование ваших знаний, что очень хорошо. Так, например, идиома 'scalar localtime' из нашего последнего примера, весьма часто используется в скриптах Perl, но легко адаптируется для использования в командной строке.

Один из лучших способов изучения магии командной строки — это изучение самого Perl. Существует множество ресурсов, посвященных изучению Perl, но если вы знакомы с его основами, тогда обратитесь к Perl Cookbook — великолепному источнику информации о приемах, аналогичных продемонстрированным в этой статье. Естественно, страницы руководства Perl (отменного качества) предлагают множество полезной информации. В нашем случае, особенно полезным будет man perlrun, где описываются различные ключи командной строки, в том числе и те, которые мы применяли в этой статье.

Об авторе

Чип Тернер работает в Red Hat три года и является ведущим архитектором Red Hat Network. Он, также, поддерживает пакет perl, все пакеты perl-модулей и пакет spamassassin для Fedora Core и Red Hat Enterprise Linux, а также является автором обвязки perl RPM2 для работы с RPM и многих других модулей в CPAN. В свободное время он любит играть со своей собакой и спорить без особой на то причины.