Выпуск 2. Апрель 2013

Удобное логирование с Log::Any | Содержание | Введение в разработку web-приложений на PSGI/Plack

Debug-fu в стиле Perl

Чаку Норрису не нужен отладчик, он просто пристально смотрит на код пока тот сам не сознается, где в нём баги. Если вы не Чак Норрис, то вам придётся изучить кунг-фу отладки кода, чтобы одержать победу в поединке с собственными (или чужими) ошибками.

Немного философии о программировании и ошибках

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

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

  • парадигма ООП — разложите код в маленькие чёрные ящички, чтобы я легко мог найти именно тот, в котором есть ошибки и мог его независимо подправить;
  • TDD — напишите тест, чтобы проверить, что в коде нет ошибок, до того как написан сам код.

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

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

Начало программы — самый важный участок кода

Самый первый этап проверки на правильность кода начинается с валидации его синтаксиса. Perl даёт большую свободу программисту при написании кода, что позволяет писать как короткие и эффективные однострочники, так и гигантские модульные комплексы. Если вы не пишете однострочник или обфусцированный код, то рекомендуется начинать программу с использования прагм strict и warnings.

use strict;
use warnings;

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

use diagnostics;

Которая будет развёрнуто объяснять вам, что означает то или иное предупреждение.

Кроме того, пользуясь случаем, хочу порекомендовать использовать CPAN-модуль strictures:

use strictures 1;

Эта прагма эквивалентна такому коду:

use strict;
use warnings FATAL => 'all';

Т.е. теперь любые предупреждения будут трактоваться как ошибки. Кроме того, в случае, если strictures выявит, что запуск происходит в условиях разработки (запуск скрипта из каталогов t, xt, lib, blib и наличие в текущем каталоге .git или .svn), это расширяется в конструкцию:

use strict;
use warnings FATAL => 'all';
no indirect 'fatal';
no multidimensional;
no bareword::filehandles;

Три дополнительные прагмы являются соответствующими модулями на CPAN и если их нет у вас в системе, то использоваться они не будут. Но они, несомненно, полезны, так как позволяют обнаруживать потенциальные ошибки в вашем коде на этапе разработки, отловить которые бывает очень сложно.

no indirect 'fatal' — запрещает использовать непрямые вызовы методов, например:

my $obj = new Module::Name;

вместо записи:

my $obj = Module::Name->new()

Почему первый вариант не стоит использовать? Посмотрите код:

sub mess($) {
    shift->{message}
}

my $y = mess { message => "hello world!" };

Вроде всё отлично, но попробуйте передвинуть определение функции mess, после её первого использования. Это, по идее, должно быть вполне законно, но в итоге приводит к странной ошибке:

Can't locate object method "mess" via package "message" (perhaps you forgot to load "message"?)

Ссылку на хеш Perl воспринял за класс и попробовал вызвать метод mess. Вот и получили настоящую неразбериху…

no multidimensional — спасает нас в ситуации, когда мы хотим получить срез значений хеша:

%hash = (
    "a" => 1,
    "b" => 2
);
print @hash{"a","b"};

Как и ожидается, это выведет значения ключей a и b, но если мы сделали опечатку и, вместо символа @, ввели $, то получим пустой результат, а Perl даже не будет ругаться, так как это вполне допустимая конструкция. Прагма no multidimensional прервёт такую программу с ошибкой.

Третья прагма bareword::filehandles отучит вас от привычки засорять глобальное пространство имён, давая названия файловым дескрипторам в виде «голых» слов (bareword):

my $content;
open(FH, "</some/file") or die $!;
while(<FH>) {
    $content .= $_;
}
close(FH);

Этот код будет выдавать ошибку с данной прагмой. Разумеется, использовать стандартные STDOUT, STDERR и прочие встроенные «голые» слова не запрещается.

От простого к сложному

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

Оператор print

Самый популярный, на сегодняшний день отладчик кода — это оператор print. Расставляя его в различных местах программы, можно проверять значения переменных, видеть в каком направлении исполняется программа. Думаю, что использовать его совершенно не зазорно, т.к. это самый простой и очевидный способ поиска проблемы. Вероятно, он даже сразу подтвердит вашу догадку об ошибке без приложения больших усилий и затрат времени. Ещё лучше использовать его вместе с модулем Data::Dumper, который позволяет выводить человеко-читаемый вывод для сложных структур данных:

use Data::Dumper;
print Dumper $some_complex_ref;

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

print if $DEBUG

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

my $DEBUG = 1;
print "debugging message" if $DEBUG;

или даже так:

use constant DEBUG => 0;
print "debugging message" if DEBUG;

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

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

Smart::Comments

Вероятно, после трудоемкого пути с комментированием/раскомментированием строк с отладочным print кому-то пришла в голову идея вставлять команды отладки через комментарии. Идея была воплощена в виде модуля Smart::Comments. В код вставляются комментарии, состоящие из трёх или более подряд идущих символов #:

my $var = 10;
### $var
### Doubled $var: $var*2

Такие комментарии заставят программу вывести значения указанных выражений:

$ perl -MSmart::Comments application.pl

### $var: 10
### Doubled $var: 20

Для вывода простого текстового сообщения нужно добавить троеточие в конце комментария:

### Do some work...

Также можно вставлять в такие текстовые сообщения метку времени и номер строки программы:

### <now> Do some work at <here>...

выведет:

### Sat Mar 16 15:03:09 2013 Do some work at "application.pl", line 12...

Существует возможность задать разный уровень у отладочных сообщений, варьируя количество символов комментария #. Тогда при запуске с такими параметрами:

$ perl -MSmart::Comments=###,#### application.pl

будут выводиться только комментарии уровня 3 и 4, но не 5 и более.

Есть также и assert, который позволяет прервать работу программы, в случае если условие не выполнено, например:

my $x = 10
### assert: $x < 10

остановит программу и выведет:

### $x < 10 was not true at application.pl line 12.
###     $x was: 10

Если прерывать программу не требуется, но нужно выдать предупреждение, если условие не выполнилось, можно использовать check:

my $x = 10
### check: $x < 10

С помощью умных комментариев можно даже выводить индикатор прогресса, например, для итераций циклов:

for my $var ( @list ) { ### Progressing...   done

Progressing...                 done
Progressing...........         done
Progressing.................   done

Если кому-то нравится индикатор прогресса с процентами, то это тоже возможно:

for my $var ( @list ) { ### Evaluating [===|    ] % done

Evaluating [|            ] 0% done
Evaluating [===|         ] 33% done
Evaluating [======|      ] 55% done

Кроме того, в случае цикла for, при продолжительном времени работы цикла, начинает выводиться также и время оставшееся до завершения цикла:

Evaluating [|             ] 9% done  (about 1 minute remaining)

Smart::Comments позволяют достаточно просто отлаживать программы. Комментарии не оказывают никаких побочных эффектов на программу, поэтому их можно безболезненно оставлять в коде.

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

### $x*2

Devel::Comments

Devel::Comments — это форк Smart::Comments, который обладает некоторым новым функционалом, например, есть возможность выводить сообщения не на экран, а в файл:

use Devel::Comments ({-file => 'log'});

Но при этом модуль имеет все те же проблемы, что и Smart::Comments.

Devel::Trace

Чтобы проследить как выполняется программа, можно использовать Devel::Trace, который отображает каждую строку исходного кода на экран перед её выполнением:

$ perl -d:Trace test1.pl
>> test1.pl:2: print "hello,world";
>> test1.pl:3: exit 0;
hello,world

Основное достоинство модуля в его простоте. Но никакой другой важной информации вы от него не получите. Существуют и альтернативные модули, которые позволяют фильтровать выводимую информацию в зависимости от модулей и/или функций в которых в текущий момент находится интерпретатор.

Carp::REPL

Модуль Carp::REPL также может использоваться при отладке:

$ perl -MCarp::REPL=warn my_code.pl

В случае, если в коде возникает ошибка или даже предупреждение, происходит останов программы и вы попадаете в интерактивную оболочку Devel::REPL, в которой можно попробовать вывести значения переменных, стек вызовов и т.д. Модуль может быть полезен для того, чтобы добраться до проблемной точки в программе и попытаться выяснить ошибку на месте в многофункциональной интерактивной командной оболочке. Модуль требует установленного Devel::REPL, который имеет зависимость от Moose (для кого-то это может быть недостатком).

Встроенный отладчик Perl

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

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

$ perl -d -e 1

Loading DB routines from perl5db.pl version 1.33
Editor support available.

Enter h or `h h' for help, or `man perldebug' for more help.

main::(-e:1): 1
DB<1>

После чего мы попадаем в командную оболочку программы, которая позволяет проводить отладку приложения. Отлаживаемая программа загружается, но останавливается перед самым первым оператором (секции BEGIN и CHECK при этом выполняются). Важно, чтобы программа компилировалась без ошибок, в противном случае отладка будет невозможна. Если отладчик не может распознать команду, то он попытается выполнить её через eval как обычный Perl-код. С этой точки зрения отладчик можно рассматривать как простую REPL (Read Eval Print Loop) оболочку для Perl.

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

DB<2> my $x=1
DB<3> print $x

DB<4> $x =1
DB<5> print $x
1

Команды отладчика

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

  • h [command] — без параметров выведет помощь по всем командам отладчика; если команда указана, то по ней будет выведена короткая документация; указание параметра h приведёт к подробному выводу помощи по всем командам и вывод будет достаточно длинным; для чтения длинного вывода удобно использовать пейджер, задаваемый символом вертикальной черты:
    | h h
  • p expr — выведет значение выражения expr, аналогичен оператору print;
  • x [maxdepth] expr — вычисляет значение выражения expr в списочном контексте и выводит результат, только, в отличие от print, может в читаемом виде показывать сложные вложенные структуры;
  • V [[pkg] [vars]] — отобразит все или только указанные переменные пакета (по умолчанию main); имя переменных указывается без символа $:
    DB<2> V main ]
    $] = 5.016003

для фильтрации списка удобно использовать регулярное выражение, указав перед шаблоном символ ~ (или ! — для исключения):

    DB<2> V main ~\d
    $2 = '~\\d'
    $1 = 'main'
    $0 = '-e'
  • X [vars] — то же самое, что и V currentpackage [vars];
  • y [level [vars]] — показывает все или выбранные лексические переменные;
  • T — вывод трассировки;
  • s — выполнение одного оператора программы со входом в функции;
  • n — выполнение одного оператора программы без входа в функции;
  • r — продолжить выполнение до конца текущей функции;
  • c — продолжить выполнение до следующей точки останова;
  • l — показать несколько строк исходного кода;
  • t — переключает режим трассировки;
  • b — устанавливает точку останова на указанной строке;
  • B — удаляет точку останова;
  • enable/disable — включает/выключает точку останова;
  • w expr — задаёт наблюдение за указанной переменной;
  • W expr — удаляет наблюдение за переменной;
  • q — выход из программы;
  • R — перезапуск программы;
  • m — список доступных для запуска методов/функций;
  • M — список загруженных модулей.

Опции отладчика

Опции отладчику можно задавать в переменной окружения PERLDB_OPTS, в файле ~/.perldb или в опциях командной строки. Они регулируют поведение отладчика, вот некоторые из них:

  • AutoTrace — задаёт режим трассировки;
  • NonStop — отключает интерактивный режим, происходит запуск программы, пока не будет прервана сигналом или каким-то другим способом;
  • ReadLine — задаёт использование библиотеки ReadLine; иногда требуется её отключать, если отлаживаемая программа сама использует ReadLine;
  • LineInfo — задаёт файл для записи информации о строках кода frame — задаёт уровень информативности вывода при входе/выходе в/из процедур;
  • RemotePorthost:port для удалённой отладки; ввод и вывод перенаправляются на сетевой сокет.

Пример неинтерактивного запуска отладчика с выводом информации о вызове функций в файл listing:

$ PERLDB_OPTS="NonStop LineInfo=listing frame=2" perl -d /usr/bin/cpan

Дополнительная информация

Подробную информацию об отладчике можно почитать в man-страницах perldebug и perldebtut

Другие интерфейсы к отладчику Perl

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

  • Devel::ptkdb — это графический интерфейс к отладчику с использованием тулкита Tk; он достаточно лёгкий, но рекомендовать его можно только, если вам не интересны другие альтернативы; всё-таки используемый тулкит староват и выглядит не очень привычно для современного десктопа;
  • Padre — имеет в своём составе графический интерфейс к отладчику; доступны большинство функций отладчика, выглядит вполне современно и удобно в использовании;
  • Devel::hdb – недавно появившийся довольно перспективный модуль, который позволяет отлаживать ваши скрипты, создавая web-интерфейс к отладчику:

    $ perl -d:hdb /usr/bin/cpan
    Debugger listening on http://127.0.0.1:8080/
  • Vim::Debug — модуль позволяет производить отладку внутри редактора vim.

Новое поколение отладчиков для Perl

Встроенный отладчик Perl имеет долгую историю. Сам по себе он представляет собой скрипт perl5db.pl в 10 тыс. строк и несёт в себе тяжёлое наследие продолжительного развития вместе с Perl. Разобраться в его коде не так просто, исправлять проблемы тяжело, а расширять функционал крайне затруднительно. По этой причине появились проекты, поставившие задачу создать новый отладчик для Perl, который можно было бы легко развивать и дополнять функционал расширениями.

Devel::ebug

Devel::ebug — это простой и расширяемый отладчик для Perl, имеющий понятное API. Он позиционируется как замена встроенному отладчику, хорошо протестированная основа для создания других отладчиков с консольным, графическим или веб-интерфейсом.

Для Devel::ebug на данный момент есть два интерфейса: ebug — консольный клиент (в составе самого пакета) и ebug_http — веб-интерфейс (в составе Devel::ebug::HTTP). Кроме того, в состав пакета входят клиент ebug-client и сервер ebug-server позволяющий производить удалённую отладку приложения.

Devel::Trepan

Devel::Trepan — это довольно серьёзная альтернатива стандартному отладчику Perl. Автор модуля Rocky Bernstein имеет большой опыт в области создания отладчиков, он приложил руку к созданию отладчиков для языков программирования Python, Ruby и Shell. Руководствуясь всё теми же мотивами (устаревший и тяжелый в поддержке и развитии код встроенного отладчика), создан новый отладчик, который также выдвигает новое важное требование — совместимость с набором команд gdb. Не секрет, что отладчик gdb знаком многим программистам независимо от того, на каком языке они программируют — отладка C/С++-программ и библиотек может потребоваться в любой момент. Если отладчик Perl будет иметь сходный набор команд, он будет значительно проще в освоении.

После установки доступен консольный отладчик trepan.pl. Запуск отладки происходит привычным способом:

$ trepan.pl application.pl
-- main::(application.pl:5)
print "Hello, World!\n";
(trepanpl):

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

  • — подключить файл с командами отладчика (по аналогии с ключом -x gdb);
  • -x — трассировка выполняемых команд (по аналогии запуска set -x в shell);
  • --client или --server — удалённая отладка кода (клиент или сервер).

Команды trepan.pl

  • help — выведет подсказку по существующим командам; вывод очень похож на аналогичный вывод gdb; помощь разделена на несколько секций-классов:

    • breakpoints — остановка программ в нужном месте;
    • data — изучение данных;
    • files — изучение файлов;
    • running — управление запущенной программой;
    • stack — изучение стека;
    • status — статусная информация о программе;
    • support — утилиты отладчика;
    • syntax — синтакс команд.

Наиболее часто используемые команды:

  • step — выполнить следующий оператор (со входом в функции);
  • next — выполнить следующий оператор (без входа в функции);
  • finish — выполнить все операторы до конца данной функции;
  • continue — продолжить выполнение до точки остонова;
  • quit — выйти из программы;
  • list — показать исходный код;
  • info var (l|m|o) — показать определённый класс переменных в данной области видимости;
  • backtrace — информацию о стеке фреймов;
  • break — установка точки останова;
  • watch — отслеживание изменения переменной.

На большинство самых востребованных команд присутствует короткие псевдонимы: s — step, n — next, l — list и т.д. Весь список можно увидеть по команде alias.

Любопытно, что можно рекурсивно запускать отладку кода внутри отладчика, с помощью команды debug. Например, отладка функции Фибоначчи:

debug fibonacci(5)

Расширения Devel::Trepan

Для отладчика Devel::Trepan уже существуют расширения, улучшающие его возможности:

  • Devel::Trepan::Shell — интерактивная командная строка Perl внутри Devel::Trepan (на основе Devel::REPL). Интерактивная командная строка в отладчике присутствует, но всё же имеет ограничения. С данным расширением вы получаете полноценный REPL;
  • Devel::Trepan::Disassemble — поддержка команды dissassemble в отладчике. Служит для вывода дизасемблированного кода операций пакета или подпрограммы. Для вывода используется модуль B::Concise.

Заключение

Несмотря на все старания, обзор получился скорее всего не полный. Не была рассмотрена отладка XS-приложений, регулярных выражений, ничего не было сказано про профайлеры, такие как Devel::NYTProf, а также не упомянуты модули B::Concise и B::Deparse, позволяющие рассмотреть, во что транслируется Perl-код для глубокого погружения в пучины отладки. Но думаю, что этим и другим более продвинутым методам можно посвятить отдельную статью.

Владимир Леттиев


Удобное логирование с Log::Any | Содержание | Введение в разработку web-приложений на PSGI/Plack
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье