hkoba blog

プログラマーです。プログラミング言語ミーハーです。ツッコミ歓迎です。よろしくどうぞ(能代口調)

Modulino + OOPの提案 - CLIからオブジェクトと遊ぼう

この記事は *.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 に渡すようにしては どうでしょう?

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) を使うことで、不正なオプションを渡されても実行時エラーにする
  • メソッドの戻り値は Data::DumperJSON を使ってシリアライズして出力する
    • CLI 専用に作ったメソッドと一般のメソッドを区別できるよう、 cmd_ などのメソッド名接頭辞を予め決めておく。
  • CLI の引数に {..} [..] が現れた場合は JSON として扱う
  • ./A.pm <TAB> でメソッド名やオプション名を補完する zsh completer

などの発展的な話題があります。それらを全て盛り込んだベースクラス MOP4Import::Base::CLI_JSON の話も、機会が有れば書いてみたいと思います。

また、新人教育に Modulino を取り入れる方法のメリットについてはこちらの記事も参考にどうぞ>
-nle と .pm から始める Perl 入門とかどうかしら?(後編) - hkoba blog

ここまで読んで下さりありがとうございました!