この記事は *.pm
による Perl のクラス定義を CLI から直接的に試せるようにする開発技法の提案・解説です。
はじめに
Perl のモジュールを CLI コマンドとしても使えるようにする手法は Modulino (モジュリーノ=コマンドとしても実行可能なモジュール) と呼ばれます。この記事では私が長年探求してきた Modulino を OOP と組合せて Perl 開発に役立てる方法の、最も基礎となる部分について解説します。 (OO Modulino 技法とでも言いましょうか)
要約すると
- クラス定義を Modulino にし、CLIから起動できるようにしておく
- CLI へのサブコマンドをメソッドに対応付けて dispatch する
- CLI への posix 風オプション(
--name=value
) を new の引数にする
というアプローチが良いという考えです。
復習: Modulino(モジュリーノ) とは
Modulino とは、 Perl の正当なモジュールファイルであり、 なおかつ端末からコマンドとして実行することも出来るような .pm ファイルのことです。
例えば以下のような(Modulino ではない)普通のクラス定義モジュールファイル A.pm
を考えます。
(ここでは説明を簡略化するため use strict; use warnings; を省略します。)
package A; sub new { my $class = shift; bless +{@_}, $class } sub hello { my $self = shift; join " ", "Hello", $self->{name} } 1;
これをコマンド行から試すには、こう書く必要があります。
% perl -I. -MA -le 'print A->new(name => "world")->hello' Hello world %
そこそこ長いですね。初心者に打たせるのは厳しい感じですし、 Perl に慣れた人でも少しダルく感じるのではないでしょうか。
これを Modulino に書き換えてみましょう。 unless (caller)
という見慣れない構文が鍵となります。
#!/usr/bin/env perl package A; unless (caller) { # コマンド行から呼ばれた時にしたい処理をここに書く。例えば… print A->new(name => shift)->hello, "\n"; } sub new { my $class = shift; bless +{@_}, $class } sub hello { my $self = shift; join " ", "Hello", $self->{name} } 1;
これなら、コマンド行から即座に実行できます。 ファイル名に shell の補完も効きますし。
% chmod +x A.pm % ./A.pm world Hello world
また、perlデバッガーを呼び出したい時も頭に perl -d
を付けるだけです。余計なコーディングは不要です。
% perl -d ./A.pm world
OOP なモジュールの unless caller には何を書くと便利か
サブコマンドをメソッドに
先程の A.pm は unless caller ブロックで A->hello
を直接呼び出していました。
しかし大抵のクラスのオブジェクトは複数のメソッドを持つでしょう。 他のメソッドを試したい時は、やはり CLI に長いコマンドを書く必要が生じます。
% perl -I. -MA -le 'print A->new(name => "world")->goodnight'
なら git のようにサブコマンドを取ることにして、
それをメソッド名として解釈してはどうでしょう?つまり
unless caller
ブロックに第一引数に基づく dispatch 処理を組み込むのです。
(引数不足のエラー処理は省略します)
unless (caller) { my $self = A->new(name => "world"); my $cmd = shift; # CLI の第一引数をメソッド名に使う print $self->$cmd(@ARGV), "\n"; }
これで任意のメソッド名を呼び出すことが出来ます
% ./A.pm goodnight Good night world
posix long オプションを new の引数に
任意のメソッドを呼び出すことは出来るようになりました。…けど、まだ不満が有りますね?
そう、new の引数が name => "world"
に固定なのです。これもコマンド行から変更可能にしたい。
これも git のように、サブコマンドの前の --name=value
スタイルの引数列を全て new に渡すようにしては
どうでしょう?
追記: --name value
のような書き方が使えず不満を感じる人もいるかもしれません。その場合は こちら の記事も読んでみて下さい
unless (caller) { # CLI から渡された --name=value 形式の引数を全て new に渡す my $self = A->new(name => 'world', _parse_posix_opts(\@ARGV)); my $cmd = shift; print $self->$cmd(@ARGV), "\n"; } sub _parse_posix_opts { my ($list) = @_; my @opts; while (@$list and $list->[0] =~ /^--(?:(\w+)(?:=(.*))?)?\z/s) { shift @$list; push @opts, $1, $2 // 1; } @opts; }
これで new の引数も CLI から制御できるようになりました。
% ./A.pm hello
Hello world
% ./A.pm --name=Perl hello
Hello Perl
まとめ
Perl のクラス定義に unless (caller)
ブロックを加えて、
オブジェクトの挙動をコマンド行から簡単に試せるようにする技法の基礎について
解説しました。
OO Modulino には他にも
- Modulino の
@INC
調整にFindBin
を使った場合に起きる問題とその対処法 - オブジェクトのメンバー変数を
use fields
で宣言しておき、メソッドの引数宣言をmy TYPE $self
のようにクラス名も書くことで、メンバー変数の typo をコンパイル時に検出する- 加えて new 時に
fields::new($class)
を使うことで、不正なオプションを渡されても実行時エラーにする
- 加えて new 時に
- メソッドの戻り値は
Data::Dumper
やJSON
を使ってシリアライズして出力する- CLI 専用に作ったメソッドと一般のメソッドを区別できるよう、
cmd_
などのメソッド名接頭辞を予め決めておく。
- CLI 専用に作ったメソッドと一般のメソッドを区別できるよう、
- CLI の引数に
{..}
[..]
が現れた場合は JSON として扱う ./A.pm <TAB>
でメソッド名やオプション名を補完する zsh completer
などの発展的な話題があります。それらを全て盛り込んだベースクラス MOP4Import::Base::CLI_JSON の話も、機会が有れば書いてみたいと思います。
また、新人教育に Modulino を取り入れる方法のメリットについてはこちらの記事も参考にどうぞ>
-nle と .pm から始める Perl 入門とかどうかしら?(後編) - hkoba blog
ここまで読んで下さりありがとうございました!