はじめに¶
最近、仕事で、いろいろスクリプト(perl/python/ruby)を使用しています。
昔々からスクリプト自体の言語文法は知っていたものの、まともに使うのは、初めてなので、各スクリプトにふさわしい実装ができず、作業が遅いと感じてます。
そのためには、とりあえず勉強、ということで、その勉強方法、および過程を書いてみようと思います。
今回は、お仕事で主に使っているperlを題材にします。
勉強方法¶
なんかの言語を学ぶ場合は、以下の方法を試してみることにしてます。
- 本を探す。
言語文法を一通り理解するため。今回は、続・初めてのperl(電子書籍) を買いました。
- 既存のライブラリなどを修正してみる
修正内容を検討する過程で、ライブラリの処理内容を見て、なるべく理解してみる。
今回は、既存のライブラリを修正してみようと思います。
題材としてSmart::Commentsというライブラリにしました。
Smart::Comments¶
Smart::Comments とは、デバッグ用のコメント出力用のライブラリです。
デバッグ用に、変数の内容をprint文出力するデバッグ(いわゆるprint文デバッグ)することは良くあります。
そして、デバッグが終わった後、デバッグ文を消し忘れる、というのは良くあることです。しかも、デバック文自体再利用する羽目になることも時々あります。
Smart::Commentsを利用すると、そこらへんがうまくいきます。以下のようなコードで、print文デバッグをすることができます。”###”というコメントに3つ連続(以上)記述すると開始します。
vi a.pl
#!/usr/bin/perl
use Smart::Comments;
my $a=1;
### $a
(上記を保存)
$ perl a.pl
### $a: 1
そして、デバッグ文を消したければ、use Smart::Commentsをコメントアウトすればデバック文は消えます。(もともと#はコメントですし)
なお、使用方法としては、use Smart::Comments;とするのではなく、以下のように起動時に -MSmart::Commentsとする方法もあります。
perl -MSmart::Comments a.pl
いろいろ試した結果、自分的には、デバッグするときは、すぐに起動したいと思うようです。
なので、ソースにuse Smart::Commentsを記述し、リリース時には
ag -f --perl | xargs perl -p -i -e's/^use Smart::Comments/#use Smart::Comments/g'
してSmart::Commentsをコメントアウトする方針にしています。
なお余談ですが、agも便利なコマンドです。要するにgrepコマンドです。(説明は割愛)
修正前準備(Smart::Commentsのインストール(cpanm使用))¶
修正するライブラリをインストールする前に、cpanmをインストールします。プログラムひとつの、お手軽構成なので使用しています。
インストールは以下の通りです。
curl -L http://cpanmin.us | perl - App::cpanminus
Smart::Commentsは、修正しやすいようにホームディレクトリにインストールします。
cpanm Smart::Comments -l ~/perl5
以下が実行時にローカルを見るよう、~/.bashrcに設定した環境変数です。
export PERL5LIB="/home/tanino/perl5/lib/perl5/x86_64-linux-gnu-thread-multi:/home/tanino/perl5/lib/perl5";
export PATH="/home/tanino/perl5/bin:$PATH";
Smart::Commentsへの追加仕様¶
勉強用の修正として、以下の仕様を追加します。
### <trace>
### <func>
私は、デバッグをする際、処理の流れを見るため、関数名を出力することが良くあります。
<here>などのログ出力場所(__FILE__,__LINE__)を出力する機能は既にありますが、関数名を出力する機能がありませんでしたので、追加しようと思います。
<trace>は、3階層まで、呼び出し元を表示します。<func>は、呼び出している時点で処理している関数名を表示します。
Smart::Commentsの処理概要¶
追加するために、Smart::Commentsの処理を軽く見てみましょう。Smart::Commentsは、以下な感じで作られています。
- 「ソースフィルタにより検索」
ソースフィルタ(Filter::Simple)でソースを全検索。
- 「内部関数に置換」
$intro(設定により異なるが、主に”###”という文字列)を検索し、Smart::Comments::_Dump等の内部関数に置換
- 「出力処理」
置換した内部関数内でそれぞれの出力処理
上記理解のために、以下のようにデバッガで起動してみます。以下のように、a.plの6行目(“### $a”の部分)がSmart::Comments::_Dump関数に変更されていることがわかります。
$ perl -d a.pl
Loading DB routines from perl5db.pl version 1.39_10
Editor support available.
Enter h or 'h h' for help, or 'man perldebug' for more help.
main::(a.pl:5): my $a=1;
DB<1> c
### $a: 1
at /home/tanino/perl5/lib/perl5/Smart/Comments.pm line 481.
Smart::Comments::_Dump('pref', '$a:', 'var', 'ARRAY(0x2521be8)') called at a.pl line 6
main::(a.pl:8): my @b=('aa','bb','cc');
実装¶
処理概要を踏まえ、以下のように実装しました。
修正は、以下の意図で修正しています。
- 「内部関数置換」部に”### <trace>”の置換を追加(130行目付近)
- 「出力処理」部(_Dump)に<trace>用処理を追加(439行目付近)
パッチは以下の通りです。
*** /home/tanino/perl5/lib/perl5/Smart/Comments.pm 2015-10-25 06:29:34.000000000 +0900
--- Comments.pm 2015-11-23 11:17:52.068027644 +0900
***************
*** 18,23 ****
--- 18,24 ----
my $average_over = 5; # Number of time-remaining estimates to average
my $minfillreps = 2; # Minimum size of a fill and fill cap indicator
my $forupdatequantum = 0.01; # Only update every 1% of elapsed distance
+ my $repeatcaller = 4;
# Synonyms for asserts and requirements...
my $require = qr/require|ensure|assert|insist/;
***************
*** 129,134 ****
--- 130,139 ----
s{ ^ $hws* $intro $hws* (.+ [.]{3}) $hws* $ }
{Smart::Comments::_Dump(pref=>qq{$1});$DBX}gmx;
+ # Dump trace|func expression (the expression is not used as the label)...
+ s{ ^ $hws* $intro $hws* <(trace|func)> $optcolon $hws* $ }
+ {Smart::Comments::_DumpTrace(pref=>q{$1:});$DBX}gmx;
+
# Dump an unlabelled expression (the expression is used as the label)...
s{ ^ $hws* $intro $hws* (.*) $optcolon $hws* $ }
{Smart::Comments::_Dump(pref=>q{$1:},var=>Smart::Comments::_quiet_eval(q{[$1]}));$DBX}gmx;
***************
*** 482,487 ****
--- 487,516 ----
$prev_STDOUT = tell(*STDOUT);
}
+ #for trace/func
+ sub _DumpTrace {
+ my %args = @_;
+ my ($pref) = @args{qw(pref)};
+
+ my @fc = ();
+ for ( my $i = 1 ; $i < $repeatcaller ; $i++ ) {
+ my (undef, undef, undef, $func) = caller($i);
+ push(@fc,$func) if defined $func;
+ }
+ my (undef, $file, $line) = caller;
+ my $f = $fc[0] // '';
+ my $t = " trace:\n### " . join("\n### ",reverse(@fc)) ;
+ $pref =~ s/(?:func)/"$f", "$file", line $line/g;
+ $pref =~ s/(?:trace)/$t "$file" , line $line/g;
+
+ $pref =~ s/:$//;
+ print STDERR "\n";
+ warn "### $pref\n";
+ $prev_STDOUT = tell(*STDOUT);
+ $prev_STDERR = tell(*STDERR);
+ return;
+ }
+
1; # Magic true value required at end of module
__END__
動作確認¶
以下のコード(s.pl)で動作確認しました。
#!/usr/bin/perl
package TEST;
use strict;
use warnings;
use Smart::Comments;
foo();
sub foo {
bar();
}
sub bar {
baz();
}
sub baz {
### <trace>
### <func>
}
### <trace>
### <func>
動作結果は以下の通りです。
$ perl s.pl
### trace:
### TEST::foo
### TEST::bar
### TEST::baz "s.pl" , line 22
### "TEST::baz", "s.pl", line 23
### trace:
### "s.pl" , line 26
### "", "s.pl", line 27
まとめ¶
なんらかの目的で修正している方が、ただソースを眺めるよりは勉強になったような気がします。
この「とりあえず修正してみる」方法の場合、あまりに難しい修正内容にすると、実装できず勉強にならない場合があるのですが今回は、あっさり実装できてよかったです。
また、ネタを見つけたら書いてみたいと思います。