Изучаем Perl
975b8bba

Допустим, вы хотите найти все


if ($name =~ /^Randal/) (

+f да, совпадает ) else ( ## нет, не совпадает

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

Это почти решает нашу задачу, но не позволяет выбрать randal или отклонить Randall. Чтобы принять randal, мы добавляем опцию игнорирования регистра — прописную букву i после закрывающей косой. Чтобы отклонить Randall, мы вводим в регулярное выражение специальный маркер границы слова (подобно тому как это делается в vi и в некоторых версиях grep) в форме \b. Это гарантирует, что символ, следующий в регулярном выражении за первой буквой 1, не является еще одной буквой. В результате наше регулярное выражение принимает вид /^randal\b/i, что означает "слово randal, стоящее в начале строки, за которым нет ни буквы, ни цифры, при этом регистр не имеет значения". Объединив этот фрагмент с остальной частью программы, получим:

#! /usr/bin/perl %words = qw ( fred camel barney llama betty alpaca wilma alpaca );

print "What is your name? "; $name = <STDIN>; chomp ($name); if ($name =~ /^randal\b/i) {

print "Hello, Randal! How good of you to be here!\n"; } else {

print "Hello, $name! \n"; # обычное приветствие $secretword = $words {$name}; # получить секретное слово if ($secretword eq "") { # не найдено

$secretword = "groucho"; t конечно, можно использовать }

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


на С*. Это значит, что grep не выполняет толком даже единственную свою задачу.)



Справедливость для всех



Итак, теперь я могу ввести Randal, randal или Randal L. Schwartz, но как быть с остальными? Барни должен вводить в точности barney (ему нельзя ввести даже пробел после barney).

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

которая переводит символы этой строки в нижний регистр.

Сначала — операция подстановки: мы хотим взять содержимое переменной $ name, найти первый специальный (не использующийся в словах) символ и убрать все символы, начиная с того места, где он стоит, и до конца строки. Искомое регулярное выражение имеет вид /\w.*/. Здесь \w обозначает специальный символ (т.е. все кроме буквы, цифры и знака подчеркивания), а . * обозначают любые символы с этого места до конца строки. Чтобы убрать эти символы, нужно взять ту часть строки, которая совпадает с рассматриваемым регулярным выражением, и заменить ее пустой строкой:

$name =~ s/\W.*//;

Мы используем ту же операцию =~, что и раньше, но справа у нас теперь стоит операция подстановки — буква s,

за которой следуют заключенные между двумя косыми регулярное выражение и строка. (Строка в данном

Однако GNU-версия утилиты egrep

выполняет эту операцию гораздо быстрее, чем Perl.

примере — это пустая строка между второй и третьей косыми.) Эта операция выглядит и выполняется во многом так же, как операции подстановки в программах-редакторах.

Теперь для того, чтобы перевести все оставшиеся символы в нижний регистр, мы преобразуем эту строку с помощью операции tr*. Она очень похожа на UNIX-команду tr, т.е. получает список искомых символов и список символов, которыми искомые символы заменяются. В нашем примере мы, чтобы перевести содержимое переменной $name в нижний регистр, используем такую запись:



$name =~ •tr/A-Z/a-z/;

Между косыми заключены списки искомых и заменяющих их символов. Дефис между буквами а и z обозначает все символы, находящиеся между ними, т.е. у нас есть два списка, в каждый из которых включено по 26 символов. Когда tr находит символ из какой-либо строки первого списка, он заменяется соответствующим символом из второго списка. В результате все прописные буквы А, В, С и т.д. становятся строчными**. Объединяя эти строки с остальной частью программы, получаем:'

#!/usr/bin/perl Swords " qw ( fred camel bamey llama betty alpaca wilma alpaca

print "What is your name? "; $name = <STDIN>; chomp ($name);

$original name = $name; # сохранить для приветствия $name =~ s/\W.*//; # избавиться от всех символов, следующих после первого слова

$name =~ tr/A-Z/a-z/; # перевести все в нижний регистр if ($name eq "randal") ( * теперь можно так сравнить

print "Hello, Randal! How good of you to be here!\n"; else (

print "Hello, $original_name! \n"; ^обычное приветствие $secretword = $words($namel; # получить секретное слово if ($secretword eq "") ( #

не найдено

$secretword == "groucho"; 4 конечно, можно использовать }

print "What is the secret word? "; $guess = <STDIN>; chomp ($guess); while ($guess ne $secretword) (

* Символы с диакритическими знаками такому переводу не поддаются. Подробности см. на man-странице рег11оса1е(\)

в версии языка Pel-15.004.

** Специалисты заметят ,что мы также могли написать нечто вродез/(\3*) .*/\L$1/, чтобы сделать все это за один присест, но специалисты, вероятно, не будут читать данный раздел.





print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; chomp ($guess);

Обратите внимание на то, что сопоставление с регулярным выражением для слова Randai вновь выполняется с помощью обычной операции сравнения. Дело втом, что и RandaL L. Schwartz, и Randai после подстановки и перевода превращаются в randai. Для всех остальных пользователей справедливо то же самое, потому что Fred и Fred Flinstone превращаются во fred; Barney Rubbie И Barney, the little guy — В barney И Т.Д.



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

Отметим, однако, что в процессе обработки имени (т.е. при его модификации, необходимой для проведения операции сравнения и поиска соответствия в таблице) первоначально введенное имя уничтожается. Поэтому перед обработкой имени программа сохраняет его в переменной $original name. (Как и имена в С, имена переменных в Perl состоят из букв, цифр и знаков подчеркивания, причем длина их практически не ограничена.) Благодаря этому мы впоследствии сможет ссылаться на $ original name.

В Perl имеется много способов, позволяющих проводить анализ и изменение символов в строках. С большинством из них вы познакомитесь в главах 7 и 15.



Повышение степени модульности



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

В Perl существует понятие подпрограммы,

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

Давайте создадим для нашей маленькой, но быстро растущей программы подпрограмму good_word, которая будет принимать имя и вариант слова и возвращать значение "истина", если это слово введено правильно, и "ложь", если слово набрано неправильно. Определение такой подпрограммы выглядит следующим образом:





sub good_word (



my($somename,$someguess) = @_; # назвать параметры $somename =~ s/\W.*//; # избавиться от всех символов, стоящих после

# первого слова

$somename =~ tr/A-2/a-z/; t перевести все символы в нижний регистр if ($somename eq "randal") ( # не нужно угадывать

return 1; # возвращаемое значение — true I elsif (($words($somename} 11 "groucho") eq $someguess) (

return 1; # возвращаемое значение — true ) else { return 0; * возвращаемое значение

false

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

Первая строка в данном конкретном определении — это операция присваивания, с помощью которой значения двух параметров подпрограммы копируются в две локальные переменные с именами $somename и $someguess (директива ту ( ) определяет эти переменные как локальные для блока, в который они входят (в данном случае для всей подпрограммы), а параметры первоначально находятся в специальном локальном массиве с именем @ .)

Следующие две строки удаляют символы, стоящие после имени (точно так же, как в предыдущей версии программы).

Оператор if-elsif-else позволяет определить, является ли введенный пользователем вариант слова ($someguess) верным для имени ($somename). Имя Randal не должно попасть в эту подпрограмму, но даже если и попадет, то любой вариант его ввода будет принят как правильный.

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

Проверка в части eisif выглядит довольно сложной; давайте разобьем ее на фрагменты:



($words($somename) И "groucho") eq $someguess

Первый элемент в круглых скобках обеспечивает проведение уже знакомого нам хеш-поиска, в результате которого отыскивается некоторое значение в массиве %words на основании ключа, полученного из массива $somename. Знак 1 1, стоящий между этим значением и строкой groucho, обозначает операцию ИЛИ, аналогичную той, что используется в языке С, awk и в различных shell. Если поиск в хеше даст некоторое значение (это





значит, что ключ $ some name находился в хеше), то оно и будет являться значением данного выражения. Если ключ найден не был, то используется строка groucho. Это весьма характерно для Perl: приводится некоторое выражение, а затем с помощью операции 1 1 для него указывается значение по умолчанию на тот случай, если результатом поиска является значение "ложь".

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

Выразим все это в виде правила: если имя — randal или если вводимый пользователем вариант соответствует одному из имен, находящемуся в массиве %words (со значением по умолчанию groucho, если имя в массиве не найдено), то подпрограмма возвращает 1; иначе подпрограмма возвращает 0. Теперь давайте свяжем все новые строки с остальной частью программы:

#! /usr/bin/perl Swords = qw( fred camel barney llama betty alpaca wilma alpaca ) ;

print "What is your name? "; $name = <STDIN>; chomp ($name) ; if ($name "~ /^randal\b/i) ( t обратно на другой путь :-)

print "Hello, Randal! How good of you to be here!\n"; I elscj/ {

print "Hello, $name! \n"; t обычное приветствие 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) ;



t... здесь вставляется определение подпрограммы good_word() .,,]

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

Наибольшее отличие этой программы от предыдущей состоит в том, что здесь используется цикл while, содержащий подпрограмму &good_word. При вызове этой подпрограммы ей передаются два параметра, $name и $auess. Значение $somename устанавливается равным первому параметру,

в данном случае $name. Аналогичным образом $someguess передается во втором параметре, $guess.

Значение, возвращаемое этой подпрограммой (1 или 0, если вы помните приведенное выше правило), логически инвертируется префиксной операцией ! (логическое НЕ). Эта операция возвращает значение "истина", если следующее за ней выражение ложно, или "ложь", если оно истинно. Результат этой операции управляет циклом while. Можете читать такую запись как "до тех пор, пока слово угадано неправильно...". Многие хорошо написанные Perl-программы очень похожи на обычный английский язык — если, конечно, не позволять себе много вольностей ни с языком Perl, ни с английским. (Но Пулитцеровскую премию даже за очень хорошую программу вам не дадут.)

Обратите внимание: при разработке этой подпрограммы предполагалось, "то значение хеша %words задается в основной программе.

Столь деликатный подход к использованию глобальных переменных вызван тем, что обращаться с ними нужно очень аккуратно. Говоря в общем, переменные, не созданные с помощью ту, глобальны для всей программы, тогда как переменные ту действуют только до завершения выполнения блока, в которым они были объявлены. Но не беспокойтесь: в языке Perl имеется множество других разновидностей переменных, включая переменные, локальные для файла (или пакета), и переменные, локальные для функции, которые сохраняют свои значения от вызова к вызову — а это как раз то, что мы могли бы здесь использовать. Впрочем, на данном этапе вашего знакомства с Perl изучение этих переменных только осложнило бы вам жизнь. Когда вы будете достаточно готовы к этому, посмотрите, что говорится о контекстах, подпрограммах, модулях и объектах в книге Programming Perl, или обратитесь к диалоговой документации, имеющейся на man-страницах perlsub(\), perlmod(l), perlobJ(l) и perltoot(l).



Перенос списка секретных слов в отдельный файл

Допустим, вы хотели бы использовать список секретных слов в трех программах. Если вы сохраните этот список так, как мы уже это делали, нам придется корректировать все три программы (если, например, Бетти решит, что ее секретным словом должно быть не alpaca, a swine). Это может стать настоящим кошмаром, особенно если Бетти отличается непостоянством.

Поэтому давайте поместим список слов в файл, а затем, чтобы ввести список в программу, просто прочитаем файл. Для этого нужно создать канал ввода-вывода, который называется дескриптором файла. Ваша Perl-программа автоматически получает три дескриптора файлов, stdin, stdout и stderr, которые соответствуют трем стандартным каналам ввода-вывода в большинстве сред программирования. Мы уже используем дескриптор stdin для чтения данных, поступающих от пользователя, запускающего нашу программу. Теперь нужно просто создать для выбранного нами файла другой дескриптор.

Это делается с помощью следующего кода:

sub init words (

open ('..'ORDSLIST, "wordslist"); while ($name = <WORDSLIST” (

chomp ($name); $word = <WORDSLIST>; chomp ($word); $words ($name} = Sword;

close (WORDSLIST) ;

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

Произвольно выбранный формат списка слов — один элемент в строке с чередованием имен и секретных слов. Для нашей базы данных мы имели бы такой список:

fred camel barney llama betty alpaca wilma alpaca

Функция open инициализирует дескриптор файла wordslist, связывая его с файлом wordslist, находящимся в текущем каталоге. Отметим, что перед этим дескриптором не ставится никакого забавного символа, вроде тех трех, что предваряют наши переменные. Кроме того, дескрипторы файлов обычно записываются прописными буквами (хотя это и не обязательно); причины этого мы рассмотрим позднее.

При выполнении цикла while читаются строки из файла wordslist (через дескриптор файла wordslist) по одной при каждом проходе цикла. Каждая строка заносится в переменную $name. По достижении конца файла операция <wordslist> возвращает пустую строку*, которая для цикла while означает "ложь", и завершает цикл.



Если бы вы выполняли программу с ключом -w, вам пришлось бы проверять, определено ли полученное возвращаемое значение. Пустая строка, которую возвращает операция <wordslist>, не совсем пуста: это опять значение undef. В тех случаях, когда это важно, проверка выражения на значение undef производится функцией defined. При чтении строк из файла эта проверка выполнялась бы следующим образом:

while ( defined ($name = <WORDSLIST) ) (

* На самом деле это опять undef, но для понимания данного материала сказанного достаточно.





Но если бы вы были еще более осторожны, вы, вероятно, проверили бы также и то, возвращает ли функция open значение "истина". Это, кстати, в любом случае неплохая идея. Для выхода из программы с сообщением об ошибке в случае, если что-то работает не так, часто используется встроенная функция die. Мы рассмотрим пример этой функции в следующей версии нашей программы.

С другой стороны, при нормальном развитии событий мы считываем строку (включая символ новой строки) в переменную $ name. Сначала с помощью функции chomp убирается символ новой строки, затем нужно прочитать следующую строку, чтобы получить секретное слово и сохранить sro в переменной $word. Символ новой строки при этом тоже убирается.

Последняя строка цикла while помещает $word в %words с ключом $name, чтобы переменную $word могла использовать остальная часть программы.

По завершении чтения файла его дескриптор можно использовать повторно, предварительно закрыв файл с помощью функции close. (Дескрипторы файлов автоматически закрываются в любом случае при выходе из программы, но мы стараемся быть аккуратными. Однако если бы мы были по-настоящему аккуратными, мы бы даже проверили бы, возвращает ли ^lose значение "истина" в случае, если раздел диска, в котором был файл, решил "отдохнуть", если сетевая файловая система стала недосягаемой или произошла еще какая-нибудь катастрофа. Такое ведь иногда случается. Законы Мерфи никто не отменял.)

Сделанное выше определение подпрограммы может идти после другого аналогичного определения или перед ним. Вместо того чтобы помещать определение %words в начало программы, мы можем просто вызывать эту подпрограмму в начале выполнения основной программы. Один из вариантов компоновки общей программы может выглядеть так:



fr! /usr/bin/perl init words () ;

print "What is your name? "; $name = <STDIN>; ;homp $name; if ($name =~ /^randal\b/i) ( * обратно на другой путь :-)

print "Hello, Randal! How good of you to be here!\n"; I else (

print "Hello, $name! \n"; # обычное приветствие •print "What is the secret word? "; $guess = <STDIN>; ^homp ($guess) ;

^hile (! good word($name, $guess)) I print "Wrong, try again. What is the secret word? "; $guess = <STDIN>; :homp ($guess);

} ## далее — подпрограммы





sub init_words ( open (WORDSLIST, "wordslist") 11

die "can' tl open woraJ.isT:: ^' .ihile ( defined ($name = <WORDSLIST”) ( chomp ($name) ; $word = <WORDSLIST>; chomp $word; $words( $name) = $word; I close (WORDSLIST) II die "couldn't close wordlist: $"'

iub good word (

my($somename,$someguess) = @_; * перечислить параметры $somename =~ s/\W.*//; # удалить все символы, стоящие после первого слова # первое слово

'somename =~ tr/A-Z/a-z/; # перевести все символы в нижний регистр :.f ($somename eq "randal") ) <t не нужно угадывать

return 1; * возвращаемое значение - true elsif (($words($somename) II "groucho") eq $someguess) (

return 1; * возвращаемое значение - true else ( •eturn 0; i возвращаемое значение — false

Теперь написанный нами код начинает выглядеть как настоящая "взрослая" программа. Обратите внимание: первая выполняемая строка — вызов подпрограммы init_word() . Возвращаемое значение в последующих вычислениях не используется, и это хорошо, потому что мы не возвратили ничего заслуживающего внимания. В данном случае это гарантированно значение "истина" (в частности, значение 1), потому что если бы close не выполнилась, то die вывела бы сообщение в stderr и вышла из программы. Функция die подробно описывается в главе 10, но поскольку очень важно проверять возвращаемые значения всего, что может завершиться неудачно, мы возьмем за правило использовать эту функцию с самого начала. Переменная $! (тоже рассматривается в главе 10) содержит системное сообщение об ошибке, поясняющее, почему данный системный вызов завершился неудачно.



Функция open используется также для открытия файлов при выводе в них информации и открытия программ как файлов (здесь она лишь упомянута). Полное описание этой функции будет дано гораздо позже, в главе 10.



Как обеспечить скромный уровень безопасности



'Этот список секретных слов должен меняться минимум раз в неделю!", — требует Главный Директор Списков Секретных Слов. Мы не можем, конечно, заставить пользователей еженедельно менять пароли, но должны хотя бы предупреждать их о том, что список секретных слов не изменялся в течение семи дней и более.

лучше всего делать это в подпрограмме init_words ( ) ; мы уже раоотаем в ней с файлом wordlist. Perl-операция -м возвращает значение, равное количеству дней, прошедшему с момента изменения файла или дескриптора файла, поэтому нам нужно просто посмотреть, превышает ли это значение число семь для дескриптора файла wordslist:

sub init words (

open (HORDSLIST, "wordslist") 11 die "can't open wordlist: $!";

if (-М WORDSLIST >= 7.0) ( f в соответствии с бюрократическими правилами

die "Sorry, the wordslist is older than seven days. "; I while ($name = <WORDSLIST” (

chomp ($name) ; $word = <WORDSLIST> ; chomp ($word) ; $words($name} = $word;

close (WORDSLIST) 11 die "couldn't close wordlist: $!";

Значение -м wordslist сравнивается со значением 7. Если оно больше, то мы, выходит, нарушили правила. Здесь мы видим новую операцию, операцию die, которая одним махом выводит сообщение на экран* и прерывает программу.

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

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



Как предупредить пользователя, если он сбился с пути



Давайте посмотрим, как можно заставить систему посылать сообщение электронной почты всякий раз, когда пользователь указывает свое секретное слово неверно. Нам нужно модифицировать только подпрограмму good word ( ) (сказывается преимущество модульности языка Perl), потому что вся необходимая информация находится у нас там.



Почтовое сообщение будет послано вам в том случае, если вы вставите свой адрес электронной почты там, где в программе записано YOUR_AD-DRESS_HERE. Все, что нам нужно для этого сделать, это непосредственно перед тем, как возвращать из подпрограммы 0, создать дескриптор файла, который фактически будет являться процессом (mail):

ячЬ good word (

my($sornename,$someguess) = @ ; # перечислить параметры •^somename =~ s/\W.*//; t удалить все символы, стоящие после

И

первого слова

* Если точнее, то в дескриптор файла stderr, но обычно это и означает терминал.





$somename =~ tr/A-Z/a-z/; # перевести все символы в нижний регистр if ($somename eq "randal") ( # не нужно угадывать return 1; #

возвращаемое значение — true

elsif (($words($somename}ll"groucho") eq $someguess) { return 1; ff возвращаемое значение — true

else (

open MAIL, "Imail YOUR_ADDRESS_HERE"; print MAIL "bad news: $somename guessed $someguess\n"; close MAIL; return 0; 4f

возвоашаемое значение — false

первый новый оператор здесь — open, в начале второго аргумента которого стоит оператор канала (1). Он указывает, что мы открываем процесс, а не файл. Поскольку оператор канала находится перед именем команды, мы открываем процесс так, чтобы можно было осуществить в него запись. (Если поставить оператор канала в конец, а не в начало, то можно будет читать выходную информацию команды.)

Следующий оператор, print, выбирает для вывода не stdout*, а дескриптор файла, стоящий между ключевым словом print и подлежащими выводу на экран значениями. Это значит, что сообщение в конечном итоге станет входной информацией для команды mail.

Наконец, мы закрываем дескриптор файла, в результате чего запускается программа mail и передает свои данные.

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



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



Несколько файлов секретных слов в текущем каталоге



Давайте слегка изменим способ определения имени файла секретных слов. Вместо файла с именем wordslist будем искать в текущем каталоге нечто, заканчивающееся на .secret. Попросим shell выдать краткий перечень таких имен.

echo *. secret

" Говоря техническим языком — выбранный в текущий момент дескриптор файла. ud этом, однако, мы поговорим позже.





Как вы скоро увидите, Perl применяет похожий синтаксис имен с использованием метасимволов. Еще раз вернемся к определению подпрограммы init_words () :

;ub init words (

while ( defined($filename = glob("*.secret")) ) ( open (WORDSLIST, $filename) 11

die "can't open wordlist: $!"; f (-M WORDSLIST >= 7.0) ( while ($name = <WORDSLIST” (

chomp $name; Sword = <WORDSLIST>; chomp $word; Swords ($name ) = $word; ) ) close (WORDSLIST) II die "couldn't close wordlist: $!";

Сначала мы поместили в новый цикл while основную часть подпрограммы из предыдущей версии. Новый элемент здесь — функция glob. По историческим причинам она называется filename glob. Эта функция работает почти так же, как <stdin>: при каждом обращении к ней она возвращает очередное значение из списка имен файлов, которые соответствуют образцу shell, в данном случае * . secret. Если таких имен файлов нет, возвращается пустая строка*.

Таким образом, если текущий каталог содержит файлы fred. secret и sarney.secret, то при первом выполнении цикла while значением переменной $fiiename будетЬагпеу.secret (именадаютсяпоалфавиту).При втором выполнении цикла значением $filename будет fred .secret. Поскольку при третьем вызове функции glob она возвращает пустую строку, го третий проход не делается, так как цикл while интерпретирует это значение как "ложь", что приводит к выходу из подпрограммы.



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

Отметим, что в отсутствие файлов, имена которых совпадали бы с шаблоном *. secret и "возраст" которых не превышал бы семи дней, подпрограмма завершится, не поместив ни одного секретного слова в массив %words. Это значит, что всем придется пользоваться словом groucho. Прекрасно. (В реальном

коде перед выходом из подпрограммы следовало бы ввести операцию проверки количества элементов в массиве %words — и при неудовлетворительном результате выполнить функцию die. Обратите внимание на функцию keys, когда мы дойдем до определения хешей в главе 5.)

* Да-да, опять undef. /.

Как получить список секретных слов

Итак, Главный Директор Списков Секретных Слов желает получить отчет обо всех секретных словах, используемых в текущий момент, с указанием их "возраста". Если мы на минутку расстанемся с программой проверки секретного слова, у нас будет время написать для Директора программу формирования необходимого ему отчета.

Сперва давайте получим все наши секретные слова, воспользовавшись для этого частью кода из подпрограммы init_words ( ) :

while ( defined($filename = glob("*.secret")) ) ( open (WORDSLIST, $filename) II die "can't open wordlist: $!"; if (-M WORDSLIST >= 7.0) < while ($name - <WORDSLIST” (

chomp ( $name ) ; $word = <WORDSLIST> ; chomp (Sword) ;

*** отсюда начинается новый код }

) I close (WORDSLIST) 11 die "couldn't close wordlist: $!"

К моменту достижения того места программы, где дан комментарий "отсюда начнется новый код", мы знаем три вещи: имя файла (содержится в переменной $filename), чье-то имя (в переменной $name) и секретное слово этого человека (содержится в $word). Здесь и нужно использовать имеющиеся в Perl инструменты формирования отчетов. Для этого где-то в программе мы должны определить используемый формат (обычно это делается в конце, как и для подпрограмм):



format STDOUT =



@“““““<““ @<““““ @“““““<

$filename, $name, $word

Определение формата начинается строкой format stdout=, а завершается точкой. Две строки между первой строкой и точкой — это сам формат. Первая строка формата — это строка определения полей, в которой задается число, длина и тип полей. В этом формате у нас три поля. Строка, следующая за строкой определения полей — это всегда строка значений полей. Строка значений содержит список выражений, которые будут вычисляться при использовании формата; результаты вычисления этих выражений вставляются в поля, определенные в предыдущей строке.

Вызывается определенный таким образом формат функцией write, например:

*! /usr/bin/perl

while ( defined($filename = glob("*.secret")) ) ( open (WORDSLIST, $filename) 11 die "can't open wordlist: $"';

ir (-M WORDSLIST >= i.u) { while ($name = <WORDSLIST>) { chomp ($name); $word = <WORDSLIST> ; chomp ($word) ; write; # вызвать format STDOUT в STDOUT

close (WORDSLIST) II die "couldn't close wordlist: $!"; }

format STDOUT ”

@<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<< $filename, $name, $word

Когда вызывается формат. Perl вычисляет выражения, имеющиеся в строке значений, и генерирует строку, которую передает в дескриптор файла stdout. Поскольку write вызывается один раз при каждом проходе цикла, мы получим ряд строк (под одной строке для каждого секретного слова); итоговый текст будет разбит на столбцы.

Гм-м. Мы забыли дать столбцам названия. Впрочем, это достаточно легко сделать. Нужно просто ввести формат начала страницы:

format STDOUT_TOP = Page @“ $%

Filename Name Word

Этот формат называется stdout_top; он будет использоваться при первом вызове формата stdout, а затем через каждые 60 строк, выведенных в stdout. заголовки столбцов позиционируются точно по столбцам формата ^tdout, поэтому все выглядит аккуратно.



В первой строке заголовка стоит неизменяемый текст (Page) и трехзначный определитель поля. Следующая строка — строка значений полей, в данном случае она содержит выражение. Это выражение является переменной $%*, в которой содержится число выведенных страниц.

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

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

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

Чтобы это опредиление заработало, попробуйте присоеденить его к преды-дущей программе. Perl отыщет формат начала страницы автоматически.

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

когда дойдем до форматов , в главе 11.



Как сделать старые списки слов более заметными



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

Вот как выглядит подпрограмма init_words () , модифицированная для выполнения такой операции:

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 the file so it gets noticed rename ($filename, "$filename.old") ||

die "can't rename $filename.old: $!";

}

}

Обратите внимание на новую часть оператора else в блоке проверки "возраста" файлов. Если файл не обновлять семь дней и более, функция rename переименовывает его. Эта функция принимает два параметра и переименовывает файл заданный первым параметром, присваивая ему имя, указанное во втором параметре.

В Perl имеет полный набор операций, необходимый для манипулирования файлами; все, что можно сделать с файлом в С-программе, можно сделать с ним в Perl.



52

Изучаем Perl

Ведение базы данных о времени правильного ввода секретных слов



Давайте проследим за тем, когда каждый пользователь последний раз правильно вводил свой вариант секретного слова. Очевидно, единственная структура данных, которая годится для этого — хеш. Например, оператор

$last_good{$name} == time;

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

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

Функция dbmopen* отображает хеш в файл (фактически в пару файлов), известный как DBM-файл. Она используется следующим образом:

dbmopen (%last_good,"lastdb",0666) ||

die "can't dbmopen lastdb: $!", $last_good{$name} = time;

dbmclose (%last_good) 11 die "can't dbrnclose lastdb: $!";

Первый оператор выполняет отображение, используя имена файлов lastdb. dir и lastdb.pag (это общепринятые имена для файлов lastdb, образующих DBM-файл). Если эти файлы необходимо создать (а это бывает при первой попытке их использования), то для них устанавливаются права доступа 0666**. Такой режим доступа означает, что все пользователи могут читать и осуществлять запись в эти файлы. Если вы работаете в UNIX-системе, то описание битов прав доступа к файлу вы найдете на man-странице chmoc/(2). В других системах chmod()



может работать так же, а может и не работать. Например, в MS- DOS для файлов не устанавливаются права доступа, тогда как в Windows NT — устанавливаются. Если уверенности нет, прочтите описание версии вашей системы.

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

* Можно также использовать низкоуровневую функцию tie с конкретной базой данных;

этот вариант подробно описан в главах 5 и 7 книги Programming Perl и на man-страницах perHfc(\) и AnyDMB_File (3).

** Фактически права доступа к этим файлам определяются в результате выполнения логической операции И над числом 0666 и текущим значением переменной umask вашего процесса.



7. Введение 53



механизму хеш физически существует и в периоды между вызовами данной программы.

Третий оператор отсоединяет хеш от DBM-файла, делая это практически так же, как операция close закрывает файл.

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

#!/usr/bin/peri dbmopen (%last_good, "lastdb", 0666) I I

die "can't dbmopen lastdb: $!";

foreach $name (sort keys (%last_good) ) {

$when = $last_good($name);

$hours = (timed - $when) / 3600; # вычислить истекшее время в часах

write;

}

format STDOUT =

User @<<<<<<: last correct guess was

@“<
hours ago.

$name, $hours

Здесь мы осуществляем несколько новых операций: выполняем цикл foreach, сортируем список и получаем значения ключей массива.

Сначала функция keys принимает имя хеша в качестве аргумента и возвращает список значений всех ключей этого хеша в произвольном порядке. Для хеша %words, описанного ранее, результат будет примерно таким:



fred, barney, betty, wiima, причем имена могут быть перечислены в произвольном порядке. В случае хеша %last_good результатом будет список всех пользователей, которые правильно ввели свои секретные слова.

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

Уже упомянутый Perl-оператор foreach очень похож на оператор foreach shell С. Он получает список значений и присваивает каждое из них по очереди какой-либо скалярной переменной (в нашем случае — переменной $name), выполняя для каждого значения по одному разу проход цикла (блок). Так, для пяти имен в списке %last_good мы делаем пять проходов, при этом каждый раз переменная $name имеет другое значение.

При выполнении цикла foreach происходит загрузка пары переменных, используемых затем в формате stdout, и вызывается этот формат. Обратите внимание: мы вычисляем "возраст" файла, вычитая записанное (в массиве) системное время из текущего времени (которое возвращает функция time), а затем деля результат на 3600 (чтобы преобразовать секунды в часы).



54

Изучаем Perl



В Perl используются также удобные способы создания и ведения текстовых баз данных (например, файла паролей) и баз данных с фиксированной длиной записей (таких, как база данных "последней регистрации", которую ведет программа login). Эти способы описаны в главе 17.



Окончательные варианты программ



Здесь вашему вниманию предлагаются программы, которые мы писалрт в этой главе, в окончательном виде. Сначала — программа-приветствие:

^!/usr/bin/peri &init_words ( ) ;

print "what is your name? " ;

$name = <STDIN>;

chomp ($name) ;



if

($name =~ /^randalVb/i) { # обратно на другой путь :-) print "Hello, Randal! 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;



} } dbmopen

(%last_good, "lastdb", 0666);

$last_good{$name} = time;

dbmclose (%last_good);



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 the file so it gets noticed

rename ($filename, "$filename.old") ||

die "can't rename $filename.old: $!";

}

close WORDSLIST;

}

}

sub good_word {

my($somename,$someguess) = @_; # перечислить параметры

$somename =~ s/\W.*//; # удалить все символы, стоящие после первого слова

$somename =~ tr/A-Z/a-z/; # перевести все символы в нижний регистр

if ($somename eq "randal") { # не нужно угадывать



1. Введение

55



return 1; # возвращаемое значение — true ) elsif (($wordsf$somename( II "groucho") eq $someguess) (

return 1; # возвращаемое значение — true } else {

open MAIL, "| mail YOUR_ADDRESS_HERE";

print MAIL "bad news: $somename guessed $someguess\n";

close MAIL;

return 0; # возвращаемое значение — false ”

Теперь — листер секретных слов:

#!/usr/bin/peri

while ($filename = <*.secret>) (

open (WORDSLIST, $filename) I I

die "can't open $filename: $!";

if (-M WORDSLIST < 7) {

while ($name = <WORDSLIST>) ( chomp ($name) ;

$word = <WORDSLIST> ;

chomp ($word) ;

write; # вызвать format STDOUT в STDOUT } } close (WORDSLIST) ;

format STDOUT = @<“““““““ @““<““ @““““<“

$ filename, $name, $word

format STDOUT_TOP =

Page @“

$%

Filename Name Word

И, наконец, программа выдачи времени последнего правильного ввода пароля:

#!/usr/bin/peri

dbmopen (%last good, "lastdb", 0666);

foreach $name (sort keys %last_good) (

$when = $last_good ($name);

$hours = (time - $when) / 3600; # вычислить истекшее время в часах

write;

}

format STDOUT =

User @<“““““: last correct guess was @“< hours ago.

$name, $hours

.



56 Изучаем Perl



Добавьте к этим программам списки секретных слов (файлы с именами что-то.secret,

находящиеся в текущем каталоге) и базу данных lastdb.dir и lastdb .рад, и у вас будет все, что нужно.


Содержание раздела