Управление реальными программами

Книга может научить вас писать маленькие программы, решающие маленькие задачи-упражнения. Этим способом вы можете научиться синтаксису. Чтобы писать реальные программы, выполняющие реальные задачи, вам нужно научиться управлять кодом, написанным на вашем языке. Как вам организовать код? Как убедиться, что он работает? Как сделать его надёжным перед лицом ошибок? Что делает код кратким, ясным и поддерживаемым?

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

Тестирование

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

Perl 5 предоставляет отличные инструменты, помогающие вам писать правильные тесты.

Test::More

Тестирование в 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 доступны сотни тестовых модулей — и все они могут работать вместе в одной и той же программе.

См. проект 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 содержит список всех остальных проверок файлов; наиболее популярны следующие:

Начиная с 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 должны понять дистрибутив.

Инструменты CPAN для управления дистрибутивами

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

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

Проектирование дистрибутивов

Описание процесса проектирования дистрибутива может занять целую книгу (см. Writing Perl Modules for CPAN Сэма Трегара (Sam Tregar)), но несколько принципов дизайна помогут вам. Начните с такой утилиты как Module::Starter или Dist::Zilla. Начальная стоимость изучения конфигурации и правил может выглядеть как чрезмерная инвестиция, но преимущества того, что всё будет правильно настроено (и в случае Dist::Zilla никогда не устареет) освобождает вас от намного более утомительной бухгалтерии.

Затем рассмотрите несколько правил:

Пакет UNIVERSAL

Встроенный пакет UNIVERSAL в Perl 5 — предок всех остальных пакетов — в объектно-ориентированном смысле (Moose). UNIVERSAL предоставляет несколько методов, которые его потомки могут унаследовать или переопределить.

Метод isa()

Метод 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()

Метод 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 упрощает работу по загрузке классов по имени — вместо танцев с requireModule::Pluggable выполняет большую часть работы по сборке и управлению системой плагинов. Ознакомьтесь с обоими дистрибутивами.

Метод VERSION()

Метод 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()

Метод 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

Возникает соблазн сохранять в UNIVERSAL другие методы, чтобы сделать их доступными для всех других классов и объектов в Perl 5. Избегайте этого соблазна; это глобальное поведение можеть иметь неочевидные побочные эффекты, потому что оно не ограничено.

Учитывая сказанное, редкое злоупотребление UNIVERSAL для целей отладки и для исправления неправильного поведения по умолчанию можеть быть извинено. Например, дистрибутив UNIVERSAL::ref Джошуа бен Джоре (Joshua ben Jore) делает практически бесполезный оператор ref() полезным. Дистрибутивы UNIVERSAL::can и UNIVERSAL::isa могут помочь вам отладить анти-полиморфные баги (Эквивалентность методов и функций). Perl::Critic может обнаружить эти и другие проблемы.

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

Кодогенерация

Новички в программировании пишут больше кода, чем нужно, частично из-за недостаточно близкого знакомства с языками, библиотеками и идиомами, но также и из-за отсутствия опыта. Они начинают с написания длинного процедурного кода, затем открывают для себя функции, затем параметры, затем объекты, и — возможно — функции высших порядков и замыкания.

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

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

Техника использования AUTOLOAD (AUTOLOAD) для отсутствующих функций и методов демонстрирует эту технику в ограниченной форме; система диспетчеризации функций и методов в Perl 5 позволяет вам настроить, что происходит, когда нормальный поиск проваливается.

eval

Самая простая техника кодогенерации — это формирование строки, содержащей фрагмент валидного кода на 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() на неопределённом значении.

Class::MOP

В отличие от установки ссылок на функции для заполнения пространств имён и создания методов, в 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.