Perl Hackers Hub

第45回Perlで作るコマンドラインツール―オプション、サブコマンド、設定ファイルへの対応(1)

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはふしはらかんさんで、テーマは「Perlで作るコマンドラインツール」です。

本稿のサンプルコードは、本誌サポートサイトから入手できます。

コマンドラインツールを作ろう

本連載を読まれているみなさんは、コマンドラインツールを使ったことのある人がほとんどかと思います。また、コマンドラインツールを作ったことのある人も多いでしょう。Perlはほとんどの環境に標準で導入されていて、強力なテキスト処理を備えていることもあり、シェルスクリプトで書くのはしんどいかなという程度のものから、かなり複雑で高負荷な処理を要するものまで、幅広くコマンドラインツールの記述に使われています。

今回は、コマンドラインツールをPerlで作る場合に使用すると便利なモジュールを、コマンドライン引数解析ライブラリであるSmart::Optionsを中心に紹介していきます。

Smart::Optionsによるコマンドライン引数の利用

コマンドラインツールを実行する際に、プログラム名に続いて渡す文字列をコマンドライン引数と言います。本節では、コマンドライン引数とそれを使ったオプションの渡し方の仕様から、Smart::Optionsの具体的な使い方までを見ていきましょう。

コマンドライン引数はなぜ必要か

コマンドライン引数は空白で区切って複数の値を渡すのが基本ですが、さまざまな設定値をオプションとして渡せるようになっている場合が多く、その仕様がPOSIXPortable Operating System Interface for UNIXやGNUでガイドラインとして定められています。

$ command -a -bc -d オプション値 --execute --file=/path/
to/file 引数1 引数2 引数3

-(ハイフン)から始まる英字1字(例:-aがオプション名です。-bcのように英字を複数書くと複数のオプションを一度に指定できます。

オプションに値を持たせる場合、-d オプション値のように空白を空けてから値を書きます。もし値に空白を含めたい場合は、値全体を-d "値 値"のようにダブルクオートで囲みます。

オプション名を2文字以上にしたい場合は--(ハイフン2文字)から始めるようにします(例:--execute⁠。この場合、値を渡す場合に空白を空けるだけなく、=(イコール)で結ぶ書き方を使うこともできます(例:--file=path⁠。GNUスタイルでは、--versionでのバージョン表示と、--helpでのUsage(コマンドの使い方)の表示が必須です。

多くのプログラミング言語では、コマンドライン引数を空白で区切った文字列の配列として受け取れるようになっています。Perlもコマンドライン引数を特殊変数@ARGVとして参照できるようになっています。

cmd.pl

#!/usr/bin/perl

use strict;
use warnings;

print join("\n", @ARGV);

このコードを実行すると、渡されたコマンドライン引数を改行で区切って出力します。

$ perl cmd.pl -a -bc -d FooBar arg1 arg2
-a
-bc
-d
FooBar
arg1
arg2

引数をシンプルに利用する

前項のサンプルコードでコマンドライン引数を配列として取得できましたが、それをPOSIXスタイルに沿ったオプションとして扱うのは手間です。たとえば-bcというオプションからbオプションとcオプションが有効になっていることを知ったり、dオプションの値としてFooBarが渡されていることを知ったりするには、@ARGVを解析するプログラムを実装する必要があります。

PerlにはGetOpt::Longというモジュールが標準添付されているほか、CPANにさまざまなコマンドライン引数解析ライブラリが登録されていて、それらを使ってコマンドライン引数の解析ができます。今回はその中から、拙作のSmart::Optionsというライブラリを紹介します。

Smart::OptionsはNode.jsのコマンドライン引数解析ライブラリであるoptimistを参考に実装したもので、前述のPOSIX、GNUスタイルなオプション値をシンプルな書き方で解析して使えるようにしたものです。試しに先ほどのコードをSmart::Optionsを使った形にあらためてみます。

cmd_smart.pl

#!/usr/bin/perl

use strict;
use warnings;

use Data::Dumper;
use Smart::Options;

print Dumper(argv(@ARGV));

argv()Smart::Optionsによってインポートされた関数で、引数に渡された配列を解析してハッシュリファレンスの形で返します。Data::Dumperでハッシュの中身をそのまま出力しているので、このコードを実行すると次のようになります。

$ perl cmd_smart.pl -a -bc -d FooBar arg1 arg2
$VAR1 = {
    '_' => [
            'arg1',
            'arg2'
            ],
    'd' => 'FooBar',
    'c' => 1,
    'b' => 1,
    'a' => 1
};

結果を見てわかるとおり、オプションの項目名-a-bなど)がハッシュのキーになり、オプションの値がハッシュの値として登録されています。また、オプションに関わらないただの引数はキー名が_の項目に配列リファレンスの形で登録されています。-aなどの値を持たないオプション(フラグ)は、Perlで真trueになる1が値として入っていますので、

my $opt = argv();
if ($opt->{a}) {
    # -aが渡されたときにやる処理
}

のように書けます。ちなみにこのコードではargv()に引数を渡していませんが、その場合は@ARGVを引数として使用します。

このようにSmart::Optionsを導入してargv()を使うだけで、コマンドライン引数からオプション値を取り出すのが簡単になります。

オプションを定義して便利に使う

Smart::Optionsではオプションの定義を事前に登録することで、より便利に使えます。

my $opt = Smart::Options->new;
$opt = $opt->alias(f => 'file');
print Dumper($opt->parse(qw/-f filename/));

先ほどまではargv()関数を使ってきましたが、これは実はSmart::Options->new->parseのショートカットです。Smart::Optionsの本来の使い方は、上記のようにSmart::Optionsのオブジェクトを生成してオプション定義を登録していき、最後にparseメソッドを実行して戻り値を受け取る、という方法になります。サンプルコード内のaliasメソッドを含め、これから紹介するオプション定義のメソッドは戻り値として(定義が追加された)オブジェクト自身を返すので、メソッドチェインで複数の定義を1行で書いてそのままparseを実行できます。

Smart::Options->new->alias(f => 'file')
                   ->demand('f')
                   ->describe(f => 'File Name')
                   ->parse();

alias ─⁠─ オプションに別名を付ける

aliasはオプションの別名を定義します。fileというオプション名を短くfでも指定できるようにする、などに使えます。

my $opt = Smart::Options->new;

$opt->parse(qw/-f filename/);
# => { 'f' => 'filename' }

$opt->alias(f => 'file')->parse(qw/-f filename/);
# => { 'file' => 'filename' }

以降のコード例では同様の表記が続きますので、変数$optはあらかじめSmart::Options->newで初期化されているものとします。実際に動かしてみる場合は、冒頭にmy $opt = Smart::Options->new;の一文を追記してください。

demand ─⁠─ オプションを必須にする

demandはオプションの指定を必須にします。コマンドライン引数で当該オプションが指定されていない場合、エラーになります。

$opt->parse(qw//);
# => { }

$opt->demand('file')->parse(qw//);
# => Error

boolean ─⁠─ フラグオプションを強制する

booleanは、指定したオプションがフラグ(値をとらず、オン/オフの判定だけに使われる)として使われることを強制します。次のコードのように引数の解析結果に影響します。

$opt->parse(qw/--flag 100/);
# => { 'flag' => '100' }

$opt->boolean('flag')->parse(qw/--flag 100/);
# => { 'flag' => '1', '_' => ['100'] }

default ─⁠─ オプションのデフォルト値を指定する

オプションに初期値を持たせるにはdefaultを使います。コマンドライン引数でオプションが指定されていなくても、戻り値に値が含まれるようになります。

$opt->parse(qw//);
# => { }

$opt->default('foo' => 'bar')->parse(qw//);
# => { 'foo' => 'bar' }

help ─⁠─ オプションの使い方を表示する

helpメソッドを実行すると、定義したオプションの詳細を列挙した使い方(Usage)の文字列を返します。

print $opt->describe('foo' => 'Sample option')->help();
# => Usage: /data/cmd_smart_ex.pl

Options:
  --foo       Sample option
  -h, --help  Show help

describeを使うとオプションの説明を登録し、Usageで出力させることができます。先ほどのコードでは定義していない--helpというオプションが表示されていますが、これはSmart::Optionsであらかじめ定義されているオプションで、Smart::Options->new->parse(=argv())を実行したときに-hあるいは--helpというオプションがあった場合はUsageを出力してプログラムを終了します。

なお、GNUスタイルで--helpオプションとともに必須となっている--versionオプションはあらかじめ定義はされません。コマンドラインツールのバージョンを指定するルールがPerlに存在しないためです。

type ─⁠─ オプション値の型をチェックする

typeを使うと、オプションの値の種類に制限をかけられます。

typeとしては、Str(文字列⁠⁠、Int(整数⁠⁠、Num(数値全般)などを指定できるようになっています。たとえばオプションの値として数値を期待している場合、次のようなコードになります。

$opt->type('num' => 'Int')->parse(qw/--num=string/);
# Error: Value 'string' invalid for option num(Int)

さらに、coerceを使うと独自のtypeを定義できます。たとえばオプションで年/月/日という文字列を受け取って、プログラム中ではTime::Pieceのインスタンスとして扱いたい場合、Timeというtypeを定義します。

use Time::Piece;

my $r = $opt->coerce(Date => 'Str',
  sub { Time::Piece->strptime($_[0], '%Y/%m/%d') }
)->type('day' => 'Date')
 ->parse(qw{--day=2017/06/24});

# $r->{day}はTime::Pieceのオブジェクトになっている

options ─⁠─ オプションを一括で定義する

ここまで、いろいろなメソッドでオプションの定義を追加する方法を説明してきましたが、optionsメソッドを使うと、1つのオプションに複数の定義を行うこともできます。

optionsを使ってfileオプションを定義する

$opt->options('f' => {
    alias => 'file',
    default => '/path/to/file',
    describe => 'file path',
})->parse();
optionsを使わずに書いた場合
$opt->alias('f' => 'file')
    ->default('file' => '/path/to/file')
    ->describe('file' => 'file path')->parse();

特殊なコマンドライン引数

Smart::Optionsでは、いくつかの特殊なオプションの書き方に対応しています。

解析の打ち切り

Smart::Optionsでは、--というオプション以降はオプションの解析を打ち切って、ただの引数として解釈します。

$ perl cmd_smart.pl -a -b 100 -- -c -d 100
$VAR1 = {
        'b' => '100',
        'a' => 1,
        '_' => [
                '-c',
                '-d',
                '100'
                ]
    };

--以降の-c-d 100というコマンドライン引数はそのまま解釈されて、_に配列として渡されます。cartonexecのように、ほかのコマンドとその引数をコマンドライン引数として受け取って内部で実行するようなプログラムで役に立ちます。

オプションの否定

--no-fooのようにオプション名の先頭にno-を付けると、それに続くオプションが偽falseになります。通常は単にそのオプションを指定しなければよいだけなのですが、たとえばfooオプションがデフォルトでtrueになるように設定されている場合に、明示的にfalseするために使います。

オプション値のネスト

--foo.barのようにオプション名の中に.(ドット/ピリオド)を含めると、オプション値を階層構造にできます。

$ perl cmd_smart.pl --foo.bar=10 --foo.baz=20
$VAR1 = {
        '_' => [],
        'foo' => {
                    'baz' => '20',
                    'bar' => '10'
        }
};

オプションの種類が多くてオプション名が衝突しそうなときなどに使えます。もっとも、そこまで複雑になってきたら後述する設定ファイルを検討したほうがよいケースも多いでしょう。

Smart::Options::Declareによる直感的なコマンドライン引数の利用

argv()を実行して解析結果のハッシュリファレンスを受け取るSmart::Optionsの基本的な使い方は直感的ですが、さらに直感的な書き方を使うことができます。それには、Smart::Optionsに付属するSmart::Options::Declareというモジュールを使います。

use Smart::Options::Declare;

opts my $foo => 'Str',
     my $bar => { isa => 'Int', default => 4 };

optsに続いてmyで変数定義をすると、変数名と同じオプションの値が変数に入ります。typeやデフォルト値なども定義できます。

上記のコードでは、@ARGVの中身が--foostringの配列のとき、$fooの内容が"string"という文字列、$barの内容が数値の4になります。

サブコマンドを利用する

Gitコマンドは、addcommitpullpushなどいろいろなサブコマンドを指定して複数の機能を切り替えて実行できます。

fileをステージに追加する
$ git add file
originリポジトリにローカルの変更をpushする
$ git push origin master

Smart::Optionsでは、このようなサブコマンドを使うコマンドラインツールを実装するためのしくみも持っています。

subcmd.pl
# コマンド全体のオプションの指定
$opt->boolean('force')->alias(f => 'force');

# addサブコマンドを定義
$opt->subcmd(add => Smart::Options->new->demand('file'));

# pushサブコマンドを定義
my $push_cmd = Smart::Options->new
                   ->describe(r => 'repository');
$opt->subcmd(push => $push_cmd);

my $result = $opt->parse(qw/-f add --file=filename/);
変数$resultの中身
{
    'force' => 1,
    'command' => 'add',
    'cmd_option' => {
        file => 'filename',
        '_' => []
    }
}

subcmdメソッドに、サブコマンド名と、サブコマンドのオプションを定義するSmart::Optionsのオブジェクトを渡してサブコマンドを定義していきます。parseメソッドの戻り値のキー名commandに指定されたサブコマンドが、キー名cmd_optionにコマンドラインオプションの解析結果が入ります。

<続きの(2)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧