greple のリファクタリングと Getopt::EX

greple 改修中

そろそろ Perl 以外のプログラムにも手を出したくなってきているのだけど、そのためにはやり残したことをやっちゃってからにするかと、いろいろ棚卸し中なのである。 しかし、作りかけだったり未リリースだったりするものが多くて、なかなか片付かないという大掃除的状況が続いています。

というわけで、正月から greple を大改修中。 機能的には大きな変更はないのだが、シングルファイルで管理する限界を感じて、全体的に構成を見直してモジュール化を進めている。 やり始めてから意識したのだが、このような作業を巷ではリファクタリングと呼ぶらしい。 自分で書いたコードなので、修正した場合の影響範囲はわかっているつもりだが、案外予想しないところに影響が現れたりして、テスト環境が整っていないので確認が大変だ。 本格的な作業は十分なテストを伴わなければならないので、今やっているのは、なんちゃってリファクタリングだ。

作業中のファイルはまだ master には反映していなくて develop というブランチを切ってある。

Getopt::EX

さて、その作業の過程で greple で使っている rc ファイルと拡張モジュールの処理を独立したモジュールにしてみた。 とりあえず Getopt::EX という名前にしてある。

使い方は簡単で、Getopt::Long を Getopt::EX::Long に変えるだけだ。 すでに cdifsdif はこれに対応するようになっている。 もっとも、インストールしてない環境で使えないのは困るので、以下のようにして Getopt::Long にフォールバックするようになっている。 いずれ本格的に対応するつもりだが、現時点での修正はこれだけ。 もちろん OO 的インタフェースでも利用できる(はず...)。

eval {
    require Getopt::EX::Long;
    import  Getopt::EX::Long;
    1;
} or do {
    die if $! !~ /^No such file/;
    require Getopt::Long;
    import  Getopt::Long;
};

さて、これだけの修正で以下の2つの機能が使えるようになる。

  1. 自動的に rc ファイルを読み込むようになる。
  2. -M オプションで、拡張モジュールを読み込むようになる。

RC ファイル

たとえば sdif であれば、~/.sdifrc というファイルを読み込むようになる。 もちろん別のファイルを指定することもできるが、それにはプログラムに若干の修正が必要になる。

このファイルの中で、オプションを自由に定義することができる。 sdif / cdif は、デフォルトでは明るい色彩の端末で使用することを前提としているのだが、暗いバックグラウンド色を好む人のために --DARK_CMY というオプションを定義してみよう。

option  --DARK_CMY \
        --cm OCOMMAND=455/011;E         \
        --cm NCOMMAND=545/101;E         \
        --cm    OFILE=455/011;DE        \
        --cm    NFILE=545/101;DE        \
        --cm    OMARK=C/444             \
        --cm    NMARK=M/444             \
        --cm    UMARK                   \
        --cm    *LINE=Y                 \
        --cm [ON]TEXT=555/111;E         \
        --cm    UTEXT=                  \
        --cdifopts '--cm APPEND=DELETE=K/545,*CHANGE=K/455'

こうすることで、

% cdif --DARK_CMY

のように実行できるようになる。

これを常に付けて実行したければ default という名前でオプションを定義すればよい。 .sdifrc の中に次の行を追加するだけだ。

option default --DARK_CMY

シェルのエイリアスを使うのと決定的に違うのは、コマンドそのものが新しいオプションを受け入れるようになるという点である。 だから、別のコマンドから呼び出したり git 環境で使うような場合にも常に有効になる。

モジュール

先頭に -M ではじまるオプションがあると、それをモジュールの指定として取り扱う。

モジュールは App::sdif にあると仮定するので、たとえば colors というモジュールを指定すると App::sdif::colors という Perl モジュールを読み込む。

これは Perl のモジュールだが、パッケージ宣言だけあれば中身はなくてもいい。そして __DATA__ セクションに書いてある内容を RC ファイルと同じように解釈する。

package App::sdif::colors;
1;
__DATA__
define :CDIF      APPEND=DELETE=K/545,*CHANGE=K/455
define :DARK_CDIF APPEND=DELETE=555/311,*CHANGE=555/113
option  --green \
        --cm *COMMAND=010/555;SE        \
        --cm    *FILE=010/555;SED       \
        --cm [ON]MARK=010/444           \
        --cm    UMARK=                  \
        --cm    *LINE=220               \
        --cm [ON]TEXT=K/454;E           \
        --cm    UTEXT=                  \
        --cdifopts '--cm :CDIF'

この例だと --green というオプションが有効になる。このようにマクロを使用することもできる。

端末の背景色によってオプションを変えてみる

オプションが定義できるだけでも結構便利なのだが、スクリプトを活用すると動的にオプションを変更することもできる。 実行された時刻によってパラメータを変えるのなんかはオチャノコだ。 試しに、使用している端末の背景色によってオプションを変更してみよう。

App::sdif::osx_autocolor というモジュールを次のように定義する。

package App::sdif::osx_autocolor;

use strict;
use warnings;

sub brightness {
    my $app = "Terminal";
    my $do = "background color of first window";
    my $bg = qx{osascript -e \'tell application \"$app\" to $do\'};
    my($r, $g, $b) = $bg =~ /(\d+)/g;
    int(($r * 30 + $g * 59 + $b * 11) / 65535); # 0 .. 100
}

sub initialize {
    my $bucket = shift;
    $bucket->setopt(
        default =>
        brightness > 50 ? '--LIGHT_SCREEN' : '--DARK_SCREEN');
}

1;

細かい使い方はドキュメントを読んでほしいが、initialize というサブルーチンが用意されていればそれが実行され、引数としてそのファイルを処理するためのオブジェクトが渡される。そのオブジェクトを通して、RC ファイルで書けることは何でも指定できるし、スクリプトでできることはなんでもできる。

ここでは、AppleScript を使って OS X の Terminal アプリから背景色を取得し、輝度を計算して 0 から 100 の値を得る。 それが 50 より大きければ --LIGHT_SCREEN、小さければ --DARK_SCREEN というオプションをデフォルトとしている。 これらのオプションはこのモジュールの中では定義されていなくて、ユーザが .sdifrc などで設定することを想定している。 たとえば、こんな具合だ:

# ~/.sdifrc
option default -Mcolors -Mosx_autocolor --onword
option --LIGHT_SCREEN --cmy
option --DARK_SCREEN  --dark_cmy

僕の ~/.gitconfig は

[pager]
    log = less
    show = sdif -n --cdif | less -cR
    diff = sdif -n --cdif | less -cR

のように設定してあるので、こうすることで git diff などの出力が次のように変わる。

デフォルトカラー

f:id:uta46:20150202141635p:plain

明るい端末では --cmy オプションを指定

f:id:uta46:20150202142010p:plain

暗い端末では --dark_cmy を指定

f:id:uta46:20150202141645p:plain

明るい端末で --mono オプションを指定

f:id:uta46:20150202163021p:plain

暗い端末で --dark_mono オプションを指定

f:id:uta46:20150202165339p:plain

--dark_mono が意外とクールだ。 これでもちゃんと修正箇所はわかる。 実は Getopt::EX の方では 24 段階のグレイスケールが使えるので、もっと微妙な設定が可能なのだが、sdif がまだそれに対応していない。

とまあ、既存の Perl スクリプトを1行変更するだけでこんな感じのことができるようになるので、結構イカしてるんじゃないかと思うのだけど、いかがでしょう? 興味のある人は使ってご意見など頂けると嬉しい。手伝ってもらえるともっと嬉しい。 今の所 grepledevelp ブランチに入っているので、clone して PERLLIB に入れてもらうのがよいかと思います。

今作った App::sdif::colorsgithub にあげておこう。

他にもいろいろできる

Getopt::EX::Colormap

Getopt::EX には Getopt::EX::Colormap というモジュールが含まれていて、上で書いたような色を指定するオプションの処理や、実際に色を付けて出力する部分を実装している。 cdif / sdif は、まだこれに対応していないがいずれするつもりだ。 一点だけ、小文字の色指定の解釈の仕方が変更されている。 今までは rgbcmy を小文字で指定すると背景色の意味になったのだが、新しいモジュールでは ANSI 16色の後半8色を表すようになっている。 背景色を指定したい場合には / の後に書く。 これから使う人は、当面小文字を使わないことをお勧めします。

これは、別モジュールにしてもよさそうなものなのだが、この後の Getopt::EX::Func も絡んできて、なかなか切り離せない関係にある。

Getopt::EX::Func

オプションを定義するだけだと、本来持っている機能を組み合わせるだけなのだけど、モジュールで定義した機能をオプションから呼び出すことができる仕掛けが入っている。 これを活用すると、スクリプト本来の機能を拡張することができる。

まあ、これは使ってみないとわからないだろう。greple の改修作業の過程で、PGP の機能を拡張モジュールに切り出した。 このモジュールによって、スクリプト本体には何も加えずに、PGP で暗号化されたファイルが検索できるようになる。 面倒な作業は App::Greple::PgpDecryptor というモジュールがやってるんだけど、インタフェースの拡張はこれだけだ。 .gpg というサフィックスを持つファイルに対して、ここで定義している App::Greple::pgp::filter という関数を入力フィルタとして挿入するオプションを定義している。 フィルタが呼び出されると、処理するファイルは STDIN に開かれているので、fork して適切なコマンドを実行するという寸法だ。

package App::Greple::pgp;
use App::Greple::PgpDecryptor;
my  $pgp;
our $opt_pgppass;
sub activate {
    ($pgp = new App::Greple::PgpDecryptor)
        ->initialize({passphrase => $opt_pgppass});
}
sub filter {
    activate if not defined $pgp;
    $pgp->reset;
    my $pid = open(STDIN, '-|') // croak "process fork failed";
    if ($pid == 0) {
        exec $pgp->decrypt_command or die $!;
    }
    $pid;
}
1;
__DATA__
option default --if s/\\.(pgp|gpg|asc)$//:&App::Greple::pgp::filter
builtin pgppass=s $opt_pgppass // pgp passphrase

パッケージングできなくてリリースできません...

今までは、インストール作業の煩雑さが嫌いで、ファイルをコピーすれば動くようなものしか作ってこなかった。 現在の greple はモジュールに分割されてはいるが、clone するなり tz を展開するなりして、実行形式を直接指定するかシンボリックリンクを張れば実行できるようになっている。こんな風に書いてあるからなんだけどね。

BEGIN {
    if ((my $lib = abs_path($0)) =~ s{/ \K (?:bin/)? \w+ $}{lib}x) {
        push @INC, $lib if -d "$lib/App/Greple";
    }
}

とはいえ、ちゃんとしたインストーラーとテスト環境を作らないといけないなあとは思うのだけど、どうも今時の開発手法が使えませんよ。 Minilla とか Milla とか使おうとしてみたんだけど、Yosemite にうまくインストールできない。 どうしたらいいんでしょう。 CPAN も使ったことなかったので、とりあえず PAUSE のアカウントだけは作った (イマココ)。