Парсинг на Perl.

Perl быстро становится ключевым инструментом обычного системного администратора и волшебной шляпой системного программиста.

Легко, однако, испугаться 211 страниц документации, которая прилагается к последнему (пятому) релизу Perl. Быть может, вы уже спрашиваете себя "с чего начинать?" и "сколько всего надо знать, чтобы писать программы на Perl?"

Легче всего - посмотреть, как кто-то другой решает простую проблему.

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

Разберем подвернувшуюся нам задачу на простые задачи и их решения.

[? We'll build up to the task at hand by looking at some simpler problems and their solutions.]

Во-первых, посмотрим, как распечатывается первая колонка вывода команды

who, [just for grins].

who | perl -ne '@F = split; print "$F[0]n";'

Вывод who передается на ввод Perl. Ключ -n позволяет выполнить некоторый код, помещая каждую входящую строку в переменную $_. Ключ -e задает код, и мы можем (и часто будем) совмещать ключи показанным образом.

В нашем случае вы имеем два выражения: операции split и print. split разбивает содержимое $_ на список слов (подразумевая разделителем между словами пробел). Результат получает массив @F.

Затем операция [? operation. операция - плохо, потому что похоже на оператор, а это функция] print отображает значение первого элемента масства, завершенное переводом строки (n). Заметим, что доступ к первому лементу @F происходит через $F[0], потому что элементы нумеруются, начиная от нуля (как в массивах C).

Можно немного сэкономить на наборе, если вынести разделение в аргументы командной строки:

who | perl -ane 'print "$F[0]n"'

Заметим, что здесь мы добавили ключ -a, который заставляет Perl разбивать содержимое $_ в @F втоматически, так же, как в предыдущем примере мы сделали это явно.

Чтобы набирать еще чуть меньше, можно добавить ключ -l, который делает две вещи сразу:

удаляет перевод строки из переменной $_ перед тем, как ее увидит наш код (на самом деле его (код) не волнует, есть ли он (перевод) там (в строке)), и
приклеивает перевод строки обратно на выходе.

После этого наш маленький командно-строковый пример будет выглядеть так:

who | perl -lane 'print $F[0]'

И, чтобы сократить еще чуть-чуть, заменим ключ -n ключом -p, который позволяет печатать то, что получилось в $_ в конце кода:

who | perl -lape '$_ = $F[0]'

Да, действительно, мы выиграли только один символ. Но это все таки один символ, и, может, это даст большие сбережения, если вы будете экономить по символу каждый день в ближайшие пять лет. Может и нет.

Скрипт, эквивалентный предыдущему вызову Perl, будет выглядеть примерно так:

#!/usr/bin/perl

$ = $/; # from -l

while (<>) { # from -p

   chop; # from -l

   @F = split; # from -a

   $_ = $F[0]; # argument to -e

   print; # from -p

}

Как вы видите, немаленький кусок кода [] можно задать несколькими символами в командной строке.

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

Здесь мы придаем этой переменной значение $/, разделителя входящих записей (как RS в awk). По умолчанию это "n". То есть разделитель вывода такой же, как разделитель ввода, и к печатаемому будет добавляться перевод строки.

Закончим, наконец, с командой who. Перейдем к реальной задаче: проход по файлу паролей для получения наибольшего пользовательского ID.

Файл паролей отличается от вывода команды who - здесь колонки разделяются не пробелами, а двоеточием. Нет проблем - укажем другой символ-разделитель:

perl -aF: -lne 'print $F[0]' /etc/passwd

и мы получим список пользователей на стандартном выводе. Ключ -F задает двоеточие как разделитель. Заметим, что мы поставили ключ -a перед -F, что, я думаю, вполне логично -- разделитель полей не имееет смысла, если их не разделять.

Если у вас запущены Желтые Страницы [Yellow Pages], то есть, я хотел сказать, Network Information Services, вам, возможно, понадобится вытягивать пароли отсюда, а не из файла, чтобы получить что-то полезное:

ypcat passwd | perl -aF: -lne 'print $F[0]'

Здесь команда ypcat выдает пароле-подобный файл на стандартный вывод, где команда Perl радостно его слизывает, как если бы это был локальный файл etc/password.

Но это имена пользователей, не пользовательские ID. Они в третьем столбце, в $F[2] (опять же, сдвинуто на один, потому что отсчет начинается с нуля). Немного подредактируем, и:

perl -aF: -lne 'print $F[2]' /etc/passwd

Теперь у нас есть список чисел. Уже лучше. Нам нужно определить наибольшее число, и распечатать еще большее.

Для этого используем скалярную переменную $max. Изначально $max не определена, и при сравнении с другими числами будет выглядеть как ноль.

Итак, работа состоит в том, чтобы сравнить номер каждого пользователя с $max, и присвоить $max этот номер, если он больше.

perl -aF: -lne '$max = $F[2] if $max < $F[2]; print $max' /etc/passwd

Здесь мы присваиваем $max значение, если выполняется условие. В данном случае условие

$max < $F[2]

вычисляется на каждой итерации цикла, и, если результат истина, происходит присваивание. Это единственное место в Perl, где логическая последовательность идет справа налево, а не слева направо.

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

#!/usr/bin/perl

$ = $/;

while (<>) {

   chop;

   @F = split /:/;

   $max = $F[2] if $max < $F[2];

   print $max;

}

Еще лучше. Однако нам все еще нужно скормить скрипту /etc/passwd, что несколько обременительно для вызывающего. Так что откроем файл /etc/passwd прямо в программе.

#!/usr/bin/perl

open(PASSWD,"/etc/passwd");

$ = $/;

while (<PASSWD>) {

   chop;

   @F = split /:/;

   $max = $F[2] if $max < $F[2];

   print $max;

}

open() создает дескриптор [? filehandle. глупо, на самом деле, переводить handle как "дескриптор"] для чтения файла /etc/passwd.

Ваше, желтостраничники [YP'ers], решение будет на пару символов длиннеее:

#!/usr/bin/perl

open(PASSWD,"ypcat passwd|");

$ = $/;

while (<PASSWD>) {

   chop;

   @F = split /:/;

   $max = $F[2] if $max < $F[2];

   print $max;

}

Perl чУдно использует вывод команды как файл. О том, что это команда, а не файл, свидетельствует завершающая вертикальная черта. Это напоминание о потоке [? pipe. не уверен.], который используется, когда мы пишем программу в командной строке.

На выходе этих последних программ будет серия чисел с наибольшим найденным пользовательский ID. На самом деле нам нужно распечатать самый последний номер. Нет, еще раз. На самом деле нам нужен номер, больший на единицу. Как это сделать в программе? Просто. Просто вынесем печать из цикла:

#!/usr/bin/perl

open(PASSWD,"/etc/passwd"); # or YP equivalent

$ = $/;

while (<PASSWD>) {

   chop;

   @F = split /:/;

   $max = $F[2] if $max < $F[2];

}

print $max + 1;

Не забудьте + 1, чтобы получить больше, чем прошлое наибольшее.

Whew! Мы можем набить этот скрипт в файл, преобразовать в исполнимый код, поместить его где-нибудь в $PATH, и, когда нам нужен новый номер пользователя, просто вызовем его в обратных кавычках [уточнить], и

получим правильное значение.

Или что-то около правильного номера. Как оказалось, некоторые системы (например, SunOS, на которой я это тестировал), имеют пользователя nobody, с очень-очень большим ID. Если вы запустите эту программу в своей системе и получите что-то вроде 65535, у вас такой тоже есть.

Так что нам нужно исключить из нашего подсчета все, что выше какого-то порога. Как же это сделать?

Допустим, $max не нужно устанавливать, если $F[2] превышает наш порог (скажем, 30000). Что делает if чуть более сложным:

#!/usr/bin/perl

open(PASSWD,"/etc/passwd"); # or YP equivalent

$ = $/;

while (<PASSWD>) {

   chop;

   @F = split /:/;

   $max = $F[2] if $F[2] < 30000 and $max < $F[2];

}

print $max + 1;

На этом можно остановиться (надеюсь). Во всяком случае, в SunOS работает.

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

perl -aF: -lne '$m=$F[2] if $F[2]<30000 and $m<$F[2];

END { print $m+1 }' /etc/passwd

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

Если вы не знакомы с Perl, возможно, вам пригодится хорошая книга. Я могу порекомендовать две, хотя я несколько пристрастен, потому что причастен к написанию обеих.

Learning Perl (O'Reilly and Associates, ISBN 1-56592-042-2) - нежное введение [? gentle introduction] в язык, с примерами и развернутыми ответами. Это книга для тех, кто "знаком с UNIX, но никак не гуру". Но требует некотрого знания снов программирования.

Programming Perl (O'Reilly and Associates, ISBN 0-937175-64-1) - здоровенный всесторонний справочник по языку, в соавторстве с создателем Perl, Ларри Уоллом. Здесь вы найдете немного поверхностной обучающей информации, и массу длинных практических примеров. Однако это скорее книга для гуру, и может пролететь мимо головы, если вы на хакаете UNIX с 1977, как я.