Книга может научить вас писать маленькие программы, решающие маленькие задачи-упражнения. Этим способом вы можете научиться синтаксису. Чтобы писать реальные программы, выполняющие реальные задачи, вам нужно научиться управлять кодом, написанным на вашем языке. Как вам организовать код? Как убедиться, что он работает? Как сделать его надёжным перед лицом ошибок? Что делает код кратким, ясным и поддерживаемым?
Современный Perl предоставляет множество инструментов и техник для написания реальных программ.
Тестирование — это процесс написания и запуска маленьких кусочков кода, помогающих удостовериться, что ваше программное обеспечение ведёт себя так, как задумано. Эффективное тестирование автоматизирует процесс, который вы уже выполняли бесчисленное количество раз: написать какое-то количество кода, запустить его, и убедиться, что он работает. Эта автоматизация чрезвычайно важна. Вместо того, чтобы доверять людям в том, что они буду идеально выполнять повторяющиеся ручные проверки, позвольте делать это компьютеру.
Perl 5 предоставляет отличные инструменты, помогающие вам писать правильные тесты.
Тестирование в Perl начинается с базового модуля Test::More
и его функции ok()
. ok()
принимает два параметра, булево значение и строку, описывающую назначение теста:
ok( 1, 'the number one should be true' );
ok( 0, '... and zero should not' );
ok( '', 'the empty string should be false' );
ok( '!', '... and a non-empty string should not' );
done_testing();
Любое условие в вашей программе, которое вы можете протестировать, так или иначе может стать двоичным значением. Каждая тестовая проверка — это простой вопрос, на который можно ответить «да» или «нет»: работает ли этот маленький кусочек кода так, как я ожидаю? Сложные программы могут иметь тысячи отдельных условий, и, в общем, чем мельче разбиение, тем лучше. Изолирование конкретных поведений в отдельные проверки позволяет вам свести к минимуму баги и недопонимания, особенно когда вы будете модифицировать код в будущем.
Функция done_testing()
сообщает Test::More
, что программа успешно выполнила все ожидаемые тестовые проверки. Если программа столкнулась с исключением времени выполнения, или по другим причинам неожиданно завершилась прежде, чем произошёл вызов done_testing()
, тестовый фреймворк уведомит вас, что что-то пошло не так. Без механизма, подобного done_testing()
, как бы вы узнали? Правда, этот пример кода слишком прост, чтобы сломаться, но код, который слишком прост, чтобы сломаться, ломается гораздо чаще, чем кто-либо мог ожидать.
Test::More
также позволяет использовать план тестов для указания числа отдельных проверок, которые вы планируете выполнить:use Test::More tests => 4; ok( 1, 'the number one should be true' ); ok( 0, '... and zero should not' ); ok( '', 'the empty string should be false' ); ok( '!', '... and a non-empty string should not' );
Аргумент
test
, указываемый дляTest::More
, устанавливает план тестов для программы. Это предохранительная сетка. Если выполнено меньше, чем четыре теста, что-то пошло не так. Если выполнено больше, чем четыре теста, что-то пошло не так.
Результирующая программа теперь — полноценная программа на Perl 5, которая генерирует следующий вывод:
ok 1 - the number one should be true
not ok 2 - ... and zero should not
# Failed test '... and zero should not'
# at truth_values.t line 4.
not ok 3 - the empty string should be false
# Failed test 'the empty string should be false'
# at truth_values.t line 5.
ok 4 - ... and a non-empty string should not
1..4
# Looks like you failed 2 tests of 4.
Этот формат соответствует стандартному выводу тестов, называемому TAP, Test Anything Protocol, Протокол тестирования чего угодно (http://testanything.org/). Проваленные TAP-тесты выдают диагностические сообщения для помощи отладке.
Вывод тестового файла, содержащего множество проверок (особенно множество проваленных проверок) может быть многословным. В большинстве случаев вам достаточно знать либо что всё прошло, либо конкретику имеющихся провалов. Базовый модуль Test::Harness
интерпретирует TAP, и связанная с ним программа prove
запускает тесты и отображает только наиболее релевантную информацию:
$ prove truth_values.t
truth_values.t .. 1/?
# Failed test '... and zero should not'
# at truth_values.t line 4.
# Failed test 'the empty string should be false'
# at truth_values.t line 5.
# Looks like you failed 2 tests of 4.
truth_values.t .. Dubious, test returned 2
(wstat 512, 0x200)
Failed 2/4 subtests
Test Summary Report
-------------------
truth_values.t (Wstat: 512 Tests: 4 Failed: 2)
Failed tests: 2-3
Тут много вывода, отображающего то, что и так очевидно: второй и третий тесты проваливаются, потому что ноль и пустая строка являются ложными значениями. Эти провалы легко исправить, инвертировав смысл условий с помощью булева приведения типов (Булево приведение типов):
ok( ! 0, '... and zero should not' );
ok( ! '', 'the empty string should be false' );
После этих двух изменений prove
выводит следующее:
$ prove truth_values.t
truth_values.t .. ok
All tests successful.
Смотрите в
perldoc prove
полезные опции тестов, такие как параллельный запуск тестов (-j
), автоматическое добавление lib/ во включаемые пути Perl (-l
), рекурсивный запуск всех файлов тестов, найденных в t/ (-r t
) и запуск медленных тестов сначала (--state=slow,save
).
Псевдоним оболочки bash
proveall
может оказаться полезным:alias proveall='prove -j9 --state=slow,save -lr t'
Хотя сердце всех автоматизированных тестов — булево условие «истинно или ложно?», сведение всего к этому булеву условию утомительно и даёт небольшие диагностические возможности. Test::More
предоставляет несколько других удобных функций проверок.
Функция is()
сравнивает два значения, используя оператор eq
. Если значения равны, тест проходит. В противном случае, тест проваливается с диагностическим сообщением:
is( 4, 2 + 2, 'addition should work' );
is( 'pancake', 100, 'pancakes are numeric' );
Как вы можете предположить, первый тест проходит, а второй — проваливается.
t/is_tests.t .. 1/2
# Failed test 'pancakes are numeric'
# at t/is_tests.t line 8.
# got: 'pancake'
# expected: '100'
# Looks like you failed 1 test of 2.
Тогда как ok()
предоставляет только номер строки провалившегося теста, is()
отображает ожидаемое и полученное значение.
is()
налагает неявный скалярный контекст на свои значения (Прототипы). Это означает, например, что вы можете проверить количество элементов в массиве без необходимости явного вычисления массива в скалярном контексте:
my @cousins = qw( Rick Kristen Alex
Kaycee Eric Corey );
is( @cousins, 6, 'I should have only six cousins' );
…хотя некоторые для ясности предпочитают писать scalar @cousins
.
Соответствующая функция Test::More
isnt()
сравнивает два значения, используя оператор ne
, и проходит, если они не равны. Она также налагает скалярный контекст на свои операнды.
И is()
, и isnt()
используют строковое сравнение с помощью операторов Perl 5 eq
и ne
. Это почти всегда то, что нужно, но для сложных значений, таких как объекты с перегрузкой (Перегрузка) или двойные переменные (Двойные переменные), вы можете предпочесть явное тестирование сравнения. Функция cmp_ok()
позволяет вам указать свой собственный оператор сравнения:
cmp_ok( 100, $cur_balance, '<=',
'I should have at least $100' );
cmp_ok( $monkey, $ape, '==',
'Simian numifications should agree' );
Классы и объекты предоставляют свои собственные интересные способы взаимодействия с тестами. Протестировать, что класс или объект расширяет другой класс (Наследование), можно с помощью isa_ok()
:
my $chimpzilla = RobotMonkey->new();
isa_ok( $chimpzilla, 'Robot' );
isa_ok( $chimpzilla, 'Monkey' );
isa_ok()
предоставляет свои собственные диагностические сообщенния при провалах.
can_ok()
проверяет, что класс или объект может выполнить запрошенный метод (или методы):
can_ok( $chimpzilla, 'eat_banana' );
can_ok( $chimpzilla, 'transform', 'destroy_tokyo' );
Функция is_deeply()
сравнивает две ссылки, чтобы убедиться, что их содержимое идентично:
use Clone;
my $numbers = [ 4, 8, 15, 16, 23, 42 ];
my $clonenums = Clone::clone( $numbers );
is_deeply( $numbers, $clonenums,
'clone() should produce identical items' );
Если сравнение проваливается, Test::More
сделает всё, что в его силах, чтобы предоставить разумную диагностику, указывающую позицию первого несоответствия между структурами. Для более настраиваемых тестов смотрите CPAN-модули Test::Differences
и Test::Deep
.
В Test::More
есть ещё несколько тестовых функций, но эти наиболее полезны.
CPAN-дистрибутивы должны включать директорию t/, содержающую один или более файлов тестов, имеющих расширение .t. По умолчанию, когда вы собираете дистрибутив с помощью Module::Build
или ExtUtils::MakeMaker
, этап тестирования запускает все файлы t/*.t, суммирует их вывод, и проходит успешно или проваливается в зависимости от результатов набора тестов как целого. Нет конкретных руководств, как управлять содержимым отдельных файлов .t, хотя наиболее популярны две стратегии:
Смешанный подход наиболее гибок; один тест может проверять, что все ваши модули компилируются, тогда как другие тесты будут проверять, что каждый модуль ведёт себя так, как ожидается. Когда дистрибутив разрастается, полезность управления тестами в привязке к функционалу становится более привлекательной; большие тестовые файлы сложнее поддерживать.
Кроме того, раздельные файлы тестов могут ускорить разработку. Если вы добавляете способность дышать огнём к вашему классу RobotMonkey
, вы, вероятно, захотите запускать только тестовый файл t/breathe_fire.t. Когда же вы добъётесь удовлетворительной работы функционала, запустите весь набор тестов для проверки того, что локальные изменения не имеют непреднамеренных глобальных эффектов.
Test::More
полагается на тестовый бэкенд, известный как Test::Builder
. Этот модуль управляет планом тестов и приводит вывод тестов в TAP. Такой дизайн позволяет множеству тестовых модулей использовать один и тот же бэкенд Test::Builder
. Вследствие этого, на CPAN доступны сотни тестовых модулей — и все они могут работать вместе в одной и той же программе.
Test::Fatal
помогает протестировать, что ваш код выбрасывает (и не выбрасывает) исключения соответствующим образом. Вам также может встретиться Test::Exception
.Test::MockObject
и Test::MockModule
позволяют вам тестировать сложные интерфейсы, имитируя (mocking) их (эмулируя, но производя другие результаты).Test::WWW::Mechanize
помогает тестировать веб-приложения, тогда как Plack::Test
, Plack::Test::Agent
и подкласс Test::WWW::Mechanize::PSGI
могут делать это, не используя внешний живой веб-сервер.Test::Database
предоставляет функции для тестирования правильного и неправильного использования баз данных. DBICx::TestDatabase
помогает тестировать схемы, сгенерированные с помощью DBIx::Class
.Test::Class
предлагает альтернативный механизм организации наборов тестов. Он позволяет вам создавать классы, в которых конкретные методы группируют тесты. Вы можете наследовать от этих тестовых классов, так же как ваши классы кода наследуют друг от друга. Это превосходный способ уменьшить дублирование в тестовых наборах. См. превосходную серию статей по Test::Class
Кёртиса Пое (Curtis Poe) (footnote: http://www.modernperlbooks.com/mt/2009/03/organizing-test-suites-with-testclass.html). Более новый дистрибутив Test::Routine
предлагает аналогичные возможности посредством использования Moose (Moose).Test::Differences
тестирует строки и структуры данных на идентичность и отображает любые найденные различия в своей диагностике. Test::LongString
добавляет похожие проверки.Test::Deep
тестирует идентичность вложенных структур данных (Вложенные структуры данных).Devel::Cover
анализирует выполнение ваших наборов тестов и даёт отчёт о количестве кода, которое тесты действительно выполняют. В общем, чем больше покрытие, тем лучше — хотя стопроцентное покрытие не всегда возможно, 95% гораздо лучше, чем 80%.См. проект Perl QA (http://qa.perl.org/) для большей информации о тестировании в Perl.
Хотя существует более чем один способ написать работающую программу на Perl 5, некоторые из этих способов могут быть путающими, неясными и даже некорректными в некоторых хитрых ситуациях. Система опциональных предупреждений в Perl 5 может помочь вам распознавать такие ситуации и избегать их.
Используйте встроенную функцию warn
чтобы выдать предупреждение:
warn 'Something went wrong!';
warn
выводит список значений в файловый дескриптор STDERR (Ввод и вывод). Perl будет добавлять имя файла и номер строки, на которой произошёл вызов warn
, если последний элемент списка не заканчивается переводом строки.
Встроенный модуль Carp
предлагает другие механизмы для генерации предупреждений. Его функция carp()
выводит предупреждение с точки зрения вызывающего кода. Если есть такая валидация параметров функции:
use Carp 'carp';
sub only_two_arguments
{
my ($lop, $rop) = @_;
carp( 'Too many arguments provided' ) if @_ > 2;
...
}
…предупреждение об арности (Арность) будет включать имя файла и номер строки вызывающего кода, а не only_two_arguments()
. cluck()
из того же модуля Carp
аналогично генерирует обратную трассировку всех вызовов функций до текущей функции.
Многословный режим Carp
добавляет обратную трассировку ко всем предупреждениям, генерируемым carp()
и croak()
(Оповещение об ошибках) по всей программе:
$ perl -MCarp=verbose my_prog.pl
Используйте Carp
при написании модулей (Модули) вместо warn
или die
.
В старом коде вы можете встретить аргумент командной строки -w
. Он включает предупреждения по всей программе, даже во внешних модулях, написанных и поддерживаемых другими людьми. Тут всё или ничего, хотя это и может быть полезным, если у вас есть необходимые средства для устранения и предупреждений, и потенциальных предупреждений по всей кодовой базе.
Современный подход — использование прагмы warnings
(footnote: …или эквивалента, такого как use Modern::Perl;
.). Это включает предупреждения в лексической области видимости и указывает, что автор кода не подразумевает, что в нормальных условиях он будет генерировать предупреждения.
Флаг -W
включает предупреждения по всей программе в одностороннем порядке, независимо от лексического включения или выключения с помощью прагмы warnings
. Флаг -X
отключает предупреждения по всей программе в одностороннем порядке. Ни один из них не имеет широкого распространения.
Все эти флаги, -w
, -W
и -X
, воздействуют на глобальную переменную $^W
. Код, написанный до появления прагмы warnings
(Perl 5.6.0 весной 2000 года) может локализовать $^W
, чтобы подавить некоторые предупреждения в пределах заданной области видимости.
Чтобы отключить выбранные предупреждения в области видимости, используйте no warnings;
со списком аргументов. Опускание списка аргументов отключает все предупреждения в этой области видимости.
perldoc perllexwarn
приводит список всех категорий предупреждений, которые ваша версия Perl 5 воспринимает с прагмой warnings
. Большая часть из них представляет действительно интересные условия, но некоторые могут быть явно бесполезны в ваших конкретных обстоятельствах. Например, предупреждение recursion
будет происходить, если Perl обнаружил, что функция вызвала себя более чем сто раз. Если вы уверены в своей способности написать условия завершения рекурсии, вы можете отключить это предупреждение в области видимости рекурсии (хотя хвостовые вызовы, возможно, подойдут лучше; Хвостовые вызовы).
Если вы генерируете код (Кодогенерация) или локально переопределяете символы, вам может понадобиться отключить предупреждение redefine
.
Некоторые опытные Perl-хакеры отключают предупреждение об неинициализированных значениях (uninitialized
) в коде обработки строк, который конкатенирует значения из множества источников. Тщательная инициализация переменных помогает избежать необходимости отключения предупреждения, но локальный стиль и лаконичность могут сделать использование этого предупреждения спорным.
Если ваш проект считает предупреждения такими же обременительными, как ошибки, вы можете сделать их лексически фатальными. Так можно повысить все предупреждения до исключений:
use warnings FATAL => 'all';
Также вы можете сделать фатальными конкретные категории предупреждений, как, например, использование нерекомендуемых конструкций:
use warnings FATAL => 'deprecated';
С надлежащей дисциплиной, это может привести к очень надёжному код — но будьте осторожны. Многие предупреждения вызываются условиями времени исполнения. Если ваш набор тестов не сможет распознать все предупреждения, с которыми вы можете столкнуться, ваша программа может завершиться при выполнении из-за непойманного исключения.
Так же как вы ловите исключения, так же вы можете ловить и предупреждения. Переменная %SIG
(footnote: См. perldoc perlvar
.) содержит обработчики внеполосных сигналов, генерируемых Perl или вашей операционной системой. Чтобы поймать предупреждение, присвойте $SIG{__WARN__}
ссылку на функцию:
{
my $warning;
local $SIG{__WARN__} = sub { $warning .= shift };
# сделать что-нибудь рискованное
...
say "Caught warning:\n$warning" if $warning;
}
Внутри обработчика предупреждений, первым аргументом будет сообщение предупреждения. Надо сказать, эта техника менее удобна, чем отключение предупреждений лексически — но это может оказаться полезным в тестовых модулях, таких как Test::Warnings
из CPAN, где важен фактический текст предупреждения.
Имейте ввиду, что переменная %SIG
глобальна. Локализуйте её в наименьшей возможной области видимости с помощью local
, но понимайте, что это всё ещё глобальная переменная.
Прагма warnings::register
позволяет вам создавать свои собственные лексические предупреждения, чтобы пользователи вашего кода могли включать и отключать лексические предупреждения. Используйте (use
) прагму warnings::register
в модуле:
package Scary::Monkey;
use warnings::register;
Это создаст новую категорию предупреждений, названную по имени пакета Scary::Monkey
. Включите эти предупреждения с помощью use warnings 'Scary::Monkey'
и отключите с помощью no warnings 'Scary::Monkey'
.
Используйте warnings::enabled()
чтобы проверить, что категория предупреждения включена в вызывающей лексической области. Используйте warnings::warnif()
для генерации предупреждений, только если предупреждения действуют. Например, чтобы выдать предупреждение в категории deprecated
:
package Scary::Monkey;
use warnings::register;
sub import
{
warnings::warnif( 'deprecated',
'empty imports from ' . __PACKAGE__ .
' are now deprecated' )
unless @_;
}
См. perldoc perllexwarn
для больших подробностей.
Большинству программ нужно как-то взаимодействовать с реальным миром. Большинству программ нужно писать, читать или другими способами манипулировать с файлами. Происхождение Perl как инструмента для системных администраторов привело к созданию языка, отлично подходящего для обработки текста.
Дескриптор файла представляет текущее состояние одного конкретного канала ввода или вывода. В каждой программе на Perl доступны три стандартных дескриптора файлов, STDIN
(ввод программы), STDOUT
(вывод программы) и STDERR
(вывод ошибок программы). По умолчанию всё, что вы выводите с помощью print
или say
, отправляется на STDOUT
, тогда как ошибки, предупреждения и всё, что вы выводите с помощью warn()
, отправляется на STDERR
. Такое разделение вывода позволяет вам перенаправить полезный вывод и ошибки в два разных места — например, в выходной файл и лог ошибок.
Используйте встроенную функцию open
для получения дескриптора файла. Так файл открывается для чтения:
open my $fh, '<', 'filename'
or die "Cannot read '$filename': $!\n";
Первый операнд — это лексическая переменная, которая будет содержать результирующий дескриптор файла. Второй оператор — режим файла, определяющий тип операций с дескриптором файла. Последний операнд — это имя файла. Если open
провалится, оператор die
выбросит исключение, с содержимым $!
, описывающим причину, по которой открытие файла не удалось.
Кроме этого, вы можете открывать файлы для записи, добавления, чтения и записи и т. д. Здесь перечислены наиболее важные режимы файлов:
Table: Режимы файлов
Символ | Описание |
---|---|
< |
Открыть для чтения |
> |
Открыть для записи, перезаписывая существующее содержимое, если файл существует, и создавая новый файл в противном случае. |
>> |
Открыть для записи, добавляя к существующему контенту или создавая новый файл. |
+< |
Открыть для чтения и записи. |
-| |
Открыть канал к внешнему процессу для чтения. |
|- |
Открыть канал к внешнему процессу для записи. |
Вы даже можете создавать дескрипторы файлов, который могут читать или писать в простые скаляры Perl, используя любой существующий режим файла:
open my $read_fh, '<', \$fake_input;
open my $write_fh, '>', \$captured_output;
do_something_awesome( $read_fh, $write_fh );
Все примеры в этом разделе имеют включенный режим use autodie;
, а потому могут безопасно игнорировать обработку ошибок. Если вы выбрали не использовать autodie
, хорошо — но не забудьте проверить возвращаемые значения всех системных вызовов, чтобы соответствующим образом обработать ошибки.
perldoc perlopentut
предлагает гораздо больше деталей о более экзотических применениях open
, включая её способность запускать и контролировать другие процессы, а также использование sysopen
для более точного контроля ввода и вывода. perldoc perlfaq5
приводит рабочий код для многих задач ввода/вывода.
open
Старый код часто использует двухаргументную форму open()
, которая объединяет режим файла с именем открываемого файла:
open my $fh, "> $some_file"
or die "Cannot write to '$some_file': $!\n";
Соответственно, Perl должен извлечь режим файла из имени файла, и здесь лежат потенциальные проблемы. Каждый раз, когда Perl приходится догадываться, что вы имеете ввиду, вы получаете риск того, что он может догадаться неверно. Хуже того, если $some_file
приходит из недоверенного пользовательского ввода, у вас возникает потенциальная проблема безопасности, так как любые неожиданные символы могут изменить поведение вашей программы.
Трёхаргументный вызов open()
— более безопасная замена для этого кода.
Дескриптор файла DATA
, специальная глобальная переменная пакета, представляет текущий файл. Когда Perl заканчивает компиляцию файла, он оставляет DATA
открытым на конце компилируемой части, если файл содержит секцию __DATA__
или __END__
. Любой текст, находящийся за пределами этого токена, доступен для чтения через DATA
. Это полезно для коротких, самодостаточных программ. См. perldoc perldata
для дальнейших подробностей.
Имея открытый для ввода дескриптор файла, прочитать из него можно с помощью встроенной функции readline
, также записываемой как <>
. Распространённая идиома читает по строке за раз в цикле while()
:
open my $fh, '<', 'some_file';
while (<$fh>)
{
chomp;
say "Read a line '$_'";
}
В скалярном контексте readline
итерирует по строкам файла, пока не достигнет его конца (eof()
). Каждая итерация возвращает следующую строку. После достижения конца файла, каждая итерация возвращает undef
. Эта идиома с while
явно проверяет определённость переменной, используемой для итерации, так что только достижение конца файла завершит цикл. Другими словами, это сокращение для следующего:
open my $fh, '<', 'some_file';
while (defined($_ = <$fh>))
{
chomp;
say "Read a line '$_'";
last if eof $fh;
}
for
налагает списочный контекст на свой операнд. В случае readline
Perl прочитает весь файл, прежде чем обработать какую-либо его часть. while
выполняет итерацию и читает по строке за раз. Когда использование памяти имеет значение, используйте while
.
Каждая строка, прочитанная с помощью readline
, содержит символ или символы, отмечающие конец строки. В большинстве случаев это платформозависимая последовательность, состоящая из перевода строки (\n
), возврата каретки (\r
), или сочетания их обоих (\r\n
). Используйте chomp
для их удаления.
Наиболее ясный способ прочитать файл строка за строкой в Perl 5 такой:
open my $fh, '<', $filename;
while (my $line = <$fh>)
{
chomp $line;
...
}
Perl по умолчанию работает с файлами в текстовом режиме. Если вы читатете двоичные данные, такие как медиафайл или сжатый файл — используйте binmode
прежде чем выполнять какой-либо ввод/вывод. Это укажет Perl обрабатывать данные файла как чистые данные, никаким образом их не модифицируя (footnote: Модификации включают перевод \n
в платформозависимую последовательность новой строки.). Хотя на Unix-подобных платформах binmode
может быть не всегда обязателен, переносимые программы играют наверняка (Юникод и строки).
Имея открытый для вывода дескриптор файла, осуществляйте вывод в него с помощью print
или say
:
open my $out_fh, '>', 'output_file.txt';
print $out_fh "Here's a line of text\n";
say $out_fh "... and here's another";
Обратите внимание на отсутствие запятой между дескриптором файла и следующим операндом.
Perl Best Practices Демьена Конвея (Damian Conway) рекомендует придерживаться привычки заключать дескриптор файла в фигурные скобки. Это необходимо для устранения неоднозначности парсинга дескриптора файла, содержащегося в агрегатной переменной, и ничему не повредит в более простых случаях.
И print
, и say
принимают список операндов. Perl 5 использует магическую глобальную переменную $,
как разделитель значений списка. Perl также использует любое значение $\
как последний аргумент print
или say
. Так что эти две строки кода дают один и тот же результат:
my @princes = qw( Corwin Eric Random ... );
print @princes;
print join( $,, @princes ) . $\;
Когда вы закончили работать с файлом, закройте его дескриптор явно с помощью close
, или дайте ему выйти из области видимости. Perl закроет его для вас. Преимущество явного вызова close
в том, что вы можете проверить — и восстановиться после них — специфические ошибки, такие как недостаток места на устройстве хранения или разорванное сетевое соединение.
Как обычно, autodie
выполнит эти проверки за вас:
use autodie;
open my $fh, '>', $file;
...
close $fh;
Для каждой прочитанной строки Perl 5 инкрементирует значение переменной $.
, которая служит счётчиком строк.
readline
использует текущее содержимое $/
как последовательность завершения строки. По умолчанию значение этой переменной содержит последовательность символов завершения строки, наиболее подходяющую для текстовых файлов на текущей платформе. По правде говоря, слово строка — неправильное название. Вы можете установить в $/
любую последовательность символов (footnote: …но, к сожалению, не регулярное выражение. Perl 5 этого не поддерживает.). Это полезно для сильно структурированных данных, в которых вы хотите считывать запись за раз. Имея файл с записями, разделёнными двумя пустыми строками, установите $/
в \n\n
для чтения по записи за раз. Применение chomp
к прочитанной из файла записи удалит последовательность двойного перевода строки.
Perl по умолчанию буферизует свой вывод, выполняя ввод/вывод только когда ожидающий вывод превышает порог по размеру. Это позволяет Perl группировать дорогие операции ввода/вывода вместо того, чтобы всегда записывать очень малые количества данных. Однако иногда вам нужно отправить данные как только вы их получаете, не ожидая буферизации — особенно если вы пишете фильтр для командной строки, соединённый с другими программами, или строко-ориентированный сетевой сервис.
Переменная $|
контролирует буферизацию на текущем активном файловом дескрипторе вывода. При установке ненулевого значения Perl будет сбрасывать вывод после каждой записи в дескриптор файла. При установке нулевого значения Perl будет использовать свою стратегию буферизации по умолчанию.
Для файлов по умолчанию действует стратегия полной буферизации. STDOUT
, присоединённый к активному терминалу &mdasy; но не к другой программе — использует стратегию построчной буферизации, при которой Perl будет сбрасывать STDOUT
каждый раз, как встретит в выводе перевод строки.
Вместо глобальной переменной, используйте на лексическом дескрипторе файла метод autoflush()
:
open my $fh, '>', 'pecan.log';
$fh->autoflush( 1 );
...
Начиная с Perl 5.14, вы можете использовать на дескрипторе файла любые методы, предоставляемые IO::File
. Вам не требуется явно загружать IO::File
. В Perl 5.12 вы должны сами загрузить IO::File
. В Perl 5.10 и раньше вы должны вместо этого загрузить FileHandle
.
Методы IO::File
input_line_number()
и input_record_separator()
обеспечивают пофайловый доступ к тому, для чего обычно вам пришлось бы использовать глобальные $.
и $/
. Смотрите документацию по IO::File
, IO::Handle
и IO::Seekable
для большей информации.
Работа с директориями похожа на работу с файлами, за исключением того, что вы не можете писать в директории (footnote: Вместо этого вы сохраняете, перемещаете, переименовываете и удаляете файлы.). Открыть дескриптор директории можно с помощью встроенной функции opendir
:
opendir my $dirh, '/home/monkeytamer/tasks/';
Встроенная функция readdir
читает из директории. Как и с readline
, вы можете выполнить итерацию по содержимому директории по одному элементу за раз, или вы можете присвоить их списку сразу:
# итерация
while (my $file = readdir $dirh)
{
...
}
# разглаживание в список
my @files = readdir $otherdirh;
Perl 5.12 добавил возможность, благодаря которой readdir
в while
устанавливает $_
:
use 5.012;
opendir my $dirh, 'tasks/circus/';
while (readdir $dirh)
{
next if /^\./;
say "Found a task $_!";
}
Любопытное регулярное выражение в этом примере пропускает так называемые скрытые файлы в Unix и Unix-подобных системах, где ведущая точка предотвращает появление их в листинге директории по умолчанию. Оно также пропускает два специальных файла .
и ..
, которые представляют текущую директорию и родительскую директорию соответственно.
Имена, возвращаемые из readdir
, относительны к самой директории. Другими словами, если директория tasks/ содержит три файла, eat, drink и be_monkey, readdir
вернёт eat
, drink
и be_monkey
, а не tasks/eat, tasks/drink и task/be_monkey. Напротив, абсолютный путь — это путь, полностью определённый в файловой системе.
Закройте дескриптор директории, позволив ему выйти за пределы области видимости, или с помощью встроенной функции closedir
.
Perl 5 предлагает взгляд в стиле Unix на вашу файловую систему и будет интерпретировать пути в стиле Unix соответствующим образом для вашей операционной системы и файловой системы. Другими словами, если вы используете Microsoft Windows, вы можете использовать путь C:/My Documents/Robots/Bender/ так же легко, как C:\My Documents\Robots\Caprica Six\.
Хотя операциями Perl и управляет семантика файлов Unix, кросс-платформенные манипуляции файлами намного легче осуществлять с помощью модуля. Семейство базовых модулей File::Spec
предоставляет абстракции, позволяющие вам манипулировать путями к файлам безопасным и переносимым образом. Эти модули уважаемы и легко понимаемы, хотя и громоздки.
Дистрибутив Path::Class
из CPAN предоставляет более приятный интерфейс. Используйте фукнцию dir()
для создания объекта, представляющего директорию, и функцию file()
для создания объекта, представляющего файл:
use Path::Class;
my $meals = dir( 'tasks', 'cooking' );
my $file = file( 'tasks', 'health', 'robots.txt' );
Вы можете получить объекты файлов из директорий и т. д.:
my $lunch = $meals->file( 'veggie_calzone' );
my $robots_dir = $robot_list->dir();
Вы даже можете отрыть дескрипторы файлов для директорий и файлов:
my $dir_fh = $dir->open();
my $robots_fh = $robot_list->open( 'r' )
or die "Open failed: $!";
И Path::Class::Dir
, и Path::Class::File
предлагают и другие полезные поведения — хотя имейте ввиду, что если вы используете какой-нибудь объект Path::Class
с другим кодом на Perl 5, таким как оператор или функция, ожидающая строку, содержащую путь к файлу, вам придётся самостоятельно преобразовать объект в строку. Это постоянное, но небольшое раздражение.
my $contents = read_from_filename( "$lunch" );
Помимо чтения и записи файлов, вы также можете манипулировать ими, как делали бы это напрямую из командной строки или файлового менеджера. Операторы проверки файлов, все вместе называемые операторы -X
, из-за того, что все они представляют собой дефис и одну букву, проверяют атрибуты файла и директории. Например, так можно проверить, что файл существует:
say 'Present!' if -e $filename;
Оператор -e
имеет один операнд, имя файла или дескриптор файла или директории. Если файл существует, выражение вернёт истинное значение. perldoc -f -X
содержит список всех остальных проверок файлов; наиболее популярны следующие:
-f
, возвращающий истинное значение, если его операнд — простой файл;
-d
, возвращающий истинное значение, если его операнд — директория;
-r
, возвращающий истинное значение, если настройка доступа к файлу, указанному как его операнд, позволяет чтение текущим пользователем;
-s
, возвращающий истинное значение, если его операнд — непустой файл.
Начиная с Perl 5.10.1, вы можете искать документацию по любому из этих операторов, например, как perldoc -f -r
.
Встроенная функция rename
может переименовать файл или переместить его между директориями. Они принимает два операнда, старое и новое имя файла:
rename 'death_star.txt', 'carbon_sink.txt';
# или если вы стильный:
rename 'death_star.txt' => 'carbon_sink.txt';
Встроенной функции для копирования файла нет, но базовый модуль File::Copy
предоставляет как фукнцию copy()
, так и move()
. Используйте встроенную функцию unlink
для удаления одного или нескольких файлов. (Встроенная функция delete
удаляет элемент из хеша, а не файл из файловой системы.) Все эти функции возвращают истинные значения в случае успеха и устанавливают $!
при ошибке.
Path::Class
предоставляет удобные методы для проверки некоторых атрибутов файла, а также для полного удаления файла, в кросс-платформенной манере.
Perl отслеживает свою текущую рабочую директорию. По умолчанию, это активная директория, из которой запущена программа. Функция cwd()
базового модуля Cwd
возвращает имя текущей рабочей директории. Встроенная функция chdir
пытается сменить текущую рабочую директорию. Работа из корректной директории важна для работы с файлами с относительными путями.
Многие считают CPAN (CPAN) самой мощной возможностью Perl 5. CPAN, по сути, представляет собой систему для поиска и установки модулей. Модуль — это пакет, содержащийся в своём собственном файле, который можно загружать с помощью use
или require
. Модуль должен быть валидным кодом на Perl 5. Он должен завершаться выражением, возвращающим истинное значение, чтобы парсер Perl 5 мог понять, что он успешно загрузил и скомпилировал модуль. Других требований нет, только твёрдые соглашения.
Когда вы загружаете модуль, Perl разбивает имя пакета по двойным двоеточиям (::
) и превращает компоненты имени пакета в имя файла. На практике, use StrangeMonkey;
заставляет Perl искать файл с именем StrangeMonkey.pm во всех директориях, входящих в @INC
, по порядку, пока он его не найдёт или не израсходует список.
Аналогично, use StrangeMonkey::Persistence;
заставляет Perl искать файл с именем Persistence.pm в каждой директории с именем StrangeMonkey/, находящейся в любой из директорий, входящих в @INC
, и т. д. use StrangeMonkey::UI::Mobile;
заставляет Perl искать относительный файловый путь StrangeMonkey/UI/Mobile.pm в каждой директории в @INC
.
Результирующий файл может содержать или не содержать объявление пакета, совпадающее с именем файла — такого технического требования нет — но вопросы поддержки рекомендуют это соглашение.
perldoc -l Module::Name
выведет полный путь к соответствующему файлу .pm, в случае, если документация к этому модулю существует в файле .pm. perldoc -lm Module::Name
выведет полный путь к файлу .pm, независимо от существования параллельного файла .pod. perldoc -m Module::Name
отобразит содержимое файла .pm.
Когда вы загружаете модуль с помощью use
, Perl загружает его с диска, а затем вызывает его метод import()
, передавая все указанные вами аргументы. По соглашению метод import()
модуля принимает список имён и экспортирует функции и другие символы в вызывающее пространство имён. Это всего лишь соглашение; модуль может отказаться предоставлять import()
, или его import()
может выполнять другие действия. Прагмы (Прагмы), такие как strict
, используют аргументы для изменения поведения вызывающей лексической области видимости вместо экспорта символов:
use strict;
# …вызывает strict->import()
use CGI ':standard';
# …вызывает CGI->import( ':standard' )
use feature qw( say switch );
# …вызывает feature->import( qw( say switch ) )
Встроенная директива no
вызывает метод unimport()
модуля, если он существует, передавая любые аргументы. Это наиболее распространено с прагмами, использование которых модифицирует поведение через import()
:
use strict;
# запрещены символьные ссылки или голые слова
# требуется объявление переменных
{
no strict 'refs';
# символьные ссылки разрешены
# ограничения 'subs' и 'vars' всё ещё активны
}
И use
, и no
действуют во время компиляции, так что:
use Module::Name qw( list of arguments );
…это то же самое, что:
BEGIN
{
require 'Module/Name.pm';
Module::Name->import( qw( list of arguments ) );
}
Аналогично:
no Module::Name qw( list of arguments );
…это то же самое, что:
BEGIN
{
require 'Module/Name.pm';
Module::Name->unimport(qw( list of arguments ));
}
…включая require
модуля.
Если import()
или unimport()
не существует в модуле, Perl не будет выдавать сообщение об ошибке. Они совершенно опциональны.
Вы можете вызвать import()
и unimport()
напрямую, хотя за пределами блока BEGIN
делать это мало смысла; после завершения компиляции действие import()
или unimport()
может не иметь особого эффекта.
В Perl 5 use
и require
регистрозависимы, хотя, в то время как Perl понимает разницу между strict
и Strict
, ваше сочетание операционной системы и файловой системы может не знать. Если бы вы написали use Strict;
, Perl не нашёл бы strict.pm в регистрозависимых файловых системах. В регистронезависимых файловых системах Perl охотно загрузит Strict.pm, но когда он попытается вызвать Strict->import()
, ничего не случится. (strict.pm объявляет пакет с именем strict
.)
Переносимые программы придерживаются строгих правил относительно этого случая, даже если и не обязаны.
Модуль может сделать определённые глобальные символы доступными другим пакетам посредством процесса, известного как экспорт — процесс, инициируемый вызовом import()
, явным или неявным.
Базовый модуль Exporter
предоставляет стандартный механизм экспорта символов из модуля. Exporter
полагается на присутствие глобальных переменных пакета — в частности, @EXPORT_OK
и @EXPORT
— которые содержат список символов, экспортируемых по запросу.
Рассмотрим модуль StrangeMonkey::Utilities
, предоставляющий несколько автономных функций, применимых по всей системе:
package StrangeMonkey::Utilities;
use Exporter 'import';
our @EXPORT_OK = qw( round translate screech );
...
Любой другой код теперь может использовать этот модуль и, опционально, импортировать любую или все три экспортируемые функции. Также вы можете экспортировать переменные:
push @EXPORT_OK, qw( $spider $saki $squirrel );
Экспортируйте символы по умолчанию, перечислив их в @EXPORT
вместо @EXPORT_OK
:
our @EXPORT = qw( monkey_dance monkey_sleep );
…так что любая конструкция use StrangeMonkey::Utilities;
будет импортировать обе функции. Имейте ввиду, что указание символов для импорта не импортирует символы по умолчанию, вы получите только то, что запросили. Чтобы загрузить модуль, не импортируя никаких символов, явно укажите пустой список:
# сделать модуль доступным, но ничего не импортировать
use StrangeMonkey::Utilities ();
Независимо от любых списков импорта, вы всегда можете вызвать функции из другого пакета с помощью их полностью определённых имён:
StrangeMonkey::Utilities::screech();
CPAN-модуль Sub::Exporter
предоставляет более приятный интерфейс для экспорта функций без использования глобальных переменных пакета. Он также предлагает более широкие настройки. Однако, Exporter
может экспортировать переменные, тогда как Sub::Exporter
экспортирует только функции.
Perl 5 не заставляет вас использовать ни модули, ни пакеты, ни пространства имён. Вы можете разместить весь свой код в единственном файле .pl, или в нескольких файлах .pl, подключаемых с помощью require
по необходимости. Вам предоставлена гибкость в управлении вашим кодом наиболее соответствующим способом, в зависимости от вашего стиля разработки, формальностей, рисков и вознаграждений проекта, вашего опыта и вашего комфорта в развёртывании Perl 5.
Несмотря на это, проект с более чем несколькими сотнями строк кода получает множество преимуществ от модульной организации:
Даже если вы не используете объектно-ориентированный подход, моделирование каждой отдельной сущности или ответственности в вашей системе в виде её собственного модуля сохраняет связанный код вместе и независимый код раздельно.
Самый простой способ управлять конфигурацией, сборкой, упаковкой, тестированием и установкой программного обеспечения — следовать соглашениям CPAN-дистрибутивов. Дистрибутив — это набор метаданных и одного или нескольких модулей (Модули), формирующих одну пригодную для распространения, тестирования и установки единицу.
Все эти руководства — как упаковать дистрибутив, как разрешать его зависимости, куда устанавливать программное обеспечение, как проверять, что оно работает, как отображать документацию, как управлять репозиторием — родились из примерного консенсуса тысяч контрибуторов, работающих над десятками тысяч проектов. Дистрибутив, построенный по стандартам CPAN, может быть протестирован на нескольких версиях Perl 5 на нескольких разных аппаратных платформах в течение нескольких часов после загрузки, при этом об ошибках будет автоматически сообщено авторам — и всё это без вмешательства человека.
Вы можете никогда не выпускать свой код как публичные CPAN-дистрибутивы, но вы можете использовать инструменты и соглашения CPAN даже для управления частным кодом. Сообщество Perl построило восхитительную инфраструктуру; почему бы ей не воспользоваться?
Помимо одного или нескольких модулей, дистрибутив включает несколько других файлов и директорий:
Правильно построенный дистрибутив должен содержать уникальное имя и единый номер версии (который часто берётся из его основного модуля). Любой дистрибутив, который вы скачаете из публичной CPAN, должен соответствовать этим стандартам. Публичный сервис CPANTS (http://cpants.perl.org/) проверяет каждый загруженный модуль по руководствам и соглашениям сборки и рекомендует улучшения. Следование руководствам CPANTS не означает, что код работает, но означает, что инструменты сборки CPAN должны понять дистрибутив.
Ядро Perl 5 включает несколько инструментов для установки, разработки и управления вашими собственными дистрибутивами:
CPAN.pm
— официальный CPAN-клиент; CPANPLUS
— альтернатива. Они в основном аналогичны. Хотя по умолчанию эти клиенты устанавливают дистрибутивы из публичной CPAN, вы можете вместо или дополнительно к этому направить их на свой собственный репозиторий.Module::Build
— это написанный на чистом Perl набор инструментов для конфигурирования, сборки, установки и тестирования дистрибутивов. Он работает с файлами Build.PL.ExtUtils::MakeMaker
— это унаследованный инструмент, на замену которому предназначен Module::Build
. Он всё ещё широко используется, хотя находится в режиме поддержки и получает только критические исправления ошибок. Он работает с файлами Makefile.PL.Test::More
(Тестирование) — базовый и самый широко используемый модуль тестирования, предназначенный для написания автоматизированных тестов для программного обеспечения на Perl.
Test::Harness
и prove
(Запуск тестов) запускают тесты и интерпретируют и генерируют отчёты по их результатам.В дополнение к этому, несколько дополнительных CPAN-модулей сделают легче вашу жизнь как разработчика:
App::cpanminus
— это не требующий конфигурации CPAN-клиент. Он обрабатывает наиболее распространённые случаи, использует мало памяти и быстро работает.
App::perlbrew
помогает вам управлять несколькими инсталляциями Perl 5. Установите новые версии Perl 5 для тестирования или производственной среды, или чтобы изолировать приложения и их зависимости.
CPAN::Mini
и комманда cpanmini
позволяют вам создать своё собственное (частное) зеркало публичной CPAN. Вы можете ввести в этот репозиторий свои собственные дистрибутивы и управлять тем, какие версии публичных модулей доступны в вашей организации.
Dist::Zilla
автоматизирует наиболее распространённые задачи распространения дистрибутивов. Хотя он использует либо Module::Build
, либо ExtUtils::MakeMaker
, он может заменить ваше использование их напрямую. Смотрите интерактивное руководство по адресу http://dzil.org/.Test::Reporter
позволяет вам выводить отчёты по результатам запуска набора автоматизированных тестов дистрибутивов, которые вы устанавливаете, давая их авторам больше данных о сбоях.Описание процесса проектирования дистрибутива может занять целую книгу (см. Writing Perl Modules for CPAN Сэма Трегара (Sam Tregar)), но несколько принципов дизайна помогут вам. Начните с такой утилиты как Module::Starter
или Dist::Zilla
. Начальная стоимость изучения конфигурации и правил может выглядеть как чрезмерная инвестиция, но преимущества того, что всё будет правильно настроено (и в случае Dist::Zilla
никогда не устареет) освобождает вас от намного более утомительной бухгалтерии.
Затем рассмотрите несколько правил:
Встроенный пакет UNIVERSAL
в Perl 5 — предок всех остальных пакетов — в объектно-ориентированном смысле (Moose). UNIVERSAL
предоставляет несколько методов, которые его потомки могут унаследовать или переопределить.
Метод isa()
принимает строку, содержащую имя класса или имя встроенного типа. Вызывайте его как метод класса или метод экземпляра на объекте. Он возвращает истинное значение, если его инвокант является указанным классом или происходит от него, или если инвокант — благословлённая ссылка на заданный тип.
Пусть есть объект $pepper
(ссылка на хеш, благословлённая в класс Monkey
, наследующий от класса Mammal
), тогда:
say $pepper->isa( 'Monkey' ); # выводит 1
say $pepper->isa( 'Mammal' ); # выводит 1
say $pepper->isa( 'HASH' ); # выводит 1
say Monkey->isa( 'Mammal' ); # выводит 1
say $pepper->isa( 'Dolphin' ); # выводит 0
say $pepper->isa( 'ARRAY' ); # выводит 0
say Monkey->isa( 'HASH' ); # выводит 0
Встроенные типы Perl 5 — SCALAR
, ARRAY
, HASH
, Regexp
, IO
и CODE
.
Любой класс может переопределить isa()
. Это может быть полезно при работе с объектами-моками (см. Test::MockObject
и Test::MockModule
в CPAN) или с кодом, не использующим ролей (Роли). Имейте ввиду, что любой класс, переопределяющий isa()
, обычно имеет для этого хорошую причину.
Метод can()
принимает строку, содержащую имя метода. Он возвращает ссылку на функцию, имплементирующую этот метод, если она существует. В противном случае он возвращает ложное значение. Вы можете вызвать его на классе, объекте или имени пакета. В последнем случае он возвращает ссылку на функцию, а не метод (footnote: …не то чтобы вы могли определить разницу, имея только ссылку.).
Хотя и UNIVERSAL::isa()
, и UNIVERSAL::can()
— методы (Эквивалентность методов и функций), вы можете безопасно использовать последний как функцию единственно для определения, существует ли класс в Perl 5. Если UNIVERSAL::can( $classname, 'can' )
возвращает истинное значение, кто-то где-то определил класс с именем $classname
. Этот класс может быть непригодным к использованию, но он существует.
Имея класс, названный SpiderMonkey
, с методом, названным screech
, получить ссылку на метод можно так:
if (my $meth = SpiderMonkey->can( 'screech' )) {...}
if (my $meth = $sm->can( 'screech' )
{
$sm->$meth();
}
Используйте can()
чтобы проверить, что класс имплементирует конкретную функцию или метод:
use Class::Load;
die "Couldn't load $module!"
unless load_class( $module );
if (my $register = $module->can( 'register' ))
{
$register->();
}
В то время как CPAN-модуль Class::Load
упрощает работу по загрузке классов по имени — вместо танцев с require
— Module::Pluggable
выполняет большую часть работы по сборке и управлению системой плагинов. Ознакомьтесь с обоими дистрибутивами.
Метод VERSION
возвращает значение переменной $VERSION
соответствующего пакета или класса. Если вы укажете номер версии как необязательный параметр, метод выбросит исключение, если опрашиваемая $VERSION
не равна или больше чем параметр.
Пусть есть модуль HowlerMonkey
версии 1.23
, тогда:
say HowlerMonkey->VERSION(); # выводит 1.23
say $hm->VERSION(); # выводит 1.23
say $hm->VERSION( 0.0 ); # выводит 1.23
say $hm->VERSION( 1.23 ); # выводит 1.23
say $hm->VERSION( 2.0 ); # исключение!
Нет особых причин переопределять VERSION()
.
Метод DOES()
появился в Perl 5.10.0. Он существует для поддержки использования в программах ролей (Роли). Передайте ему инвокант и имя роли, и метод вернёт истинное значение, если соответствующий класс как-либо выполняет эту роль — будь то через наследование, делегирование, композицию, применение роли или любой другой механизм.
По умолчанию реализация DOES
откатывается к isa()
, потому что наследование — единственный механизм, с помощью которого класс может выполнять роль. Пусть есть Cappuchin
:
say Cappuchin->DOES( 'Monkey' ); # выводит 1
say $cappy->DOES( 'Monkey' ); # выводит 1
say Cappuchin->DOES( 'Invertebrate' ); # выводит 0
Переопределите DOES()
, если вы вручную предоставляете роль или предоставляете другое алломорфичное поведение.
Возникает соблазн сохранять в UNIVERSAL
другие методы, чтобы сделать их доступными для всех других классов и объектов в Perl 5. Избегайте этого соблазна; это глобальное поведение можеть иметь неочевидные побочные эффекты, потому что оно не ограничено.
Учитывая сказанное, редкое злоупотребление UNIVERSAL
для целей отладки и для исправления неправильного поведения по умолчанию можеть быть извинено. Например, дистрибутив UNIVERSAL::ref
Джошуа бен Джоре (Joshua ben Jore) делает практически бесполезный оператор ref()
полезным. Дистрибутивы UNIVERSAL::can
и UNIVERSAL::isa
могут помочь вам отладить анти-полиморфные баги (Эквивалентность методов и функций). Perl::Critic
может обнаружить эти и другие проблемы.
За пределами очень осторожно контролируемого кода и очень специфичных, очень прагматичных ситуаций, нет причин напрямую помещать код в UNIVERSAL
. Почти всегд есть намного лучшие альтернативы дизайна.
Новички в программировании пишут больше кода, чем нужно, частично из-за недостаточно близкого знакомства с языками, библиотеками и идиомами, но также и из-за отсутствия опыта. Они начинают с написания длинного процедурного кода, затем открывают для себя функции, затем параметры, затем объекты, и — возможно — функции высших порядков и замыкания.
По мере того, как вы становитесь лучше как программист, вы будете писать меньше кода для решения тех же самых задач. Вы будете использовать лучшие абстракции. Вы будете писать более универсальный код. Вы сможете повторно использовать код — и когда вы сможете добавлять возможности, удаляя код, вы достигнене чего-то замечательного.
Написание программ, пишуших для вас программы — метапрограммирование или кодогенерация — предлагает большие возможности для абстракции. Хотя вы можете устроить огромный бардак, но вы можете и создать изумительные вещи. Например, техники метапрограммирования делают возможным Moose (Moose):
Техника использования AUTOLOAD
(AUTOLOAD) для отсутствующих функций и методов демонстрирует эту технику в ограниченной форме; система диспетчеризации функций и методов в Perl 5 позволяет вам настроить, что происходит, когда нормальный поиск проваливается.
Самая простая техника кодогенерации — это формирование строки, содержащей фрагмент валидного кода на Perl, и компиляция её с помощью строкового оператора eval
. В отличие от отлавливающего исключения блочного оператора eval
, строковый eval
компилирует содержимое строки в текущей области видимости, включая текущий пакет и лексические привязки.
Наиболее общее использование этой техники — предоставление возможности отката, если вы не можете (или не хотите) загружать опциональную зависимость:
eval { require Monkey::Tracer }
or eval 'sub Monkey::Tracer::log {}';
Если Monkey::Tracer
недоступен, его функция log()
будет существовать, но ничего не будет делать. Однако этот простой пример обманчив. Правильное использование eval
требует определённой работы; вы должны обработать проблемы заключения в кавычки для включения переменных в передаваемый в eval
код. Добавьте сложности чтобы интерполировать одни переменные, но не другие:
sub generate_accessors
{
my ($methname, $attrname) = @_;
eval <<"END_ACCESSOR";
sub get_$methname
{
my \$self = shift;
return \$self->{$attrname};
}
sub set_$methname
{
my (\$self, \$value) = \@_;
\$self->{$attrname} = \$value;
}
END_ACCESSOR
}
Горе тем, кто забыл обратный слеш! Удачи в объяснении вашей подсветке синтаксиса, что происходит! Хуже того, каждый вызов строкового eval
создаёт новую структуру данных, представляющую код целиком, и компиляция кода тоже не бесплатна. Но даже с этими ограничениями эта техника проста.
Хотя построение аксессоров и мутаторов с помощью eval
просто, замыкания (Замыкания) позволяют вам добавлять параметры в генерируемый код во время компиляции без необходимости дополнительного вычисления:
sub generate_accessors
{
my $attrname = shift;
my $getter = sub
{
my $self = shift;
return $self->{$attrname};
};
my $setter = sub
{
my ($self, $value) = @_;
$self->{$attrname} = $value;
};
return $getter, $setter;
}
Этот код избегает неприятных проблем с заключением в кавычки и компилирует каждое замыкание только один раз. Он даже использует меньше памяти из-за разделения скомпилированного кода между всеми экземплярами замыканий. Всё, что различается — это привязка к лексической переменной $attrname
. В долго выполняющихся процессах, или при большом количестве аксессоров, эта техника может быть очень полезна.
Установка в таблицу символов относительно проста, хоть и некрасива:
{
my ($get, $set) = generate_accessors( 'pie' );
no strict 'refs';
*{ 'get_pie' } = $get;
*{ 'set_pie' } = $set;
}
Странный синтаксис звёздочки (footnote: Воспринимайте её как сигил тайпглоба, где тайпглоб — Perl-жаргон для «таблицы символов».) разыменовывает хеш, ссылающийся на символ в текущей таблице символов, являющейся частью текущего пространства имён, содержащего глобально-доступные символы, такие как глобальные данные пакета, функции и методы. Присваивание ссылки записи таблицы символов устанавливает или заменяет соответствующую запись. Чтобы повысить анонимную функцию до метода, сохраните ссылку на эту функцию в таблице символов.
CPAN-модуль Package::Stash
предлагает более приятный интерфейс для подобного хакинга таблицы символов.
Присваивание таблице символов символа, содержащего строку, не литеральное имя переменной, это символическая ссылка. Для этой операции вам нужно отключить строгую проверку ссылок strict
. Многие программы имеют неочевидный баг в аналогичном коде, так как они присваивают и генерируют в одной строчке:
{
no strict 'refs';
*{ $methname } = sub {
# неочевидный баг: strict refs отключено здесь тоже
};
}
Этот пример отключает ограничения для внешнего блока, так же как и для тела самой функции. Только присваивание нарушает строгую проверку ссылок, так что отключайте ограничения только для этой операции.
Если имя метода — строковый литерал в вашем исходном коде, а не содержимое переменной, вы можете присвоить соответсвующему символу напрямую:
{
no warnings 'once';
(*get_pie, *set_pie) =
generate_accessors( 'pie' );
}
Прямое присваивание глобу не нарушает ограничения, но упоминание глоба только раз будет генерировать предупреждение «used only once», если вы явно не подавите его в области видимости.
В отличие от кода, написанного явно как код, код, генерируемый с помощью строкового eval
, компилируется во время выполнения. Там, где вы можете ожидать от нормальной функции, что она будет доступна всё время жизни программы, сгенерированная функция может не быть доступной, когда вы этого ожидаете.
Заставьте Perl выполнять код — генерирующий другой код — во время компиляции, обернув его в блок BEGIN
. Когда парсер Perl 5 встречает блок BEGIN
, он парсит весь блок. Если он не содержит синтаксических ошибок, блок будет выполнен немедленно. После его завершения парсинг продолжится, как если бы не было никаких прерываний.
Разница между записью:
sub get_age { ... }
sub set_age { ... }
sub get_name { ... }
sub set_name { ... }
sub get_weight { ... }
sub set_weight { ... }
…и:
sub make_accessors { ... }
BEGIN
{
for my $accessor (qw( age name weight ))
{
my ($get, $set) =
make_accessors( $accessor );
no strict 'refs';
*{ 'get_' . $accessor } = $get;
*{ 'set_' . $accessor } = $set;
}
}
…преимущественно в поддерживаемости.
Внутри модуля, любой код за пределами функций выполняется, когда вы используете его посредством use
, из-за неявного BEGIN
, который Perl добавляет вокруг require
и import
(Импорт). Любой код за пределами функций, но внутри модуля, будет выполнен перед тем, как произойдёт вызов import()
. Если вы загружаете модуль с помощью require, неявного блока BEGIN
не будет. Выполнение кода за пределами функций произойдёт в конце парсинга.
Учитывайте взаимодействие между лексическими объявлениями (ассоциирование имени с областью видимости) и лексическими присваиваниями. Первые происходят во время компиляции, тогда как последние происходят в точке выполнения. Этот код имеет неочевидный баг:
# добавляет метод require() в UNIVERSAL
use UNIVERSAL::require;
# ведёт к ошибкам; не использовать
my $wanted_package = 'Monkey::Jetpack';
BEGIN
{
$wanted_package->require();
$wanted_package->import();
}
…потому что блок BEGIN
будет выпволнен перед тем, как произойдёт присваивание строкового значения $wanted_package
. Результатом будет исключение из-за попытки вызвать метод require()
на неопределённом значении.
В отличие от установки ссылок на функции для заполнения пространств имён и создания методов, в Perl 5 нет простого способа программного создания классов. На помощь приходит Moose с входящей в него библиотекой Class::MOP
. Она предоставляет протокол метаобъектов — механизм для создания и манипулирования объектной системой в терминах её самой.
Вместо написания своего собственного хрупкого кода со строковым eval
или попыток вручную ковыряться в таблицах символов, вы можете манипулировать сущностями и абстракциями вашей программы с помощью объектов и методов:
Так создаётся класс:
use Class::MOP;
my $class = Class::MOP::Class->create(
'Monkey::Wrench'
);
Добавьте атрибуты и методы в этот класс, когда создадите его:
my $class = Class::MOP::Class->create(
'Monkey::Wrench' =>
(
attributes =>
[
Class::MOP::Attribute->new('$material'),
Class::MOP::Attribute->new('$color'),
]
methods =>
{
tighten => sub { ... },
loosen => sub { ... },
}
),
);
…или в метакласс (объект, представляющий этот класс), когда он будет создан:
$class->add_attribute(
experience => Class::MOP::Attribute->new('$xp')
);
$class->add_method( bash_zombie => sub { ... } );
…и вы можете инспектировать метакласс:
my @attrs = $class->get_all_attributes();
my @meths = $class->get_all_methods();
Аналогично, Class::MOP::Attribute
и Class::MOP::Method
позволяют вам создавать, манипулировать и анализировать атрибуты и методы.
Perl 5 не является глубоко объектно-ориентированным языком. Его внутренние типы данных (скаляры, массивы и хеши) не являются объектами с перегружаемыми методами, но вы можете контролировать поведение своих собственных классов и объектов, особенно когда они подвергаются приведению типов или контекстному вычислению. Это перегрузка.
Перегрузка может быть неочевидной, но мощной. Интересный пример — перегрузка того, как объект ведёт себя в булевом контексте, особенно если вы используете что-нибудь вроде шаблона ноль-объекта (http://www.c2.com/cgi/wiki?NullObject). В булевом контексте объект будет восприниматься как истинное значение, если вы не перегрузите преобразование в булево значение.
Вы можете перегрузить поведение объекта почти для любой операции или приведения типа: преобразование в строку, преобразование в число, преобразование в булево значение, итерации, вызов, обращение как к массиву, обращение как к хешу, арифметические операции, операции сравнения, умное сопоставление, побитовые операции и даже присваивание. Преобразование в строку, в число и в булево значение наиболее важны и наиболее распространены.
Прагма overload
позволяет вам ассоциировать функцию с операцией, которую вы можете перегрузить, передав пары аргументов, где ключ обозначает тип перегрузки, а значение — ссылка на функцию, которая должна вызываться для этой операции. Класс Null
, перегружающий булево вычисление так, что он всегда будет восприниматься как ложное значение, может выглядеть так:
package Null
{
use overload 'bool' => sub { 0 };
...
}
Можно легко добавить преобразование в строку:
package Null
{
use overload
'bool' => sub { 0 },
'""' => sub { '(null)' };
}
Переопределение преобразования в число более сложно, потому что арифметические операторы имеют тенденцию быть бинарными (Арность). Если есть два оператора, оба с перегруженными методами для сложения, какой будет приоритетнее? Ответ должен быть последовательным, простым для объяснения и понятным людям, которые не читали исходный код реализации.
perldoc overload
пытается объяснить это в разделах Calling Conventions for Binary Operations и MAGIC AUTOGENERATION, но наиболее простое решение — перегрузить преобразование в число (имеющее ключ '0+'
) и указать overload
использовать предоставленные перегрузки как запасной вариант (fallback) где возможно:
package Null
{
use overload
'bool' => sub { 0 },
'""' => sub { '(null)' },
'0+' => sub { 0 },
fallback => 1;
}
Установка fallback
в истинное значение позволяет Perl использовать любые другие определённые перегрузки для выполнения запрошенной операции, если возможно. Если это невозможно, Perl будет действовать так, как если бы не было никаких перегрузок. Это часто именно то, чего вы хотите.
Без fallback
Perl будет использовать только конкретные перегрузки, которые вы предоставили. Если кто-нибудь попытается выполнить операцию, которую вы не перегрузили, Perl выбросит исключение.
Подклассы наследуют перегрузки от своих предков. Они могут переопределить это поведение одним из двух способов. Если родительский класс использует перегрузку как показано, с напрямую предоставленными ссылками на функции, класс-потомок должен переопределить перегруженное поведение родителя с помощью прямого использования overload
.
Родительские классы могут позволить своим потомкам большую гибкость, указывая имя метода для вызова в качестве реализации перегрузки, вместо хардкода ссылки на функцию:
package Null
{
use overload
'bool' => 'get_bool',
'""' => 'get_string',
'0+' => 'get_num',
fallback => 1;
}
В этом случае любые дочерние классы могут выполнять эти перегруженные операции иначе, переопределив методы с соответствующими именами.
Перегрузка может выглядеть заманчивым инструментом для использования для генерации символьных сокращений для новых операций, но это редко происходит в Perl 5 по весомым причинам. CPAN-дистрибутив IO::All
доводит эту идею до предела в производстве умных идей для краткого и компонуемого кода. Однако на каждый блестящий API, доведённый до совершенства путём соответствующего использования перегрузки, дюжина других устраивают бардак. Иногда лучший код избегает умности в пользу простоты.
Переопределение сложения, умножения и даже конкатенации в классе Matrix
имеет смысл только из-за того, что распространена существующая нотация для этих операций. Новая предметная область, не имеющая такой установившейся нотации — плохой кандидат для перегрузки, как и предметная область, в которой вам приходится изощряться, чтобы поставить существующие операторы Perl в соответствие с другой нотацией.
Perl Best Practices Демьена Конвея (Damian Conway) предлагают другое использование перегрузки: для предотвращения случайного неправильного использования объектов. Например, перегрузка преобразования в число таким образом, чтобы она делала croak()
, для объектов, не имеющих разумного представления в виде одного числа, может помочь вам найти и исправить реальные ошибки.
Perl предоставляет инструменты для написания безопасных программ. Эти инструменты — не замена для внимательного обдумывания и планирования, но они вознаграждают внимание и понимание и могут помочь вам избежать неочевидных ошибок.
Режим испорченных данных (taint mode, или просто taint) добавляет метаданные ко всем данным, приходящим из-за пределов вашей программы. Любые данные, производные от испорченных данных, тоже испорчены. Вы можете использовать испорченные данные в своей программе, но если вы используете их для воздействия на окружающий мир — если вы используете их небезопасно — Perl выбросит фатальное исключение.
perldoc perlsec
объясняет режим испорченных данных во всех подробностях.
Для включения режима испорченных данных запустите свою программу с аргументом командной строки -T
. Если вы используете этот аргумент в строчке #!
, вы должны запустить программу напрямую; если вы запустите её как perl mytaintedapppl.pl
и пренебрежёте флагом -T
, Perl выйдет с исключением. К тому моменту, когда Perl встречает флаг в строке #!
, уже упущена возможность пометить испорченными данные окружения, например, входящие в %ENV
.
Испорченные данные могут приходить из двух мест: файловый ввод и операционное окружение программы. Первое — это всё, что вы читаете из файла или получаете от пользователей в случае веб- или сетевого программирования. Второе включает любые аргументы командной строки, переменные окружения и данные из системных вызовов. Даже такие операции, как чтение из дескриптора директории, генерируют испорченные данные.
Функция tainted()
из базового модуля Scalar::Util
возвращает истину, если её аргумент испорчен:
die 'Oh no! Tainted data!'
if Scalar::Util::tainted( $suspicious_value );
Чтобы удалить испорченность, вы должны выделить заведомо нормальные порции данных с помощью захвата в регулярном выражении. С захваченных данных будет снята испорченность. Если ваш пользовательский ввод содержит телефонный номер США, вы можете снять с него испорченность так:
die 'Number still tainted!'
unless $number =~ /(\(/d{3}\) \d{3}-\d{4})/;
my $safe_number = $1;
Чем более конкретен ваш шаблон относительно того, что вы позволяете, тем более безопасной может быть ваша программа. Противоположный подход запрета конкретных элементов или форм имеет риск недосмотра чего-либо вредоносного. Гораздо лучше не пропустить что-нибудь безопасное, но неожиданное, чем пропустить что-нибудь опасное, но выглядящее безопасным. Несмотря на это, ничто не запрещает вам написать захват для всего содержимого переменной — но в этом случае зачем использовать режим испорченных данных?
Суперглобальная переменная %ENV
представляет переменные окружения системы. Эти данные испорчены, потому что силы за пределами контроля программы могут манипулировать данными в них. Любая переменная окружения, влияющая на то, как Perl или оболочка находит файлы и директории — это направление атаки. Чувствительная к испорченным данным программа должна удалить несколько ключей из %ENV
и установить в $ENV{PATH}
конкретный и хорошо защищённый путь:
delete @ENV{ qw( IFS CDPATH ENV BASH_ENV ) };
$ENV{PATH} = '/path/to/app/binaries/';
Если вы не установите $ENV{PATH}
соответствующим образом, вы будете получать сообщения о его небезопасности. Если эта переменная окружения содержит текущую рабочую директорию, или если она содержит относительные директории, или если указанные директории имеют общий доступ на запись, умный атакующий может взломать системные вызовы для нанесения вреда.
По аналогичным причинам @INC
в режиме испорченных данных не содержит текущую рабочую директорию. Perl также будет игнорировать переменные окружения PERL5LIB
и PERLLIB
. Используйте прагму lib
или флаг -I
для perl
, чтобы добавить библиотечные каталоги в программу.
Режим испорченных данных — это всё или ничего. Он либо включен, либо выключен. Это иногда приводит людей к использованию разрешительных шаблонов для снятия испорченности с данных и даёт иллюзию безопасности. Внимательно проверяйте снятие испорченности.
К сожалению, не все модули правильно обрабатывают испорченные данные. Это ошибка, которую CPAN-авторы должны принимать всерьёз. Если вам нужно сделать унаследованный код taint-безопасным, рассмотрите использование флага -t
, который включает режим испорченных данных, но понижает его нарушения с исключений до предупреждений. Это не замена для полного режима, но это позволяет вам сделать существующие программы безопасными без подхода «всё или ничего» -T
.