2007年02月11日 13:45 [Edit]
perl - B::Deparse
尻馬乗るべし、ということでB::Deparseの紹介。
スクリプト言語用の CUIのデバッガの使い方を簡単にまとめました。対象言語は Ruby, Python, Perl です。
実は私も、デバッガーはperl -de1ぐらいしか使っていない(perl -de1は非常によく使うので、Terminal.appのウィンドウの一つがそれ専用になっている。スクリプト言語のインタラクティブな利用法に関しては以前「404 Blog Not Found:LL Intaractive」にまとめたのでそちらをご覧頂くとして、ここではなぜスクリプト言語では滅多にデバッガーを使わないかをおさらいした上で、B::Deparseを取り上げる。
私は C, C++ でプログラムを書いているときはデバッガ (主に GNU/Linux 上の gdb) を頻繁に利用します。しかし、スクリプト言語ではそれほどでもありません。これはおそらく次のような理由によります。
- ビルドが不要なので printf デバッグが容易 (ある程度大きい C++ のプログラムではビルド時間が長いので printf の挿入はしんどい)
- 異常終了時にスタックトレースが表示される (Ruby, Python なら自動、Perl の場合は use Carp; $SIG{__DIE__} = \&Carp::confess; など)
- オブジェクトのインスペクトが簡単 (Ruby なら p/pp, Python なら print, Perl なら Data::Dumper)
- セグメンテーションフォルトは (基本的に) 発生しない
- 小さい単純なプログラムを書いている場合が多い
Perlの場合、これに是非「B::Deparseがあるから」というものを加えたい。それくらい便利なツールである。
利用法その一:スクリプト全体をDeparse
Perlの悪名の理由の一つは、「書きやすいが読みにくい」というものであった。実際そうでもないし、昨今、特に「Perl Best Practices」が上梓されてからはblogosphereを賑わすcode snippetsの可読性もみちがえるように上がったのだが、それでも一行野郎(one-line)などは読みやすさは2の次3の次だし、golfやObfuscnated Codesなど、わざと読みにくくしているものもある。
そんな時は、B::Deparseにそのコードを読み解いてもらおう。使い方は簡単。
perl ...
としていたところを、
perl -MO=Deparse ...
とすればよい。-MB=Deparseでない点に注意。そのコードが実行される代わりに、そのコードがPerlから見てどう見えたかが表示される。
例えば、「ファイル中に登場する0.01をすべて0.02に書き換えて、元のファイルをバックアップする」という作業なら、
perl -i.bak -ple 's/0\.01/0.02/g' files ...
と書くが、これに-MB=Deparseをふりかけるとこう表示される。
% perl -MO=Deparse -i.bak -ple 's/0\0.1/0.02/g'
BEGIN { $^I = ".bak"; }
BEGIN { $/ = "\n"; $\ = "\n"; }
LINE: while (defined($_ = )) {
chomp $_;
s/0\0.1/0.02/g;
}
continue {
print $_;
}
-e syntax OK
「いやなブログ」のデバッギ(debugee - これ、バッドノウハウ英語で紹介してもいいのでは)も、手だれのPerl Mongerならこうしているだろう。
perl -MLWP::Simple -e 'print get shift' http://0xcc.net
これも、-MB=Deparseで「元に戻せ」る。
% perl -MO=Deparse -MLWP::Simple -e 'print get shift' http://0xcc.net use LWP::Simple; print get(shift @ARGV); -e syntax OK
表示を微調整することも出来る。例えばこんな風に。
% perl -MO=Deparse,-l,-x7 -MLWP::Simple -e 'print get shift' http://0xcc.net
#line 0 "-e"
sub BEGIN {
#line 0 "-e"
require LWP::Simple;
#line 0 "-e"
do {
#line 0 "-e"
'LWP::Simple'->import
};
}
#line 1 "-e"
print get(shift @ARGV);
-e syntax OK
詳しくは例によってperldoc B::Deparseを参照のこと。B::Deparseは初心者の味方である。
クロージャーをディスクローズ
中級以上のPerl Programmerは、今やcode referenceの利用が日常茶飯事である。非常に強力なのだが、欠点としてデバッグしにくいということがある。ものによっては動的にcodeを生成するので、本当に実行されるコードはどうなのか、ソースを見ても見えないのだ。
こういう時こそ、B::Deparseの出番である。
たとえば、以下のようなコードがあるとする。
1 : use strict;
2 : use warnings;
3 : {
4 : package MyFileHandle;
5 : use base 'FileHandle';
6 : sub each_line {
7 : my $fh = shift;
8 : my $coderef = shift;
9 : while ( my $line = $fh->getline ) {
10 : $coderef->( $fh, $line );
11 : }
12 : }
13 : }
14 : my $fh = MyFileHandle->new( shift, 'r' ) or die;
15 : $fh->each_line(
16 : sub {
17 : my ( $fh, $line ) = @_;
18 : printf "%4d : %s", $fh->input_line_number, $line
19 :
20 : }
21 : );
まず、ここでeach_lineの方でどんなcode referenceを受け取ったかを確認してみたいものとする。こうすればよい。
3 : {
4 : package MyFileHandle;
5 : use base 'FileHandle';
6 : use B::Deparse;
7 : sub each_line {
8 : my $fh = shift;
9 : my $coderef = shift;
10 : my $bd = B::Deparse->new;
11 : warn $bd->coderef2text($coderef);
12 : while ( my $line = $fh->getline ) {
13 : $coderef->( $fh, $line );
14 : }
15 : }
16 : }
すると、each_lineが呼ばれる都度
{
use warnings;
use strict 'refs';
my($fh, $line) = @_;
printf '%4d : %s', $fh->input_line_number, $line;
} at deparse.pl line 11.
と出てくる。
メソッドをディスクローズ
上記の例で、逆にeach_line()の中身を呼び出し側で確認したい場合はどうしたらよいか?
こうする。
15 : my $fh = MyFileHandle->new( shift, 'r' ) or die; 16 : use B::Deparse; 17 : my $bd = B::Deparse->new(); 18 : warn $bd->coderef2text($fh->can('each_line'));
実行されると、こうなる。
{
package MyFileHandle;
use warnings;
use strict 'refs';
my $fh = shift @_;
my $coderef = shift @_;
while (my $line = $fh->getline) {
&$coderef($fh, $line);
}
} at deparse.pl line 18.
これを使えば、デバッガーを使わずともメソッドのコードを覗く事ができるのだ。
Data::DumperやStorableと組み合わせて
実はData::DumperもStorableも、B::Deparseをサポートしている。これらを利用すると、code referenceを普通のオブジェクトのように扱ったり、シリアライズしたりすることが簡単に出来る。詳しくはそれぞれのマニュアルを参照のこと。
まとめ
以上のように、B::Deparseは初心者から上級者まで、実に使い出のあるモジュールである。これのおかげでかつてはデバッガに頼っていたこともこれでほとんど間に合うようになった。RubyやPythonではどうしているのだろうか....
Dan the Perl Monger