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>... Повторяемый параметр

Каждая строка определяет один параметр. Имена параметров должны отделяться от описаний хотя бы двумя пробелами.

Дополнительная настройка

Некоторые настройки в тексте справки не выразить. Передайте вторым параметром массив с ключами по именам параметров:

$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 Массив допустимых значений

Текучий API

Когда вам нужно больше контроля над определениями параметров, используйте текучий API с методами addSwitch(), addOption() и addArgument(). Такой подход даёт доступ ко всем возможностям, включая нормализаторы, перечисления и точное управление каждым параметром:

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']);

Переключатели, параметры и аргументы

Есть три вида ввода в командной строке:

Переключатели – флаги без значений вроде --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

Ограничение значений перечислением

Ограничьте принимаемые значения определённым набором:

$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() можно сочетать с текучими методами, когда нормализаторы нужны лишь для некоторых параметров:

$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. Значение не входит в перечисление

Используйте isEmpty(), чтобы проверить, не были ли аргументы командной строки вообще не заданы (то есть пользователь запустил просто script.php и ничего после него):

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";

Цвет задаётся как 'передний план' или 'передний план/фон'. Доступные цвета: black, gray, silver, white, navy, blue, green, lime, teal, aqua, maroon, red, purple, fuchsia, olive и yellow.

Цвета включаются автоматически, только если вывод их поддерживает. При отключённых цветах метод color() возвращает обычную строку, так что вызывать его всегда безопасно. Поведение можно задать и вручную:

$console->useColors(false); // отключить цвета
$console->useColors(true);  // принудительно включить цвета

Определение терминала

Два статических метода помогают решить, использовать ли возможности, доступные только в терминале. detectColors() возвращает false, когда задана переменная окружения NO_COLOR либо когда вывод не является CLI-терминалом; переменная FORCE_COLOR перебивает проверку терминала:

if (Console::detectColors()) {
	// терминал поддерживает цвета ANSI
}

detectTerminal() говорит, является ли вывод интерактивным терминалом (TTY). Это удобно для автоматического отключения возможностей, которые имеют смысл только в терминале: индикаторов выполнения, вывода с перезаписью строки или интерактивных вопросов:

if (Console::detectTerminal()) {
	// вывод идёт в интерактивный терминал, а не в файл или канал
}

Полный пример

Вот настоящий скрипт-конвертер файлов, сочетающий Parser и Console:

#!/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 до проверок (избегаем ошибки "missing argument")
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