コマンドライン引数の処理あれこれ

PHPといえば「ほーむぺーじ」を作るためにあるような言語なわけだが、何故か自分はコマンドラインアプリを作る機会が多い*1ので、引数のパースについて少し考えてみる。


対象ファイルとスイッチのオンオフ程度なら$argvを回しながら自前でパースしても事足りるんだけど、オプションに引数が付いたり省略可能だったりと複雑になってくると何らかのライブラリを頼った方が圧倒的に簡単になる。

選択肢

コマンドラインの解析にはいくつかの方法がある。

  • 標準のgetopt()関数
  • PEARのConsole_Getopt
  • PEARのConsole_GetoptPlus
  • PEARのConsole_Getargs
  • PEARのConsole_CommandLine
  • Zend FrameworkのZend_Console_Getopt

メジャーなところだとざっとこんなもん。
PEARの4つについてはPEAR :: Manual :: オプションパーサの比較を見るとそれぞれの特徴がわかる。

標準のgetopt()関数

コマンドラインの引数解析を簡単に! - アシアルブログによると、不明なオプションを判断できなかったり後ろの引数が取れなかったりと非常に残念な実装のようなので、採用しないほうがよさそう。

Console_Getopt

getoptをまともにしたようなライブラリ。
PEAR自身が依存しているライブラリなので、PEARが入っている環境なら即使えるのが強み。
コマンドライン引数をうまく切り分けてくれる以上のことはしてくれない。
連想配列に直したりデフォルト値で埋めたりヘルプを出したりは自分で実装しなければならないので結構だるい。ループのswitch文がひたすら長くなる。
多くのことはしてくれないということは、何でも好きなようにできるとも言える。


気になる点は、パラメータ省略可能なオプションを定義したときに'-m hoge'じゃなくて'-mhoge'じゃないとパースしてくれないこと。

Console_GetoptPlus

比較表によるとgetoptをPHP5用に作り直して若干機能追加したものということだが、何せbetaなので……。
stableになったら改めて検討してみることにしよう。

Console_Getargs

(たぶん連載) Console_Getargsで書いてみるによると、とても微妙な仕様であることがわかる。
使い勝手悪そうなので自分はパス。

Console_CommandLine

アシアルさんもおすすめのライブラリ。
比較表を見ても分かるように、オブジェクト指向スタイルで豊富な機能を扱える。便利そう。
pythonのoptparseモジュールにインスパイアされたものらしい。使い勝手も似ている。


一点気になったのは必須オプションが定義できないこと(やり方あるのかもしれないけどわからなかった)。
どうもオプションというものは付帯的なものであって、何もなくても動くというのがプログラムのあるべき姿みたいな思想らしい。
誤解の無いように書いておくと、オプションと引数は明確に区別されていて、オプションは'-t'とかで挙動を変えたりするやつで、引数は処理対象のファイルとか。なので、一切何も指定せずに'php somecommand.php'とかやっても動くべきと言ってるわけではなくて、'-f target_file'が必須なのは良くない設計だということを言っている。……んだと思う。

Zend_Console_Getopt

使ったことがないのでわからないが、マニュアルを読む限りでは、こちらもオブジェクト指向化して機能追加したもののようだ。
ただ、オプションの定義に':'や'='を使っていたりと、getoptらしいところは残っている。Console_GetoptとConsole_CommandLineの中間ぐらいのイメージだろうか。

使ってみる

実際にどんな風に使うのか順に書いていこうかと思ったけど、上の文章を書くだけで力尽きたので、有力候補のConsole_GetoptとConsole_CommandLineのソースだけ貼っておく。

Console_Getopt
<?php
require_once 'Console/Getopt.php';

$con = new Console_Getopt;

// PHPの環境に左右されずに$argvを取る
$args = $con->readPHPArgv();
if (PEAR::isError($args)) {
    die($args->getMessage() . PHP_EOL);
}

$APP_NAME    = 'AppName';
$APP_VERSION = 'v1.0.0';

// プログラム名を取得
$PROGRAM_NAME = basename(array_shift($args));

// オプションの定義
// NOTE: ':'および'='はパラメータ必須
//           -f foo
//           --file=foo
//       '::'および'=='はパラメータ省略可
//           -m
//           -m1
//           --mode
//           --mode=3
//           (-m 3)はなんかダメっぽい
//       何も無しはパラメータを取らない
//           -h
//           --help
$optdef = array(
// short  => long
    'h'   => 'help',
    'V'   => 'version',
    'v'   => 'verbose',
    'f:'  => 'file=',
    'm::' => 'mode==',
    't'   => null,
);

// パースする
// NOTE: 先頭のプログラム名がある場合はgetopt()、
//       ない場合はgetopt2()を使う
$optsargs = $con->getopt2($args,
            implode('', array_keys($optdef)),
            array_values($optdef));
if (PEAR::isError($optsargs)) {
    usage();
}

$opts = $optsargs[0];   // オプション
$args = $optsargs[1];   // オプション以外の引数

// アプリのデフォルト設定など
$conf = array(
    'mode'    => null,
    'test'    => false,
    'verbose' => false,
);

// オプションを解釈していく
foreach ($opts as $opt) {
    list($name, $value) = $opt;
    switch ($name) {
    case 'h':
    case '--help':
        usage();
        exit(0);

    case 'V':
    case '--version':
        echo "$APP_NAME $APP_VERSION" . PHP_EOL;
        exit(0);

    case 'v':
    case '--verbose':
        $conf['verbose'] = true;
        break;

    case 'f':
    case '--file':
        $conf['file'] = $value;
        break;

    case 'm':
    case '--mode':
        $conf['mode'] = isset($value) ? $value : 2;
        break;

    case 't':
        $conf['test'] = true;
        break;
    }
}
print_r($conf);
print_r($args);

function usage()
{
    global $APP_NAME, $APP_VERSION, $PROGRAM_NAME;

    print <<<__USAGE__
$APP_NAME $APP_VERSION

Usage:
  $PROGRAM_NAME [option] [args]

Options:
  -f path, --file=path          Specify target file path.
  -m[123], --mode=[123]         Set process mode. (default=2)
                                    1: low quality.
                                    2: normal quality.
                                    3: high quality.
  -v, --verbose                 Verbose mode.
  -h, --help                    Show this help and exit.
  -V, --version                 Show version and exit.

__USAGE__;
    exit(0);
}
?>

コメントつけてるせいもあるけど長い……。

Console_CommandLine
<?php
require_once 'Console/CommandLine.php';

$cmdline = new Console_CommandLine(array(
    'description' => 'A Console_CommandLine sample program.',
    'version'     => '1.0.0',
));
$cmdline->addOption('config', array(
    'short_name'  => '-c',
    'long_name'   => '--conf',
    'description' => 'Specify config FILE path.',
    'help_name'   => 'FILE',
    'action'      => 'StoreString',
));
$cmdline->addOption('mode', array(
    'short_name'  => '-m',
    'long_name'   => '--mode',
    'description' => 'Set process mode. (default=2)' . PHP_EOL .
                     '1: low quality' . PHP_EOL .
                     '2: normal quality' . PHP_EOL .
                     '3: high quality',
    'help_name'   => '[123]',
    'action'      => 'StoreInt',
    'default'     => 2,
));
$cmdline->addOption('verbose', array(
    'short_name'  => '-v',
    'long_name'   => '--verbose',
    'description' => 'Verbose mode.',
    'action'      => 'StoreTrue',
    'default'     => false,
));

$cmdline->addArgument('src', array(
    'description' => 'Source files.',
    'multiple'    => true,
));
$cmdline->addArgument('dest', array(
    'description' => 'Destination file.',
));

try {
    $result = $cmdline->parse();
    print_r($result->options);
    print_r($result->args);
} catch (Exception $e) {
    $cmdline->displayError($e->getMessage());
}
?>

だいぶすっきり。

*1:本当はPerlとかで作りたい