Nette Command-Line

Una libreria leggera per costruire applicazioni da riga di comando in PHP. Analizza switch, opzioni e argomenti posizionali e vi aiuta a produrre output colorato nel terminale con il supporto ANSI.

Installazione:

composer require nette/command-line

Richiede PHP versione 8.2 e supporta PHP fino alla 8.5.

Analisi degli argomenti da riga di comando

Ogni script CLI deve gestire argomenti come --verbose, -o output.txt oppure semplici nomi di file. La classe Nette\CommandLine\Parser offre il modo più rapido di cominciare: basta scrivere il vostro testo di aiuto e lasciare che il parser ne ricavi le definizioni delle opzioni:

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();

Ecco fatto. Il parser capisce che --verbose è uno switch, che --output richiede un valore e che --format ha un valore facoltativo con json come ripiego. Il vostro testo di aiuto resta allineato alle definizioni reali delle opzioni.

Il metodo parse() restituisce un array associativo. Le chiavi corrispondono esattamente ai nomi delle opzioni come sono definiti, trattini compresi:

[
	'--help' => true,         // oppure null se non è stata usata
	'--verbose' => null,
	'--output' => 'file.txt', // oppure null se non è stata usata
	'--format' => 'json',     // ripiego da (default: json)
	'--include' => ['src', 'lib'],
	'--dry-run' => null,
]

Per impostazione predefinita parse() legge da $_SERVER['argv']. Potete passare un array vostro, il che torna comodo per i test:

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

Sintassi del testo di aiuto

Il parser ricava le definizioni delle opzioni dal testo di aiuto formattato secondo queste regole:

--verbose Switch (senza valore)
-v, --verbose Switch con alias breve
--output <file> Opzione con valore obbligatorio
--format [type] Opzione con valore facoltativo
(default: json) Imposta il valore di ripiego
<path>... Opzione ripetibile

Ogni riga definisce un'opzione. I nomi delle opzioni devono essere separati dalle loro descrizioni da almeno due spazi.

Configurazione aggiuntiva

Alcune impostazioni non si possono esprimere nel testo di aiuto. Passate come secondo parametro un array con chiavi corrispondenti ai nomi delle opzioni:

$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,
	],
]);

Chiavi disponibili:

Parser::Repeatable Raccoglie più valori in un array
Parser::RealPath Verifica che il file esista e lo risolve in un percorso assoluto
Parser::Normalizer Funzione di trasformazione fn($value) => ...
Parser::Default Valore di ripiego (uguale a (default: x) nel testo di aiuto)
Parser::Enum Array dei valori consentiti

API fluent

Quando vi serve più controllo sulle definizioni delle opzioni, usate l'API fluent con i metodi addSwitch(), addOption() e addArgument(). Questo approccio vi dà accesso a tutte le funzionalità, compresi normalizzatori, enum e controllo preciso su ogni parametro:

use Nette\CommandLine\Parser;

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

$args = $parser->parse();

Come con addFromHelp(), potete passare a parse() un array vostro per i test:

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

Switch, opzioni e argomenti

Ci sono tre tipi di input da riga di comando:

Gli switch sono flag senza valore, come --verbose oppure -v. Vengono analizzati come true quando sono presenti, come null quando mancano:

$parser->addSwitch('--verbose', '-v');
// --verbose  → true
// -v         → true
// (non usato) → null

Le opzioni accettano valori, come --output file.txt. Il valore si può separare con uno spazio oppure con =:

$parser->addOption('--output', '-o');
// --output file.txt    → 'file.txt'
// --output=file.txt    → 'file.txt'
// -o file.txt          → 'file.txt'
// --output             → lancia un'eccezione (valore obbligatorio)
// (non usata)          → null

Notate che l'opzione in sé è sempre facoltativa: non usarla restituisce null. Quando però viene usata, il valore è obbligatorio per impostazione predefinita. Impostate optionalValue: true per permettere l'opzione senza valore (viene allora analizzata come true):

$parser->addOption('--format', '-f', optionalValue: true);
// --format json        → 'json'
// --format             → true
// (non usata)          → null

Quando la stessa opzione viene usata più volte senza repeatable: true, vince l'ultimo valore:

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

Gli argomenti sono valori posizionali senza trattini. Per impostazione predefinita sono obbligatori. Impostate optional: true per renderli facoltativi:

$parser->addArgument('input');
// script.php file.txt  → 'file.txt'
// (non usato)          → lancia un'eccezione

$parser->addArgument('output', optional: true);
// (non usato)          → null

$parser->addArgument('output', optional: true, fallback: 'out.txt');
// (non usato)          → 'out.txt'

Usate fallback per indicare il valore usato quando un'opzione o un argomento facoltativo non viene fornito. Per le opzioni con optionalValue: true tenete presente che usare l'opzione senza valore viene comunque analizzato come true, mentre il ripiego si usa solo quando l'opzione non è presente affatto:

$parser->addOption('--format', '-f', optionalValue: true, fallback: 'json');
// --format xml  → 'xml'
// --format      → true (opzione usata senza valore)
// (non usata)   → 'json' (ripiego)

Gli argomenti possono comparire in qualsiasi punto della riga di comando, non devono per forza venire dopo le opzioni:

// tutte queste forme sono equivalenti:
// script.php --verbose input.txt
// script.php input.txt --verbose

Limitare i valori con enum

Limitate i valori accettati a un insieme determinato:

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

Opzioni ripetibili

Impostate repeatable: true per raccogliere più valori in un array:

$parser->addOption('--include', '-I', repeatable: true);
// -I src -I lib  → ['src', 'lib']
// (non usata)    → []

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

Trasformare i valori

Usate un normalizer per trasformare il valore analizzato:

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

Per validare i percorsi dei file usate il normalizeRealPath integrato:

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

Combinare i due approcci

Potete combinare addFromHelp() con i metodi fluent quando vi servono i normalizzatori solo per alcune opzioni:

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

Gestione degli errori

Il parser lancia \Exception per gli input non validi:

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);
}

Messaggi di errore più frequenti:

Option --output requires argument. Opzione usata senza il valore obbligatorio
Unknown option --foo. Opzione non riconosciuta
Missing required argument <file>. Argomento obbligatorio non fornito
Unexpected parameter foo. Argomento posizionale in più
Value of option --format must be json, or xml. Valore non presente nell'enum

Usate isEmpty() per verificare se non è stato fornito alcun argomento da riga di comando (cioè se l'utente ha lanciato solo script.php senza nulla dopo):

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

Gestire –help e –version

Quando il vostro script ha argomenti obbligatori, lanciare script.php --help fallirebbe normalmente perché manca l'argomento obbligatorio. Usate parseOnly() per controllare prima le opzioni informative:

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

// prima controlliamo le opzioni informative (nessuna validazione, nessuna eccezione)
$info = $parser->parseOnly(['--help', '--version']);

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

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

// ora facciamo l'analisi completa con la validazione
$args = $parser->parse();

Il metodo parseOnly():

  • analizza solo le opzioni indicate, ignorando tutto il resto,
  • rispetta gli alias (-h--help),
  • non lancia mai eccezioni,
  • restituisce null per le opzioni che non sono state usate.

Output colorato

La classe Nette\CommandLine\Console racchiude il testo nei codici colore ANSI, così il vostro output spicca nel terminale:

use Nette\CommandLine\Console;

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

Il colore si indica come 'primo piano' oppure 'primo piano/sfondo'. I colori disponibili sono: black, gray, silver, white, navy, blue, green, lime, teal, aqua, maroon, red, purple, fuchsia, olive e yellow.

I colori si attivano automaticamente solo quando l'output li supporta. Il metodo color() restituisce una stringa semplice quando i colori sono disattivati, quindi si può sempre chiamare senza rischi. Il comportamento lo potete forzare a mano:

$console->useColors(false); // disattiva i colori
$console->useColors(true);  // forza i colori

Rilevare il terminale

Due metodi statici vi aiutano a decidere se usare funzionalità legate al terminale. detectColors() restituisce false quando è impostata la variabile d'ambiente NO_COLOR, oppure quando l'output non è un terminale CLI; la variabile FORCE_COLOR ha la precedenza sul controllo del terminale:

if (Console::detectColors()) {
	// il terminale supporta i colori ANSI
}

detectTerminal() vi dice se l'output è un terminale interattivo (un TTY). Torna utile per disattivare automaticamente le funzionalità che hanno senso solo in un terminale, come gli indicatori di avanzamento, l'output che riscrive la riga o le richieste interattive:

if (Console::detectTerminal()) {
	// l'output va a un terminale interattivo, non a un file o a una pipe
}

Esempio completo

Ecco uno script reale di conversione file che combina Parser e 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(...));

// gestiamo --help prima della validazione (evita l'errore "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;
}

// ... qui la logica di conversione ...

echo "Done!\n";

Lo script accetta comandi come:

  • convert input.txt – conversione con i valori predefiniti
  • convert -v --format xml input.txt – modalità verbose, formato XML
  • convert -o result.txt input.txt – indica il file di output
  • convert --help – mostra l'aiuto (funziona anche senza il file di input)
versione: 1.x