Tie::Traceで簡単に変数の中身を追う
前置き
こんにちは、id:ktatです。最近は、Util::Allというモジュールをいじってますが、CPAN にはあげてないので、紹介できません。
というわけで、今日はデバッグのお供に使えるかもしれない、Tie::Traceを紹介します。
本題
さて、perlのプログラムのデバッグするなら、perl -d というのも良いですが、print デバッグもお手軽でいいですよね。
しかし、怪しい変数を追いかけたり、見知らぬオブジェクトの中を調べたりするのに、いちいち print や warn を挿入していくのも面倒です。
そんな時には、Tie::Trace が役に立つかもしれません。
単純な例
次の $hoge、 @hoge、 %hoge の各変数を追いかけてみます。
use Tie::Trace qw/watch/; watch my $hoge; watch my @hoge; watch my %hoge; $hoge = 1; push @hoge, "a"; delete $hoge[-1]; $hoge{1} = 1; $hoge{"abc"} = "xyz";
以下のようなメッセージが表示されます。
main:: $hoge => 1 at sample1.pl line 7. main:: @hoge => PUSH('a') at sample1.pl line 9. main:: @hoge[0] => DELETED('a') at sample1.pl line 10. main:: %hoge => {1} => 1 at sample1.pl line 12. main:: %hoge => {abc} => 'xyz' at sample1.pl line 13.
若干分かりにくい表示ですが、main:: 変数名 は、main package で宣言されている変数という意味です。=> の後ろに、代入されたり削除された値が入ります。PUSH や DELETED のように何かしら説明っぽいものが入る場合もあります。
リカーシブに追跡
watch 関数で監視された変数に、ハッシュリファレンスやアレイリファレンスが代入された場合、それらは再帰的にチェックされ、監視する対象になります。
use Tie::Trace qw/watch/; watch my @array; push @array, { a => 1 }, [0, 1, 2, [10, 20, 30, { a => 'b'}]]; $array[0]->{b} = 2; $array[1]->[3]->[3]->{a} = "c"; $array[1]->[3]->[3]->{c} = "d";
としてみます。
main:: @array => PUSH({'a' => 1},[0,1,2,[10,20,30,{'a' => 'b'}]]) at sample2.pl line 4. main:: @array => [0]{b} => 2 at sample2.pl line 5. main:: @array => [1][3][3]{a} => 'c' at sample2.pl line 6. main:: @array => [1][3][3]{c} => 'd' at sample2.pl line 7.
このように、@arrayに挿入された無名ハッシュや無名配列に値が入ってもメッセージが表示されます。ただし、オブジェクトや tie されたリファレンスなどが入ってきた場合、それらは監視対象にはなりませんので、ご注意ください。
オブジェクトの中身をチェックする
オブジェクトの中身にをチェックするのにも使えます。Mouse(0.40)でやってみました。
package Hoge; use Mouse; has "name" => (is =>"rw"); has "height" => (is =>"rw"); package main; use Tie::Trace qw/watch/; my $x = Hoge->new; watch %$x; $x->name("ktat"); $x->height(173)
以下のような表示がされます。
{name} => 'ktat' at accessor for name (.../Mouse/Meta/Method/Accessor.pm) line 2. {height} => 173 at accessor for age (.../Mouse/Meta/Method/Accessor.pm) line 2.
package名や変数名が取れていないときは、グローバル変数、無名リファレンスなんかです。
ちなみに、Moose(0.92)だとこんな感じでした。
{name} => 'ktat' at accessor name defined at sample4.pl line 6. {height} => 173 at accessor height defined at sample4.pl line 7.
にしても、Mouse も Moose もメッセージが独特な感じですね。Mouseの "line 2" がよくわかりませんが、追ってません。
しかし、いずれも代入された箇所がわかりませんね。以下のように、caller オプションを与えてみましょう。
watch %$x, caller => [0, 1];
watch の変数名の後の引数はオプションになります。callerオプションをつけると、指定された分だけたどってくれます。
{name} => 'ktat' at accessor name defined at sample5.pl line 6. at sample5.pl line 15. {height} => 173 at accessor height defined at sample5.pl line 7. at sample5.pl line 16.
これで、代入場所が特定できましたね。
リカーシブなチェックをやめる
リカーシブなチェックはいらないという場合もあるでしょう。そういうときは次のようにします。
watch %var, r => 0;
これで、リファレンスが代入されても、それらは監視対象にはなりません。
条件に一致するキーのみ
use Tie::Trace qw/watch/; watch my %foo, key => ['foo']; watch my %bar, key => [qr/bar/]; watch my %buz, key => [sub{my($self,$key) = @_;$key =~/buzbuz/}]; watch my %foobarbuz, key => ['foo', qr/bar/, sub {my ($self, $key) = @_; return $key =~/buzbuz/}]; $foo{foo} = 1; $foo{hoge} => {foo => 2}; $foo{hoge}->{foo} = 2; $bar{bar} = 1; $bar{beer} = 2; $bar{barbarbar} = 3; $buz{buz} = 1; $buz{buzbuz} = 1; @foobarbuz{qw/foo barbar buzbuz BUZ Buzbuz/} = ('a' .. 'e');
結果は以下のように、key が条件に合致するものしか出力されません。
main:: %foo => {foo} => 1 at sample5.pl line 10. main:: %foo => {hoge}{foo} => 2 at sample5.pl line 12. main:: %bar => {bar} => 1 at sample5.pl line 14. main:: %bar => {barbarbar} => 3 at sample5.pl line 16. main:: %buz => {buzbuz} => 1 at sample5.pl line 19. main:: %foobarbuz => {foo} => 'a' at sample5.pl line 21. main:: %foobarbuz => {barbar} => 'b' at sample5.pl line 21. main:: %foobarbuz => {buzbuz} => 'c' at sample5.pl line 21.
同じ要領で、key を value に変えると、value に対する条件を指定できます。
メッセージを変更する
表示するメッセージを変えたいこともあるでしょう。例えば、キー 'foo' が入ってきたときの、キー 'var' の値が知りたいとか。
use Tie::Trace qw/watch/; watch my %foo, key => ['foo'], debug => sub {my ($self, $v) = @_; if (%{$self->storage}) { return $v . " {var} is " . $self->storage->{var} } else { return $v; } }; %foo = (foo => 1, var => 2); $foo{foo} = 2; $foo{var} = 3; $foo{foo} = 4;
'foo' が代入された行数でのみ、メッセージが出ています。
main:: %foo => {foo} => 1 at sample7.pl line 13. main:: %foo => {foo} => 2 {var} is 2 at sample7.pl line 14. main:: %foo => {foo} => 4 {var} is 3 at sample7.pl line 16.
キー 'bar' が入ってきたときの、親のハッシュリファレンスのキー 'xxx' の値が知りたいとか。
use Tie::Trace qw/watch/; my %parent = (child => {foo => 10, bar => 100}, xxx => "xxx"); watch %parent, key => ['bar'], debug => sub { my ($self, $v) = @_; if (my $p = $self->parent) { return "$v; parent->{xxx} is ". $p->storage->{xxx}; } else { return $v; } }; $parent{child}->{bar} = 1; $parent{xxx} = 10; $parent{child}->{bar} = 1000;
以下のようなメッセージとなります。
main:: %parent => {child}{bar} => 1; parent->{xxx} is xxx at sample8.pl line 15. main:: %parent => {child}{bar} => 1000; parent->{xxx} is 10 at sample8.pl line 17.
ここで使われている、storage メソッドは、実際のデータが入っているリファレンスを返し、parent メソッドは親のオブジェクトを返します。
注意事項
たいていのケースで特に問題なく動くとは思いますが、変数を変質させてますので、何か変なことが起きるかもしれません。そういうときは、素直に perl -d でもしてくださいませ。
まとめ
今回は、Tie::Trace を使った、お手軽な print デバッグについて解説しました。
一応日本語のドキュメントも用意してありますので、よろしければ、そちらもどうぞ。
というわけで今回はここまで。明日は id:masartz さんです。