スクリプトの勉強

第1回 perl(Smart::Comments)

2015.12.16

はじめに

最近、仕事で、いろいろスクリプト(perl/python/ruby)を使用しています。

昔々からスクリプト自体の言語文法は知っていたものの、まともに使うのは、初めてなので、各スクリプトにふさわしい実装ができず、作業が遅いと感じてます。

そのためには、とりあえず勉強、ということで、その勉強方法、および過程を書いてみようと思います。

今回は、お仕事で主に使っているperlを題材にします。

勉強方法

なんかの言語を学ぶ場合は、以下の方法を試してみることにしてます。

  1. 本を探す。

    言語文法を一通り理解するため。今回は、続・初めてのperl(電子書籍) を買いました。

  2. 既存のライブラリなどを修正してみる

    修正内容を検討する過程で、ライブラリの処理内容を見て、なるべく理解してみる。

今回は、既存のライブラリを修正してみようと思います。

題材として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は、以下な感じで作られています。

  1. 「ソースフィルタにより検索」

    ソースフィルタ(Filter::Simple)でソースを全検索。

  2. 「内部関数に置換」

    $intro(設定により異なるが、主に”###”という文字列)を検索し、Smart::Comments::_Dump等の内部関数に置換

  3. 「出力処理」

    置換した内部関数内でそれぞれの出力処理

上記理解のために、以下のようにデバッガで起動してみます。以下のように、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');

実装

処理概要を踏まえ、以下のように実装しました。

修正は、以下の意図で修正しています。

  1. 「内部関数置換」部に”### <trace>”の置換を追加(130行目付近)
  2. 「出力処理」部(_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

まとめ

なんらかの目的で修正している方が、ただソースを眺めるよりは勉強になったような気がします。

この「とりあえず修正してみる」方法の場合、あまりに難しい修正内容にすると、実装できず勉強にならない場合があるのですが今回は、あっさり実装できてよかったです。

また、ネタを見つけたら書いてみたいと思います。

著者プロフィール

tanino

過去にミドルウェア関連業務を担当。現在は組込系が主担当。

記事一覧Index