Nette Command-Line

PHP でコマンドラインのアプリケーションを作るための軽いライブラリです。スイッチ、オプション、位置の引数を読み解き、ANSI に対応した色付きの端末への出力を作るのを助けます。

インストール:

composer require nette/command-line

PHP のバージョン 8.2 が要り、PHP 8.5 まで対応しています。

コマンドラインの引数を読み解く

どの CLI のスクリプトも、--verbose-o output.txt、あるいはただのファイル名といった引数を扱う必要があります。Nette\CommandLine\Parserクラスは、いちばん手早く始める方法を差し出します。ヘルプの文を書けば、そこからオプションの定義を読み取ってくれるのです。

use Nette\CommandLine\Parser;

$parser = new Parser;
$parser->addFromHelp('
	-h, --help              Show this help
	-v, --verbose           Enable verbose mode
	-o, --output <file>     Output file
	-f, --format [type]     Output format (default: json)
	-I, --include <path>... Include paths
	--dry-run               Show what would be done
');

$args = $parser->parse();

これだけです。この読み取り器は、--verbose がスイッチで、--output が値を求め、--format は省略できる値を持ち、json が代わりの値であることを理解します。ヘルプの文は、実際のオプションの定義と食い違いません。

parse() メソッドは連想配列を返します。キーは、ダッシュも含めて定義したとおりのオプションの名前と一致します。

[
	'--help' => true,         // 使われなければ null
	'--verbose' => null,
	'--output' => 'file.txt', // 使われなければ null
	'--format' => 'json',     // (default: json) からの代わりの値
	'--include' => ['src', 'lib'],
	'--dry-run' => null,
]

既定では parse()$_SERVER['argv'] から読みます。独自の配列も渡せて、テストに便利です。

$args = $parser->parse(['--verbose', '-o', 'out.txt']);

ヘルプの文の書き方

読み取り器は、整えられたヘルプの文から次の決まりに沿ってオプションの定義を取り出します。

--verbose スイッチ(値なし)
-v, --verbose 短い別名の付いたスイッチ
--output <file> 値が必須のオプション
--format [type] 値を省略できるオプション
(default: json) 代わりの値を決めます
<path>... 繰り返せるオプション

それぞれの行がひとつのオプションを定めます。オプションの名前は、説明と少なくとも空白 2 つで分けなければなりません。

追加の設定

ヘルプの文では表せない設定もあります。第 2 パラメータに、オプションの名前をキーにした配列を渡します。

$parser->addFromHelp('
	-c, --config <file>   Configuration file
	-I, --include <path>  Include path
	-n, --count <num>     Number of iterations
', [
	'--config' => [
		Parser::RealPath => true,
	],
	'--include' => [
		Parser::Repeatable => true,
	],
	'--count' => [
		Parser::Normalizer => fn($v) => (int) $v,
	],
]);

使えるキーです。

Parser::Repeatable 複数の値を配列に集めます
Parser::RealPath ファイルが存在するかを確かめ、絶対パスに解決します
Parser::Normalizer 変換の関数 fn($value) => ...
Parser::Default 代わりの値(ヘルプの文の (default: x) と同じ)
Parser::Enum 許される値の配列

fluent な API

オプションの定義をもっと思いどおりにしたいなら、addSwitch()addOption()addArgument() のメソッドによる fluent な API を使います。このやり方なら、正規化器、enum、そしてそれぞれのパラメータの細かな制御も含め、すべての機能を使えます。

use Nette\CommandLine\Parser;

$parser = new Parser;
$parser
	->addSwitch('--verbose', '-v')
	->addOption('--output', '-o')
	->addArgument('file');

$args = $parser->parse();

addFromHelp() と同じく、テストのために parse() へ独自の配列を渡せます。

$args = $parser->parse(['--verbose', '-o', 'out.txt', 'input.txt']);

スイッチ、オプション、引数

コマンドラインの入力には 3 つの種類があります。

スイッチ--verbose-v のような値のない旗です。あれば true、なければ null と読み解かれます。

$parser->addSwitch('--verbose', '-v');
// --verbose  → true
// -v         → true
// (使われない) → null

オプション--output file.txt のように値を受け取ります。値は空白でも = でも区切れます。

$parser->addOption('--output', '-o');
// --output file.txt    → 'file.txt'
// --output=file.txt    → 'file.txt'
// -o file.txt          → 'file.txt'
// --output             → 例外を投げます(値が必須)
// (使われない)           → null

オプションそのものはいつも省略できて、使わなければ null が返ることに注意してください。とはいえ使うなら、既定では値が必須です。値なしのオプションを許すには optionalValue: true にします(その場合 true と読み解かれます)。

$parser->addOption('--format', '-f', optionalValue: true);
// --format json        → 'json'
// --format             → true
// (使われない)           → null

同じオプションが repeatable: true なしで何度も使われた場合、最後の値が勝ちます。

$parser->addOption('--output', '-o');
// -o first.txt -o second.txt  → 'second.txt'

引数 はダッシュのない位置の値です。既定では必須です。省略できるようにするには optional: true にします。

$parser->addArgument('input');
// script.php file.txt  → 'file.txt'
// (使われない)           → 例外を投げます

$parser->addArgument('output', optional: true);
// (使われない)           → null

$parser->addArgument('output', optional: true, fallback: 'out.txt');
// (使われない)           → 'out.txt'

省略できるオプションや引数が渡されなかったときに使う値は fallback で決めます。optionalValue: true のオプションでは、値なしでそのオプションを使うとやはり true と読み解かれ、fallback はそのオプションがまったくないときにだけ使われることに注意してください。

$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json');
// --format xml  → 'xml'
// --format      → true(値なしでオプションを使った場合)
// (使われない)    → 'json'(fallback)

引数はコマンドラインのどこにでも書けます。オプションのうしろでなくてもかまいません。

// 次はどれも同じです:
// script.php --verbose input.txt
// script.php input.txt --verbose

enum で値を制限する

受け入れる値を決まった一そろいに限れます。

$parser->addOption('--format', '-f', enum: ['json', 'xml', 'csv']);
// --format yaml  → "Value of option --format must be json, or xml, or csv." を投げます

繰り返せるオプション

repeatable: true にすると、複数の値を配列に集めます。

$parser->addOption('--include', '-I', repeatable: true);
// -I src -I lib  → ['src', 'lib']
// (使われない)     → []

$parser->addArgument('files', optional: true, repeatable: true);
// a.txt b.txt    → ['a.txt', 'b.txt']

値を変換する

読み解いた値を変えるには normalizer を使います。

$parser->addOption('--count', normalizer: fn($v) => (int) $v);
// --count 42  → 42(整数)

ファイルのパスの検証には、組み込みの normalizeRealPath を使います。

$parser->addOption('--config', normalizer: Parser::normalizeRealPath(...));
// --config app.ini     → '/full/path/to/app.ini'
// --config missing.ini → "File path 'missing.ini' not found." を投げます

両方のやり方を混ぜる

一部のオプションにだけ正規化器が必要なら、addFromHelp() と fluent なメソッドを組み合わせられます。

$parser
	->addFromHelp('
		-v, --verbose  Enable verbose mode
		-q, --quiet    Suppress output
	')
	->addOption('--config', '-c', normalizer: Parser::normalizeRealPath(...))
	->addArgument('input');

エラーの処理

読み取り器は、正しくない入力に対して \Exception を投げます。

use Nette\CommandLine\Parser;

$parser = new Parser;
$parser
	->addOption('--output', '-o')
	->addArgument('file');

try {
	$args = $parser->parse();
} catch (\Exception $e) {
	fwrite(STDERR, "Error: {$e->getMessage()}\n");
	exit(1);
}

よくあるエラーのメッセージです。

Option --output requires argument. 必須の値なしでオプションが使われました
Unknown option --foo. 知らないオプション
Missing required argument <file>. 必須の引数が渡されていません
Unexpected parameter foo. 余分な位置の引数
Value of option --format must be json, or xml. 値が enum にありません

コマンドラインの引数がまったく渡されなかったか(つまり利用者が script.php のうしろに何も付けずに走らせたか)は isEmpty() で調べられます。

if ($parser->isEmpty()) {
	$parser->help();
	exit;
}

--help と –version を扱う

スクリプトに必須の引数があると、script.php --help を走らせても必須の引数がないのでふつうは失敗します。まず情報のオプションを調べるには parseOnly() を使います。

$parser = new Parser;
$parser
	->addSwitch('--help', '-h')
	->addSwitch('--version', '-V')
	->addArgument('input');  // 必須

// まず情報のオプションを調べます(検証も例外もありません)
$info = $parser->parseOnly(['--help', '--version']);

if ($info['--help']) {
	$parser->help();
	exit;
}

if ($info['--version']) {
	echo "1.0.0\n";
	exit;
}

// そのあと検証付きの完全な読み取りを行います
$args = $parser->parse();

parseOnly() メソッドは次のように働きます。

  • 指定されたオプションだけを読み解き、ほかはすべて無視します
  • 別名を認めます(-h--help
  • 例外を決して投げません
  • 使われなかったオプションには null を返します

色付きの出力

Nette\CommandLine\Consoleクラスは、出力が端末で目立つように、文を ANSI の色のコードで包みます。

use Nette\CommandLine\Console;

$console = new Console;
echo $console->color('red', 'Error!') . "\n";
echo $console->color('white/blue', 'White text on blue background') . "\n";

色は '前景''前景/背景' の形で渡します。使える色は blackgraysilverwhitenavybluegreenlimetealaquamaroonredpurplefuchsiaoliveyellow です。

色は、出力がそれに対応しているときにだけ自動的に有効になります。色が切られているとき color() メソッドはただの文字列を返すので、いつでも安全に呼べます。振る舞いは手で決められます。

$console->useColors(false); // 色を切ります
$console->useColors(true);  // 色を強制します

端末を見分ける

端末でだけ意味のある機能を使うかどうかを決めるのに、2 つの静的メソッドが役立ちます。detectColors() は、NO_COLORの環境変数が設定されているときや、出力が CLI の端末でないときに false を返します。FORCE_COLOR の変数は端末の判別を上書きします。

if (Console::detectColors()) {
	// 端末は ANSI の色に対応しています
}

detectTerminal() は、出力が対話的な端末(TTY)かどうかを教えてくれます。進み具合の表示、行を書き直す出力、対話的な問いかけのように、端末でだけ意味のある機能を自動的に切るのに役立ちます。

if (Console::detectTerminal()) {
	// 出力はファイルやパイプではなく、対話的な端末へ向かっています
}

完全な例

ParserConsole を組み合わせた、実際に使えるファイルの変換のスクリプトです。

#!/usr/bin/env php
<?php
use Nette\CommandLine\Parser;

require __DIR__ . '/vendor/autoload.php';

$parser = new Parser;
$parser
	->addFromHelp('
		-h, --help           Show this help
		-v, --verbose        Show detailed output
		-n, --dry-run        Show what would be done
		-f, --format [type]  Output format (default: json)
		-o, --output <file>  Output file
	', [
		'--format' => [
			Parser::Enum => ['json', 'xml', 'csv'],
		],
	])
	->addArgument('input', normalizer: Parser::normalizeRealPath(...));

// 検証の前に --help を扱います(「引数がない」のエラーを避けます)
if ($parser->isEmpty() || $parser->parseOnly(['--help'])['--help']) {
	echo "Usage: convert [options] <input>\n\n";
	$parser->help();
	exit;
}

try {
	$args = $parser->parse();
} catch (\Exception $e) {
	fwrite(STDERR, "Error: {$e->getMessage()}\n");
	exit(1);
}

if ($args['--verbose']) {
	echo "Converting {$args['input']} to {$args['--format']}...\n";
}

if ($args['--dry-run']) {
	echo "Dry run: no changes made.\n";
	exit;
}

// ... ここに変換の論理 ...

echo "Done!\n";

このスクリプトは次のようなコマンドを受け取ります。

  • convert input.txt – 既定の設定で変換します
  • convert -v --format xml input.txt – 詳しい出力、XML の形式
  • convert -o result.txt input.txt – 出力のファイルを指定します
  • convert --help – ヘルプを表示します(入力のファイルがなくても働きます)
バージョン: 1.x