Слово Perl является аббревиатурой выражения Practical Extraction and Report Language (практический язык извлечений и отчетов), хотя иногда его называют Pathologically Eclectic Rubbish Lister (патологически эклектичный мусорный листер). Не стоит спорить о том, какое из этих названий более правильное, потому что оба они принадлежат Ларри Уоллу, создателю и главному архитектору, распространителю и опекуну языка Perl. Ларри создал этот язык, когда пытался формировать отчеты из иерархии файлов системы оповещения об ошибках, похожей на Usenet-новости, а возможности применявшегося в то время обработчика потоков данных awk оказались исчерпанными. Будучи настоящим (то есть ленивым) программистом, Ларри решил вырвать данную проблему с корнем, применив для этого какой-нибудь универсальный инструмент, который он надеялся использовать и в дальнейшем. В результате появилась первая версия языка Perl.
Позабавившись немного с этой версией, добавив кое-что, Ларри предложил ее сообществу читателей материалов телеконференций Usenet, известному также как "Сеть" (the Net). Пользователи, имеющие доступ к входящим в систему Usenet компьютерам, разбросанным по всему свету (а их в то время было несколько десятков тысяч), обеспечили для создателя Perl эффективную "обратную связь", спрашивая, как делать одно, другое, третье. Многие из этих задач Ларри даже и не собирался ставить перед своим маленьким новым языком программирования.
В результате Perl все рос и рос, причем почти с той же скоростью, что и операционная система UNIX. (Специально для новичков: все ядро UNIX тогда требовало памяти объемом 32 К! Теперь мы счастливы, если нам удается уместить его в несколько мегабайтов.) Выросли и его возможности.
Повысилась переносимость. То, что когда-то было компактным языком, теперь сопровождается сотнями страниц документации, в состав которой входят десятки man-страниц, 600-страничный справочник серии Nutshell, материалы множества телеконференций Usenet с 200000 подписчиков, — а теперь еще и эта скромная книга.
Ларри уже не сопровождает Perl в одиночку, но сохраняет свой эксклюзивный титул главного архитектора. A Perl все растет и растет.
Примеры программ, содержащихся в этой книге, мы проверяли на Perl версии 5.004 (на момент написания книги это был самый последний выпуск). Все программы, которые здесь рассматриваются, должны работать с Perl версии 5.0 и со всеми последующими версиями. По сути дела, даже программы, написанные на языке Perl версии 1.0, довольно неплохо работают с последними версиями, если только не сталкиваются с некоторыми эксцентричными нововведениями, сделанными во имя прогресса.
Назначение языка Perl — помочь программисту в выполнении рутинных задач, которые для shell слишком трудны или плохо переносимы, а также чересчур заумны, одноразовы или сложны для кодирования на С или ином используемом в UNIX языке.
Научившись пользоваться языком Perl, вы, возможно, обнаружите, что начинаете тратить меньше времени на правильное заключение в кавычки различных параметров shell (или на корректное выполнение С-объявлений), а больше — на чтение Usenet-новостей и катание с гор на лыжах, потому что Perl — замечательное средство для вашего совершенствования как программиста. Мощные конструкции этого языка позволяют создавать (с минимальной затратой сил) некоторые очень эффективные специализированные решения и универсальные инструменты. Эти инструменты можно использовать и в дальнейшем, потому что написанные на Perl программы отличаются высокой переносимостью и готовностью к использованию. В результате у вас появится еще больше времени для чтения Usenet-новостей и посещения с друзьями баров караоке.
Как и любой язык, Perl может быть языком "только_для_написания" программ, которые потом будет невозможно прочитать. Однако при правильном подходе вы можете избежать этого весьма распространенного недостатка. Да, иногда Perl-текст выглядит для непосвященных как случайный набор символов, но умудренный опытом Perl-программист знает, что у этого набора есть контрольная сумма и каждый его символ имеет свое предназначение. Если вы будете следовать указаниям, приведенным в нашей книге, ваши программы будут легкими для чтения и простыми для сопровождения — но, вероятно, не выиграют никаких "состязаний по заумности".
Если при попытке вызвать Perl из Shell вы получите сообщение
perl: not found
это значит, что вашего системного администратора еще не охватила Perl-лихорадка. Если язык Perl не инсталлирован в вашей системе, его можно получить бесплатно (или почти бесплатно).
Perl распространяется по открытой лицензии GNU (GNU Public License)(*1), согласно которой "вы можете распространять двоичные файлы языка Perl только в том случае, если предоставляете исходный код бесплатно, а если вы модифицируете их, вы должны распространять и код этих изменений". По сути дела это означает, что Perl распространяется бесплатно. Его исходный текст можно получить по цене пустой ленты или оплатив стоимость передачи нескольких мегабайтов информации по телефонной линии. При этом никто не может "придержать" сам Perl и послать вам просто двоичные файлы, соответствующие чьему-либо конкретному представлению о "поддерживаемых аппаратных платформах".
По сути дела, Perl не только бесплатен, но и работает достаточно хорошо почти на всем, что называет себя UNIX или UNIX-подобной системой и включает С-компилятор. Это обусловлено тем, что пакет распространяется с адаптивной программой конфигурации, называемой Configure, которая рыщет и шарит по системным каталогам в поисках нужных ей вещей, соответствующим образом корректирует используемые файлы и определенные символы, обращаясь к вам за подтверждением результатов своих изысканий.
Программисты настолько увлеклись языком Perl, что, помимо UNIX- и UNIX-подобных систем, начали использовать его и в системах Amiga, Atari ST, системах семейства Macintosh, VMS, OS/2, даже MS-DOS и, наконец, в Windows NT и Windows 95. К тому моменту, когда вы будете читать эти строки, Perl, вероятно, перенесут и на многие другие системы. Исходные тексты языка Perl (и многие предкомпилированные двоичные файлы для He-UNIX-архитектур) можно получить на одном из серверов сети CPAN (Comprehensive Perl Archive Network). Если вы имеете доступ к World Wide Web, посетите сервер hftp://www.perl.com/CPAN, являющийся одним из множества "зеркальных" (дублирующих) серверов. Если вы абсолютный новичок, отправьте по адресу bookquestions@ora.com послание с вопросом "Где можно получить Perl?!?!" ^Where 1 сап set Perl?!?!").
Сценарий shell — это не что иное, как последовательность команд shell, оформленная в виде текстового файла. Этот файл затем превращается в исполняемый путем включения бита исполнения (посредством команды chmod + х имя_файла), после чего по приглашению shell вводится имя файла. Давайте рассмотрим какой-нибудь сценарий shell. Например, сценарий выполнения команды date, а затем команды who можно записать и выполнить так:
% echo date > somescript % echo who ' somescript % cat somescript date who ' % chmod +x somescript % somescript [результат выполнения команды date, затем команды who] %
Аналогичным образом Perl-программа — это набор Perl-операторов и определений, записанных в виде файла. Затем включается бит исполнения (*2), после чего по приглашению shell вводится имя файла. При этом, однако, файл должен иметь признак, указывающий, что это Perl-программа, а не программа shell.
В большинстве случаев операция введения такого признака заключается в помещении в начало файла строки
#!/usr/bin/perl
Если же ваш Perl инсталлирован в каком-то нестандартном каталоге или если ваша система "не понимает" строку, начинающуюся с символов #!, придется сделать кое-что еще. Узнайте об этом у того, кто инсталлировал вам Perl. В примерах, приведенных в книге, предполагается, что вы пользуетесь именно этим, общепринятым механизмом обозначения Perl-программ.
Perl — это, в основном, язык со свободным форматом записи программ (вроде С) — пробельные символы, включаемые между лексемами (элементами программы, например print или +), не обязательны, если две рядом стоящие лексемы невозможно принять за какую-то третью лексему. В последнем случае какой-нибудь пробельный символ является обязательным. (К пробельным символам относятся пробелы, знаки табуляции, символы новой строки, символы возврата каретки, символы перехода на новую страницу.) Имеется также ряд конструкций, в которых требуется использовать определенный пробельный символ в определенном месте, но об этом мы расскажем, когда дойдем до их описания. Вы можете считать, что тип и
количество пробельных символов между лексемами во всех прочих случаях могут быть произвольными.
Хотя почти каждую Perl-программу можно записать в одну строку, эти программы обычно пишут с отступами, как С-программы, причем вложенные операторы записывают с большим отступом, чем охватывающие. В этой книге вы увидите множество примеров, записанных с типичными для языка Perl отступами.
Аналогично сценарию shell, Perl-программа состоит из всех операторов Perl, имеющихся в файле и рассматриваемых в совокупности как одна большая программа, подлежащая выполнению. Понятия "основной" (main) программы, как в С, здесь нет.
Комментарии в Perl похожи на комментарии shell (современные). Комментарием является все, что следует за незаключенным в кавычки знаком # вплоть до конца строки. Многострочных комментариев, как в С, здесь нет.
В отличие от большинства shell (но аналогично awk и sed), интерпретатор языка Perl перед выполнением программы полностью разбирает ее и компилирует в свой внутренний формат. Это значит, что после запуска программы вы никогда не получите сообщение о синтаксической ошибке и что пробельные символы и комментарии не замедляют ход выполнения программы. Такой метод обеспечивает быстрое выполнение операций языка Perl после запуска и является дополнительным стимулом к отказу от использования С в качестве служебного языка систем лишь на том основании, что С — транслируемый язык.
Но процедура компиляции все же требует времени, и применение большой Perl-программы, которая быстро выполняет одну маленькую задачу (из множества тех, которые она способна выполнить), а затем заканчивает свою работу, не будет эффективным, ибо время ее выполнения окажется ничтожно малым по сравнению со временем компиляции.
Perl, таким образом, работает и как компилятор, и как интерпретатор. С одной стороны, это компилятор, потому что перед выполнением первого оператора программы она полностью считывается и разбирается. С другой стороны. Perl — интерпретатор, потому что никакого объектного кода, занимающего место на диске в ожидании исполнения, в данном случае нет. Другими словами, он сочетает в себе лучшее из компилятора и интерпретатора. Конечно же, было бы просто здорово, если бы выполнялось какое-то кэширование компилированного объектного кода между вызовами, а то и его трансляция в "родной" машинный код. Рабочая версия такого компилятора фактически уже существует, и сейчас планируется, что она войдет в выпуск 5.005. О текущем состоянии дел можно узнать в сборнике FAQ, посвященном Perl.
Наше путешествие по стране Perl мы начнем с небольшой прогулки. В ходе этой прогулки мы ознакомимся с некоторыми возможностями языка Perl на примере небольшого приложения. Здесь приведены лишь очень краткие пояснения; каждая тема гораздо подробнее освещается в соответствующей главе. Тем не менее эта короткая прогулка должна дать вам возможность быстро "почувствовать" этот язык, и вы сможете решить, будете ли вы дочитывать книгу до конца или же отправитесь обратно к своим Usenet-новостям и лыжным склонам.
Давайте рассмотрим небольшую программу, которая делает что-то реальное. Вот ваша базовая программа, которая выводит на экран слова "Hello, World":
#!/usr/bin/perl -w
print ("Hello, World\n");
Первая строка говорит о том, что это программа написана на языке Perl.
Кроме того, первая строка является комментарием; ведь комментарием в Perl,
как и во многих интерпретирующих языках программирования, являются все
лексемы, стоящие после знака # и до конца текущей строки. Но, в отличие от
всех остальных комментариев, включенных в эту программу, комментарий в первой
строке особенный: Perl ищет здесь необязательные аргументы. В данном случае
использовался ключ -w. Этот очень важный ключ дает Perl указание выдавать
дополнительные предупреждающие сообщения о потенциально опасных конструкциях.
Вам следует всегда использовать в своих программах ключ -w.
Вторая строка — это выполняемая часть данной программы. Здесь мы видим функцию
print. В данном случае встроенная функция print имеет всего один аргумент,
С-подобную текстовую строку. В этой строке комбинация символов \n
обозначает символ новой строки. Оператор print завершается точкой с
запятой ;. Как и в С, все простые операторы в Perl завершаются
точкой с запятой (*3).
Когда вы вызываете эту программу, ядро запускает интерпретатор
Perl, который разбирает всю программу (обе строки, включая первую, т.е.
комментарий), а затем выполняет компилированный вариант. Первая и
единственная операция — выполнение функции print>, которая посылает
значения своих аргументов на устройство вывода. По окончании выполнения
программы этот Perl-процесс завершается и возвращает родительскому shell код
успешного выполнения.
Скоро вы увидите Perl-программы, в которых print и другие функции иногда
вызываются с круглыми скобками, а иногда — без них. Правило здесь простое:
круглые скобки для встроенных функций Perl не являются ни обязательными,
ни запрещенными. Их применение может прояснить ситуацию, а может и запутать
ее, так что выработайте собственный стиль.
Давайте немного усложним пример, ведь приветствие Hello, World —
слишком простое и статичное. Сделаем так, чтобы программа называла вас по
имени. Для этого нам нужно место для хранения имени, способ задания вопроса
об имени и способ получения ответа.
Одно из мест для хранения значений (вроде имени) — скалярная переменная.
Для хранения вашего имени в нашей программе мы используем скалярную
переменную $name. В главе 2, "Скалярные данные", мы более подробно
узнаем о том, что можно хранить в этих переменных и что можно с ними делать.
Пока же предположим, что мы можем хранить в скалярной переменной только одно
число или строку (последовательность символов).
Программа должна иметь возможность спросить у вас имя. Для этого нам нужен
способ выдачи приглашения и способ принятия от вас данных. Предыдущая
программа показала нам, как можно приглашать, используя для этого функцию
print. Получение же строки с терминала осуществляется с помощью конструкции
<stdin>, которая (в нашем случае) получает одну строку введенных данных.
Введенные данные мы присваиваем переменной $name. Это дает нам следующую
программу:
print "What is your name? "; #Как ваше имя? $name = <STDIN>;
Значение переменной $name пока содержит завершающий символ новой строки
(имя Anton поступает как Anton\n). Чтобы избавиться от этого символа,
мы воспользуемся функцией chomp, которая в качестве своего единственного
аргумента принимает скалярную переменную и удаляет из ее строкового значения
завершающий символ перехода на новую строку (пробельный символ), если он
присутствует:
chomp ($name);
Теперь нам нужно лишь сказать Hello и указать значение переменной
$name, что мы можем сделать так, как в Shell, поместив эту переменную
в заключенную в кавычки строку:
print "Hello, $name ! \n";
Как и в shell, если нам нужен именно знак доллара, а не ссылка на скалярную
переменную, мы можем предварить этот знак обратной косой чертой.
Сложив все вместе, получаем:
file "ch01-01.pl"
#!/usr/bin/perl -w
print "What is your name?";
$name = <STDIN>;
chomp($name);
print "Hello, $name \n";
Добавляем возможность выбора
Допустим теперь, что у нас припасено какое-то особое приветствие для
пользователя по имени Anton, а для остальных — обычное. Для этого
нам нужно сравнить имя, которое было введено, со строкой Anton, и,
если оно совпадает, сделать что-то особое. Давайте добавим в программу
С-подобную ветвь if-then-eise и операцию сравнения:
file "ch01-02.pl"
#!/usr/bin/perl
print "What is your name?";
$name = <STDIN>;
chomp ($name);
if ($name eq "Anton")
{
print "Hello, Anton! How good of you to be here!\n";
}
else
{
# обычное приветствие
print "Hello, $name! \n";
}
#!/usr/bin/perl
В операции eq сравниваются две строки. Если они равны (т.е.
совпадают все символы и длина строк одинакова), результатом будет
"истина". (В С и C++ подобной операции нет (*4) ).
file "ch01-03.pl"
#! /usr/bin/perl -w
$secretword = "llama"; # секретное слово
print "What is your name?";
$name = <STDIN>;
chomp $name;
if ($name eq "Anton")
{
print "Hello, Anton! How good of you to be here!\n";
}
else
{
print "Hello, $name, \n"; # обычное приветствие
print "What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
while ($guess ne $secretword)
{
print "Wrong, try again. What is the secret word?";
$guess = <STDIN>;
chomp ($guess) ;
}
}
Сначала мы задаем секретное слово, помещая его в скалярную переменную
$secretword. После приветствия программа спрашивает (посредством
вызова еще одной функции print) у пользователя (не Антона) его
вариант секретного слова. Этот вариант сравнивается с заданным секретным
словом в операции ne. Данная операция возвращает значение "истина",
если сравниваемые строки не равны (т.е. данная операция противоположна
операции eq). Результат сравнения управляет циклом while, который
выполняет этот блок операторов до тех пор, пока сравнение дает значение
"истина".
Конечно, эта программа плохо защищена, потому что любой, кому надоест
угадывать секретное слово, может просто прервать ее выполнение и вернуться
к приглашению, а то и подсмотреть секретное слово в исходном тексте. Мы,
однако, не пытались разработать систему обеспечения безопасности, а лишь
хотели привести подходящий для данного раздела пример.
Давайте посмотрим, как можно модифицировать эту программу так, чтобы она принимала несколько секретных слов. Используя то, что мы уже видели, можно было бы многократно сравнивать вариант-догадку с рядом правильных ответов, хранящихся в отдельных скалярных переменных. Такой список, однако, было бы трудно корректировать или модифицировать в зависимости от дня недели и даты.
Более эффективное решение — хранить все возможные ответы в структуре данных, которая называется список, или (предпочтительнее) массив. Каждый элемент массива — это отдельная скалярная переменная, которой можно присваивать значение и затем использовать ее независимо от других. Можно также одним махом присвоить значение всему массиву. Мы имеем право присвоить значение всему массиву с именем @words так, чтобы он содержал три возможных правильных пароля:
@words = ("camel", "llarna", "alpaca");
Имена переменных-массивов начинаются с символа @, что позволяет
отличать их от имен скалярных переменных. Существует еще один способ
записи этой конструкции так, чтобы не нужно было ставить все эти кавычки —
с помощью операции qw(), например:
@words = qw(camel llama alpaca);
Это абсолютно то же самое; операция qw работает так, как будто мы взяли в
кавычки каждую из трех строк.
Присвоив значения элементам массива, мы можем обращаться к каждому из
них по индексной ссылке. Так, $words[0] — это camel,
$words[1] — llama, a $words[2] — alpaca.
Индекс может быть и выражением, поэтому если мы присвоим $i значение
2, то элементом $words[$i] будет alpaca.
(Индексные ссылки начинаются с символа $, а нес @, потому
что они обозначают один элемент массива, а не весь массив.) Вернемся к
нашему предыдущему примеру:
file "ch01-04.pl"
#!/usr/bin/perl -w
@words = qw (camel llama alpaca);
print "What is your name?";
$name = <STDIN>;
chomp ($name);
if ($name eq "Anton")
{
print "Hello, Anton! How good of you to be here!\n";
}
else
{
print "Hello, $name!\n"; # обычное приветствие
print "What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
$i = 0; # сначала попробуем это слово
$correct = "maybe"; # догадка верна или нет?
while ($correct eq "maybe")
{
# продолжаем проверку
if ($words [$i] eq $guess) # верно?
{
$correct = "yes"; # да!
}
elsif ($i < 2) # смотреть еще слова?
{
$i=$i+l; #в следующий раз посмотреть следующее слово
}
else
{
# больше слов нет, должно быть, неверно
print "Wrong, try again. What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
$i = 0; # вновь начать проверку с первого слова
}
} # конец цикла while для неверных слов
} # конец цикла "не Антон"
Заметьте, что мы используем скалярную переменную $correct для того,
чтобы показать, все еще ищем мы правильный пароль или уже нашли его.
В этой программе показан также блок elsif оператора if-then-else.
Такой конструкции нет ни в одном другом языке программирования; это
сокращенная запись блока else с новым условием if, но без вложения еще
одной пары фигурных скобок. Сравнение набора условий в каскадной цепочке
if-elsif-elsif-elsif-else очень характерно для языка Perl. В нем нет
но вы можете сами без особых хлопот создать такой оператор.
В предыдущем случае любой пользователь мог угадать одно из трех секретных
слов и получить доступ к программе. Если мы хотим, чтобы для каждого
пользователя было задано свое секретное слово, нам нужна примерно такая
таблица соответствий:
Пользователь Секретное слово Fred camel Barney llama Betty alpaca Wilma alpaca
Обратите внимание: у последних двух пользователей одинаковые секретные слова.
Такое допускается.
Самый простой способ сохранить такую таблицу — использовать хеш. В каждом
элементе хеша содержится отдельное скалярное значение (как и в массиве
любого другого типа), но в соответствие каждому элементу хеша ставится ключ.
Ключом может быть любое скалярное значение (любая строка или число, в том
числе нецелые и отрицательные числа). Чтобы создать хеш под именем %words
(обратите внимание на то, что используется символ % вместо @) с ключами и
значениями, данными в приведенной выше таблице, мы присвоим Swords значение
(почти так же, как мы делали раньше с массивом):
%words = qw (fred camel barney llama
betty alpaca wilma alpaca);
Каждая пара в этом списке представляет в хеше один ключ и соответствующее
ему значение. Обратите внимание на то, что мы разбили эту процедуру
присваивания на несколько строк без каких-либо символов продолжения строк,
потому что пробельные символы в Perl-программах обычно никакой роли не играют.
Чтобы найти секретное слово для Betty, мы должны использовать имя Betty
как ключ в ссылке на хеш %words с помощью выражения вроде $words{"betty"}.
Значение этой ссылки - alpaca, это похоже на то, что мы видели раньше,
работая с другим массивом. Как и раньше, ключом может быть любое выражение,
поэтому установка $person в значение betty и вычисление $words{$person}
также дает в результате alpaca.
Сведя все это воедино, мы получаем такую программу:
file "ch01-05.pl"
#!/usr/bin/perl -w
%words = qw (fred camel barney llama
betty alpaca wilma alpaca);
print "What is your name? ";
$name = <STDIN>;
chomp ( $name);
if ($name eq "Anton")
{
print "Hello, Anton! How good of you to be here!\n";
}
else
{
print "Hello, $name !\n"; # обычное приветствие
$secretword = $words{$name}; # получить секретное слово
print "What is the secret word? ";
$guess = <STDIN>;
chomp ($guess);
while ($guess ne $secretword)
{
print "Wrong, try again. What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
}
}
Обратите внимание на то, как происходит поиск секретного слова. Если имя не
найдено, то значением переменной $secretword будет пустая строка (*5),
и мы можем использовать оператор if, чтобы задать секретное слово по умолчанию
для кого-нибудь еще. Вот как это выглядит:
file "ch01-06.pl"
[... остальная часть программы пропущена ...]
$secretword = $words{$name}; # получить секретное слово
if ($secretword eq "") # не найдено
{
$secretword = "groucho"; #можно использовать
}
print "What is the secret word? ";
[... остальная часть программы пропущена ...]
Если вместо Anton вы введете Anton S. или anton, то
тем самым лишите Антона права на особое приветствие, потому что
сравнение eq предполагает точное равенство. Давайте рассмотрим
один способ решения задачи обработки различных вариантов ввода.
Допустим, вы хотите найти все строки, которые начинаются со слова Anton,
а не просто строку, равную Anton. В sed, awk или grep это можно сделать
с помощью регулярного выражения — шаблона, определяющего совокупность
соответствующих строк. Как и в sed, awk или grep, в Perl регулярным
выражением, которое соответствует любой строке, начинающейся со слова
Anton, будет ^Anton. Чтобы сравнить его со строкой,
содержащейся в скалярной переменной $name, мы используем операцию
сопоставления:
if ($name =~ /^Anton/)
{
# да, совпадает
}
else
{
# нет, не совпадает
}
Обратите внимание на то, что регулярное выражение выделяется косой чертой
с обеих сторон. Пробелы и другие пробельные символы, заключенные между
косыми, имеют значение, поскольку они являются частью строки.
Это почти решает нашу задачу, но не позволяет выбрать anton или
отклонить Anton. Чтобы принять anton, мы добавляем
опцию игнорирования регистра — прописную букву i
после закрывающей косой. Чтобы отклонить
Antonn, мы вводим в регулярное выражение специальный маркер границы
слова (подобно тому как это делается в vi и в некоторых версиях grep)
в форме \b. Это гарантирует, что символ, следующий в регулярном выражении
за второй буквой n, не является еще одной буквой. В результате наше регулярное
выражение принимает вид /^anton\b/i, что означает "слово anton, стоящее
в начале строки, за которым нет ни буквы, ни цифры, при этом регистр не имеет
значения". Объединив этот фрагмент с остальной частью программы, получим:
file "ch01-07.pl"
#!/usr/bin/perl
%words = qw (fred camel barney llama
betty alpaca wilma alpaca);
print "What is your name?";
$name = <STDIN>;
chomp ($name);
if ($name =~ /^anton\b/i)
{
print "Hello, Anton! How good of you to be here!\n";
}
else
{
print "Hello, $name!\n"; # обычное приветствие
$secretword = $words {$name}; # получить секретное слово
if ($secretword eq "") # не найдено
{
$secretword = "groucho"; # можно использовать
}
print "What is the secret word? ";
$guess = <STDIN>;
chomp ($guess);
while ($guess ne Ssecretword)
{
print "Wrong, try again. What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
}
}
Как видите, эта программа уже довольно далека от простенькой Hello, World. Хотя она и очень мала, но вполне работоспособна, причем краткость программы достигается весьма небольшими усилиями. В этом — стиль Perl.
В Perl имеется все, что необходимо для работы с регулярными выражениями, т.е.
он предоставляет все возможности, которые обеспечивает любая стандартная
утилита UNIX (и даже некоторые нестандартные). Способ сопоставления строк,
используемый в Perl, является, чуть ли не самым быстрым сравнительно с
другими языками, поэтому производительность системы при выполнении
Perl-программ никоим образом не снижается. (Написанная на Perl grep-подобная
программа часто превосходит прилагаемую поставщиками программу grep на С (*6).
Это значит, что grep не выполняет толком даже единственную свою задачу.)
Итак, теперь я могу ввести Anton, anton или Anton N. Solo, но как быть с
остальными? Барни должен вводить в точности barney (ему нельзя ввести даже
пробел после barney).
Чтобы быть справедливыми по отношению к Барни, мы должны перед поиском
имени в таблице взять первое слово из того, что введено, а затем заменить
все его символы символами нижнего регистра. Это делается с помощью
двух операций — операции подстановки, которая находит регулярное выражение
и заменяет его строкой, и операции перевода, которая переводит символы этой
строки в нижний регистр.
Сначала — операция подстановки: мы хотим взять содержимое переменной $name,
найти первый специальный (не использующийся в словах) символ и убрать все
символы, начиная с того места, где он стоит, и до конца строки. Искомое
регулярное выражение имеет вид /\W.*/. Здесь \W обозначает
специальный символ (т.е. все кроме буквы, цифры и знака подчеркивания),
а .* обозначают любые символы с этого места до конца строки. Чтобы убрать
эти символы, нужно взять ту часть строки, которая совпадает с рассматриваемым
регулярным выражением, и заменить ее пустой строкой:
$name =~ s/\W.*//;
Мы используем ту же операцию =~, что и раньше, но справа у нас
теперь стоит операция подстановки — буква s, за которой следуют
заключенные между двумя косыми регулярное выражение и строка.
(Строка в данном примере — это пустая строка между второй и третьей косыми.)
Эта операция выглядит и выполняется во многом так же, как операции подстановки
в программах-редакторах.
Теперь для того, чтобы перевести все оставшиеся символы в нижний регистр,
мы преобразуем эту строку с помощью операции tr (*7). Она очень похожа на
UNIX-команду tr, т.е. получает список искомых символов и список символов,
которыми искомые символы заменяются. В нашем примере мы, чтобы перевести
содержимое переменной $name в нижний регистр, используем такую запись:
$name =~ tr/A-Z/a-z/;
Между косыми заключены списки искомых и заменяющих их символов. Дефис
между буквами а и z обозначает все символы, находящиеся между ними, т.е.
у нас есть два списка, в каждый из которых включено по 26 символов.
Когда tr находит символ из какой-либо строки первого списка, он заменяется
соответствующим символом из второго списка. В результате все прописные буквы
А, В, С и т.д. становятся строчными (*8). Объединяя эти строки с остальной
частью программы, получаем:'
file "ch01-08.pl"
#!/usr/bin/perl
%words = qw (fred camel barney llama
betty alpaca wilma alpaca);
print "What is your name?";
$name = <STDIN>;
chomp ($name);
$originalname = $name; # сохранить для приветствия
$name =~ s/\W.*//; # избавиться от всех символов,
# следующих после первого слова
$name =~ tr/A-Z/a-z/; # перевести все в нижний регистр
if ($name eq "anton") # теперь можно так сравнить
{
print "Hello, Anton! How good of you to be here!\n";
}
else
{
print "Hello, $originalname!\n"; #обычное приветствие
$secretword = $words{$name}; # получить секретное слово
if ($secretword eq "") # не найдено
{
$secretword = "groucho"; # конечно, можно использовать
}
print "What is the secret word? ";
$guess = <STDIN>;
chomp ($guess);
while ($guess ne $secretword)
{
print "Wrong, try again. What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
}
}
Обратите внимание на то, что сопоставление с регулярным выражением для
слова Anton вновь выполняется с помощью обычной операции сравнения.
Дело в том, что и Anton N. Solo, и Anton после подстановки и перевода
превращаются в anton. Для всех остальных пользователей справедливо
то же самое, потому что Fred и Fred Flinstone превращаются во fred;
Barney Rubbie и Barney, the little guy — В barney И Т.Д.
Итак, благодаря всего нескольким операторам наша программа стала гораздо
более дружелюбной. Вы увидите, что проведение сложных манипуляций со
строками посредством всего лишь нескольких нажатий клавиш — одна из
многих сильных сторон языка Perl.
Отметим, однако, что в процессе обработки имени (т.е. при его модификации,
необходимой для проведения операции сравнения и поиска соответствия в
таблице) первоначально введенное имя уничтожается. Поэтому перед
обработкой имени программа сохраняет его в переменной $originalname.
(Как и имена в С, имена переменных в Perl состоят из букв, цифр и
знаков подчеркивания, причем длина их практически не ограничена.)
Благодаря этому мы впоследствии сможет ссылаться на $originalname.
В Perl имеется много способов, позволяющих проводить анализ и изменение
символов в строках. С большинством из них вы познакомитесь в главах 7 и 15.
Теперь, когда мы добавили так много строк к нашему первоначальному коду, нам
при его просмотре будет непросто уловить общую логику построения программы.
Поэтому было бы неплохо отделить высокоуровневую логику (запрос имени, циклы,
используемые для обработки введенных секретных слов) от низкоуровневой
(сравнение введенного секретного слова с заданным). Это необходимо сделать,
например, для облегчения понимания программы другими пользователями, или по
той причине, что один человек пишет высокоуровневую часть, а другой —
низкоуровневые фрагменты.
В Perl существует понятие подпрограммы, имеющей параметры и возвращаемые
значения. Подпрограмма определяется в программе один раз,
может многократно использоваться путем вызова ее из любого места программы.
Давайте создадим для нашей маленькой, но быстро растущей программы
подпрограмму good_word, которая будет принимать имя и вариант слова
и возвращать значение "истина", если это слово введено правильно,
и "ложь", если слово набрано неправильно. Определение такой подпрограммы
выглядит следующим образом:
file "ch01-09.pl"
sub good_word
{
# назвать параметры
my($somename,$someguess) = @_;
# избавиться от всех символов, стоящих после первого слова
$somename =~ s/\W.*//;
# перевести все символы в нижний регистр
$somename =~ tr/A-2/a-z/;
if ($somename eq "anton") # не нужно угадывать
{
return 1; # возвращаемое значение — true
}
elsif ((%words{$somename} || "groucho") eq $someguess)
{
return 1; # возвращаемое значение — true
}
else
{
return 0; # возвращаемое значение — false
}
}
Во-первых, определение подпрограммы состоит из зарезервированного для этих
целей слова sub, за которым идет имя подпрограммы и блок ее кода (выделенный
фигурными скобками). Это определение может стоять в тексте программы где
угодно, но большинство программистов помещают его в конец.
Первая строка в данном конкретном определении — это операция присваивания,
с помощью которой значения двух параметров подпрограммы копируются в
две локальные переменные с именами $somename и $someguess (директива
my() определяет эти переменные как локальные для блока, в который они
входят (в данном случае для всей подпрограммы), а параметры первоначально
находятся в специальном локальном массиве с именем @_)
Следующие две строки удаляют символы, стоящие после имени (точно так же,
как в предыдущей версии программы).
Оператор if-elsif-else позволяет определить, является ли введенный
пользователем вариант слова $someguess верным для имени $somename.
Имя Anton не должно попасть в эту подпрограмму, но даже если и попадет,
то любой вариант его ввода будет принят как правильный.
(%words{$somename} || "groucho") eq $someguess
Первый элемент в круглых скобках обеспечивает проведение уже знакомого
нам хеш-поиска, в результате которого отыскивается некоторое
значение в массиве %words на основании ключа, полученного из массива
$somename. Знак ||,, стоящий между этим значением и строкой
groucho, обозначает операцию ИЛИ, аналогичную той, что используется
в языке С, awk и в различных shell. Если поиск в хеше даст некоторое
значение (это значит, что ключ $somename находился в хеше), то оно и
будет являться значением данного выражения. Если ключ найден не был,
то используется строка groucho. Это весьма характерно для Perl:
приводится некоторое выражение, а затем с помощью операции || для него
указывается значение по умолчанию на тот случай, если результатом поиска
является значение "ложь".
file "ch01-09.pl"
#/usr/bin/perl
init_words();
print "What is your name?";
$name = <STDIN>;
chomp ($name);
if ($name =~ /^anton\b/i)
{
# обратно на другой путь :-)
print "Hello, Anton! How good of you to be here!\n";
}
else
{
print "Hello, $name!\n"; # обычное приветствие
print "What is the secret word? ";
$guess = <STDIN>;
chomp ($guess);
while (!good_word($name,$guess))
{
print "Wrong, try again. What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
}
}
## далее — подпрограммы
sub init_words
{
open (WORDSLIST, "wordslist.secret") || die "can't open wordlost.secret\n";
while ($name = <WORDSLIST>)
{
chomp($name);
$word = <WORDSLIST>;
chomp($word);
$words{$name} = $word;
}
close(WORDSLIST);
}
sub good_word
{
# назвать параметры
my($somename,$someguess) = @_;
# избавиться от всех символов, стоящих после первого слова
$somename =~ s/\W.*//;
# перевести все символы в нижний регистр
$somename =~ tr/A-2/a-z/;
if ($somename eq "anton") # не нужно угадывать
{
return 1; # возвращаемое значение — true
}
elsif (($words{$somename} || "groucho") eq $someguess)
{
return 1; # возвращаемое значение — true
}
else
{
return 0; # возвращаемое значение — false
}
}
Обратите внимание: мы вновь вернулись к использованию регулярного выражения
для проверки наличия имени Anton в массиве, потому что теперь уже в основной
программе не требуется выделять первое имя и заменять все его символы символами
нижнего регистра.
file "ch01-10.pl"
sub init_words
{
open (WORDSLIST, "wordslist.secret");
while ($name = <WORDSLIST>)
{
chomp ($name);
$word = <WORDSLIST>;
chomp($word);
$words($name} = Sword;
}
close(WORDSLIST);
}
Мы помещаем его в подпрограмму, чтобы не загромождать основную программу. Это
означает также, что позже мы сможем изменить место хранения списка слов и даже
его формат.
file "wordslist.secret"
fred
camel
barney
llama
betty
alpaca
wilma
alpaca
Функция open инициализирует дескриптор файла wordslist.secret, связывая его с файлом
wordslist.secret, находящимся в текущем каталоге. Отметим, что перед этим дескриптором
не ставится никакого забавного символа, вроде тех трех, что предваряют наши переменные.
Кроме того, дескрипторы файлов обычно записываются прописными буквами (хотя это и не
обязательно); причины этого мы рассмотрим позднее.
while (defined ($name = <WORDSLIST>)
{
...
}
Но если бы вы были еще более осторожны, вы, вероятно, проверили бы также и то,
возвращает ли функция open значение "истина". Это, кстати, в любом случае неплохая
идея. Для выхода из программы с сообщением об ошибке в случае, если что-то работает
не так, часто используется встроенная функция die. Мы рассмотрим пример этой функции
в следующей версии нашей программы.
file "ch01-10.pl"
#/usr/bin/perl
init_words();
print "What is your name?";
$name = <STDIN>;
chomp ($name);
if ($name =~ /^anton\b/i)
{
# обратно на другой путь :-)
print "Hello, Anton! How good of you to be here!\n";
}
else
{
print "Hello, $name!\n"; # обычное приветствие
print "What is the secret word? ";
$guess = <STDIN>;
chomp ($guess);
while (!good_word($name,$guess))
{
print "Wrong, try again. What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
}
}
## далее — подпрограммы
sub init_words
{
open (WORDSLIST, "wordslist.secret") || die "can't open wordlist.secret\n";
while ($name = <WORDSLIST>)
{
chomp ($name);
$word = <WORDSLIST>;
chomp($word);
$words($name} = Sword;
}
close(WORDSLIST);
}
sub good_word
{
# назвать параметры
my($somename,$someguess) = @_;
# избавиться от всех символов, стоящих после первого слова
$somename =~ s/\W.*//;
# перевести все символы в нижний регистр
$somename =~ tr/A-2/a-z/;
if ($somename eq "anton") # не нужно угадывать
{
return 1; # возвращаемое значение — true
}
elsif ((%words{$somename} || "groucho") eq $someguess)
{
return 1; # возвращаемое значение — true
}
else
{
return 0; # возвращаемое значение — false
}
}
Теперь написанный нами код начинает выглядеть как настоящая "взрослая" программа.
Обратите внимание: первая выполняемая строка — вызов подпрограммы init_words().
Возвращаемое значение в последующих вычислениях не используется, и это хорошо, потому
что мы не возвратили ничего заслуживающего внимания. В данном случае это гарантированно
значение "истина" (в частности, значение 1), потому что если бы opem не выполнилась,
то die вывела бы сообщение в stderr и вышла из программы. Функция die подробно описывается
в главе 10, но поскольку очень важно проверять возвращаемые значения всего, что может
завершиться неудачно, мы возьмем за правило использовать эту функцию с самого начала.
Переменная $! (тоже рассматривается в главе 10) содержит системное сообщение об ошибке,
поясняющее, почему данный системный вызов завершился неудачно.
file "ch01-11.pl"
sub init_words
{
open (WORDSLIST, "wordslist.secret") || die "can't open wordlist.secret\n";
if (-M WORDSLIST >= 7.0)
{
# в соответствии с бюрократическими правилами
die "Sorry, the wordslist is older than seven days.";
}
while ($name = <WORDSLIST>)
{
chomp ($name);
$word = <WORDSLIST>;
chomp($word);
$words($name} = Sword;
}
close(WORDSLIST);
}
Значение -M WORDSLIST сравнивается со значением 7. Если оно больше,
то мы, выходит, нарушили правила. Здесь мы видим новую операцию, операцию die,
которая одним махом выводит сообщение на экран (*10) и прерывает программу.Первый новый оператор здесь — open, в начале второго аргумента которого стоит оператор канала (1). Он указывает, что мы открываем процесс, а не файл. Поскольку оператор канала находится перед именем команды, мы открываем процесс так, чтобы можно было осуществить в него запись. (Если поставить оператор канала в конец, а не в начало, то можно будет читать выходную информацию команды.)A.C.: В винде у меня этот пример не заработал. file "ch01-12.pl" sub good_word { # назвать параметры my($somename,$someguess) = @_; # избавиться от всех символов, стоящих после первого слова $somename =~ s/\W.*//; # перевести все символы в нижний регистр $somename =~ tr/A-2/a-z/; if ($somename eq "anton") # не нужно угадывать { return 1; # возвращаемое значение — true } elsif (($words{$somename} || "groucho") eq $someguess) { return 1; # возвращаемое значение — true } else { open MAIL, "1mail tototo\@mail.ru" || die "can't open mail\n"; print MAIL "bad news: $somename guessed $someguess\n"; close MAIL; return 0; # возвращаемое значение — false } }
echo *.secretКак вы скоро увидите, Perl применяет похожий синтаксис имен с использованием метасимволов. Еще раз вернемся к определению подпрограммы init_words () :
Сначала мы поместили в новый цикл while основную часть подпрограммы из предыдущей версии. Новый элемент здесь — функция glob. По историческим причинам она называется filename glob. Эта функция работает почти так же, как <stdin>: при каждом обращении к ней она возвращает очередное значение из списка имен файлов, которые соответствуют образцу shell, в данном случае *.secret. Если таких имен файлов нет, возвращается пустая строка (*9).A.C.: В винде glob() не работает file "ch01-13.pl" sub init_words { while (defined($filename = glob("*.secret"))) { open (WORDSLIST, $filename) || die "can't open $filename\n"; if (-M WORDSLIST >= 7.0) { # в соответствии с бюрократическими правилами die "Sorry, the wordslist is older than seven days."; } while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp($word); $words($name} = Sword; } close(WORDSLIST) || die "couldn't close wordlist: $!" } }
Perl doc: These operators may spawn the C shell (csh), which cannot be made safe. This restriction will be lifted in a future version of Perl when globbing is implemented without the use of an external program.
К моменту достижения того места программы, где дан комментарий "# здесь начнется новый код", мы знаем три вещи: имя файла (содержится в переменной $filename), чье-то имя (в переменной $name) и секретное слово этого человека (содержится в $word). Здесь и нужно использовать имеющиеся в Perl инструменты формирования отчетов. Для этого где-то в программе мы должны определить используемый формат (обычно это делается в конце, как и для подпрограмм):A.C.: Здесь и далее я немного поменял текст - удалил вызов glob(). $filename = "wordslist.secret"; open (WORDSLIST, $filename) || die "can't open $filename\n"; if (-M WORDSLIST >= 7.0) { # в соответствии с бюрократическими правилами die "Sorry, the wordslist is older than seven days."; } while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST>; chomp($word); # здесь начнется новый код } close(WORDSLIST);
format STDOUT = @<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<< $filename, $name, $word .Определение формата начинается строкой format STDOUT =, а завершается точкой. Две строки между первой строкой и точкой — это сам формат. Первая строка формата — это строка определения полей, в которой задается число, длина и тип полей. В этом формате у нас три поля. Строка, следующая за строкой определения полей — это всегда строка значений полей. Строка значений содержит список выражений, которые будут вычисляться при использовании формата; результаты вычисления этих выражений вставляются в поля, определенные в предыдущей строке.
file "ch01-14.pl"
#!/usr/bin/perl
$filename = "wordslist.secret";
open (WORDSLIST, $filename) || die "can't open $filename\n";
if (-M WORDSLIST >= 7.0)
{
# в соответствии с бюрократическими правилами
die "Sorry, the wordslist is older than seven days.";
}
while ($name = <WORDSLIST>)
{
chomp ($name);
$word = <WORDSLIST>;
chomp($word);
write; # вызвать format STDOUT в STDOUT
}
format STDOUT =
@<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<<
$filename, $name, $word
.
Когда вызывается формат, Perl вычисляет выражения, имеющиеся в строке значений, и генерирует
строку, которую передает в дескриптор файла stdout. Поскольку write вызывается один раз при
каждом проходе цикла, мы получим ряд строк (под одной строке для каждого секретного слова);
итоговый текст будет разбит на столбцы.
file "ch01-15.pl"
format STDOUT_TOP =
Page @<<<
$%
Filename Name Word
.
Этот формат называется STDOUT_TOP он будет использоваться при первом вызове
формата stdout, а затем через каждые 60 строк, выведенных в STDOUT. Заголовки столбцов
позиционируются точно по столбцам формата STDOUT, поэтому все выглядит аккуратно.
file "ch01-16.pl"
A.C.: В винде не заработало ???
sub init_words
{
while ($filename = <*.secret>)
{
open (WORDSLIST, $filename)
|| die "can't open $filename: $!";
if (-M WORDSLIST <= 7)
{
while ($name = <WORDSLIST>)
{
chomp($name);
$word = <WORDSLIST>;
chomp($word);
$words{$name} = $word ;
}
}
else
{
# переименовываем файлы как было предложено
rename ($filename, "$filename.old")
|| die "can't rename $filename.old: $!";
}
}
}
Обратите внимание на новую часть оператора else в блоке проверки "возраста" файлов.
Если файл не обновлять семь дней и более, функция rename переименовывает его.
Эта функция принимает два параметра и переименовывает файл заданный первым
параметром, присваивая ему имя, указанное во втором параметре.
$last_good{$name} = time;
присваивает значение текущего времени, выраженное во внутреннем формате (некоторое
большое целое, свыше 800 миллионов, число, которое увеличивается на единицу каждую секунду)
элементу хеша %last_good, имеющему указанное имя в качестве ключа. В последующем это даст
базу данных о времени последнего правильного ввода секретного слова каждым из пользователей,
который вызывал эту программу.
dbmopen (%last_good, "lastdb", 0666) ||
die "can't dbmopen lastdb: $!", $last_good{$name} = time;
dbmclose (%last_good) || die "can't dbrnclose lastdb: $!";
Первый оператор выполняет отображение, используя имена файлов lastdb.dir и lastdb.pag
(это общепринятые имена для файлов lastdb, образующих DBM-файл). Если эти файлы необходимо
создать (а это бывает при первой попытке их использования), то для них устанавливаются
права доступа 0666 (*14). Такой режим доступа означает, что все пользователи могут читать
и осуществлять запись в эти файлы. Если вы работаете в UNIX-системе, то описание битов
прав доступа к файлу вы найдете на man-странице chmod [2]. В других системах chmod()
может работать так же, а может и не работать. Например, в MS-DOS для файлов не
устанавливаются права доступа, тогда как в Windows NT — устанавливаются. Если уверенности
нет, прочтите описание версии вашей системы.
file "ch01-17.pl"
#!/usr/bin/perl
dbmopen (%last_good, "lastdb", 0666) ||
die "can't dbmopen lastdb: $!";
foreach $name (sort keys (%last_good) )
{
$when = $last_good($name);
$hours = (timed - $when) / 3600; # вычислить истекшее время в часах
write;
}
dbmclose (%last_good)
|| die "can't dbrnclose lastdb: $!";
format STDOUT =
User @<<<<<<<<: last correct guess was @<<<< hours ago.
$name, $hours
.
Здесь мы осуществляем несколько новых операций: выполняем цикл foreach, сортируем
список и получаем значения ключей массива.
file "ch01-18.pl"
dbmopen (%last_good, "lastdb", 0666)
|| die "can't dbmopen lastdb: $!";
$last_good{$name} = time;
dbmclose (%last_good)
|| die "can't dbrnclose lastdb: $!";
#/usr/bin/perl
init_words();
print "What is your name?";
$name = <STDIN>;
chomp ($name);
if ($name =~ /^anton\b/i)
{
# обратно на другой путь :-)
print "Hello, Anton! How good of you to be here!\n";
}
else
{
print "Hello, $name!\n"; # обычное приветствие
print "What is the secret word? ";
$guess = <STDIN>;
chomp ($guess);
while (!good_word($name,$guess))
{
print "Wrong, try again. What is the secret word?";
$guess = <STDIN>;
chomp ($guess);
}
}
## далее — подпрограммы
sub good_word
{
# назвать параметры
my($somename,$someguess) = @_;
# избавиться от всех символов, стоящих после первого слова
$somename =~ s/\W.*//;
# перевести все символы в нижний регистр
$somename =~ tr/A-2/a-z/;
if ($somename eq "anton") # не нужно угадывать
{
return 1; # возвращаемое значение — true
}
elsif ((%words{$somename} || "groucho") eq $someguess)
{
return 1; # возвращаемое значение — true
}
else
{
return 0; # возвращаемое значение — false
}
}
sub init_words
{
open (WORDSLIST, "wordslist.secret") || die "can't open wordlist.secret\n";
if (-M WORDSLIST >= 7.0)
{
# в соответствии с бюрократическими правилами
die "Sorry, the wordslist is older than seven days.";
}
while ($name = <WORDSLIST>)
{
chomp ($name);
$word = <WORDSLIST>;
chomp($word);
$words($name} = Sword;
}
close(WORDSLIST);
}