Nette Documentation Preview

syntax
Výstup do terminálu
*******************

.[perex]
Barevný text, klikací odkazy, řádek s průběhem a dotazy na uživatele. Třída `Console` sama pozná, kam výstup míří, a zachová se podle toho: v terminálu je výstup barevný a živý, v souboru, v rouře nebo v CI zůstane čistý text. Nemusíte kvůli tomu psát jedinou podmínku.


Barevný výstup
==============

Objekt [api:Nette\CommandLine\Console] píše do jednoho výstupu, standardně na obrazovku, a umí text obarvit:

```php
use Nette\CommandLine\Console;

$console = new Console;
$console->writeLine('3 images converted', 'green');
$console->writeLine('logo.png skipped, it is already WebP', 'yellow');
$console->writeLine($console->color('gray', 'Results in ') . $outputDir);  // řádek z více barev
```

Konzoli obvykle vytvoříte jednou na začátku skriptu a předáte ji třídám, které vypisují, třeba v konstruktoru, jak ukazuje [úvodní stránka |@home#Výstup do terminálu].

Barvy přidávají jen metody, nikdy značky v textu. Některé knihovny v textu hledají značky jako `<info>`, takže XML, HTML nebo vstup od uživatele musíte nejdřív escapovat, jinak se rozbije. Tady se všechno vypíše přesně tak, jak to je.

Jestli bude text barevný, nezáleží na vašem kódu, ale na tom, kam výstup míří. V terminálu barevný je. Když výstup přesměrujete do souboru, pošlete rourou jinému programu nebo skript běží v CI, vypíše stejný kód prostý text, protože `color()` ani `link()` pak nic nepřidají. Pozor jen na text odjinud, který si barvy nese s sebou, třeba výstup jiného programu. Ty z něj případně odstraňte pomocí `Ansi::strip()`.


Dva výstupy
-----------

Každý program má dva výstupy: standardní (`STDOUT`) pro výsledky a chybový (`STDERR`) pro hlášky a chyby. Uživatel je může přesměrovat každý jinam, třeba výsledky do souboru a chyby nechat na obrazovce. Proto má každý výstup vlastní konzoli, která sama pozná, jestli její výstup vede do terminálu:

```php
$out = new Console(STDOUT);
$err = new Console(STDERR);
```

Bez parametru píše konzole do `STDOUT`. Když program, který čte váš výstup, skončí dřív, jako `head` v `php images.php list | head -1`, zbytek výstupu se tiše zahodí, místo aby každý zápis vyvolal chybu.


Barvy
-----

Barvu zadáte jako `'popředí'` nebo `'popředí/pozadí'`, třeba `'white/blue'`. Základní jména `black`, `gray`, `silver`, `white`, `navy`, `blue`, `green`, `lime`, `teal`, `aqua`, `maroon`, `red`, `purple`, `fuchsia`, `olive` a `yellow` vykreslí terminál podle svého barevného motivu. Barvu, která má vypadat všude stejně, zapíšete hexadecimálně, `'#87D7FF'` nebo zkráceně `'#8DF'`, případně jako číslo 0-255 rozšířené palety, třeba `'117'`.

Neznámá barva vyhodí výjimku, i když jsou barvy vypnuté, takže překlep odhalíte hned. Barva `null` znamená žádnou barvu, což se hodí pro podmínky: `$console->color($ok ? 'green' : null, $text)`.

Ne každý terminál umí všechny barvy. Konzole to zjistí sama a barvu vykreslí tak věrně, jak to jde: v moderním terminálu přesně, ve starším jako nejbližší barvu, kterou umí. Kolik barev terminál zvládne, vyjadřuje [api:Nette\CommandLine\ColorDepth]: `TrueColor` (jakákoli barva), `Ansi256` (paleta 256 barev), `Ansi16` (16 základních barev) a `None` (bez barev).

Chování můžete ovlivnit proměnnými prostředí, které znají i jiné nástroje. [NO_COLOR |https://no-color.org] barvy vypne. [FORCE_COLOR |https://force-color.org] je zapne i při výstupu do souboru nebo roury, hodnota `2` nebo `3` přitom vyžádá aspoň `Ansi256`, případně `TrueColor`. `COLORTERM` s hodnotou `truecolor` nebo `24bit` řekne, že terminál umí jakoukoli barvu.


Volba `--no-color`
------------------

Uživatelé často chtějí barvy vypnout přímo na příkazové řádce. Přidejte programu příznak `--no-color` a po zpracování řádky vypněte barvy svým konzolím metodou `setColorDepth()`:

```php
use Nette\CommandLine\ColorDepth;

$out = new Console(STDOUT);
$err = new Console(STDERR);
$tools = new ImageTools($out);

$program = new Command('images');
$program->addFlag('--no-color', 'Print plain output without colors');
$program->addCommandsFrom($tools);

$parser = new Parser;
$args = $parser->parse($program);
if ($args->get('--no-color')) {
	$out->setColorDepth(ColorDepth::None);
	$err->setColorDepth(ColorDepth::None);
}
```

Aby se volba týkala i nápovědy a chybových hlášek, vypisujte je přes renderery s těmito konzolemi: `new HelpRenderer($out)` pro nápovědu a `new HelpRenderer($err)` pro chyby. Jen chyba v samotné příkazové řádce se nahlásí dřív, než parser příznak přečte, a zůstane proto barevná. Úplně všude barvy vypne proměnná prostředí `NO_COLOR`.

Konzoli bez barev můžete vytvořit i rovnou, `new Console(STDOUT, colorDepth: ColorDepth::None)`.

Konzoli se můžete i zeptat, jak na tom je:

```php
$console->hasColors();      // jestli vypisuje barevně
$console->getColorDepth();  // kolik barev používá, třeba ColorDepth::Ansi256
$console->isTerminal();     // jestli výstup vede do terminálu, kde se někdo dívá
$console->getWidth();       // šířka terminálu ve sloupcích, mimo terminál 80
```

`hasColors()` a `isTerminal()` odpovídají na dvě různé otázky. Podle první se rozhodujte, jestli barvit, podle druhé, jestli ukazovat věci, které mají smysl jen pro člověka u obrazovky, třeba řádek s průběhem nebo dotaz. Uživatel totiž může barvy vypnout a přitom u terminálu sedět.

`getWidth()` se terminálu zeptá jen jednou a výsledek si pamatuje, takže změnu velikosti okna pozná až nová konzole. Proměnná prostředí `COLUMNS` má přednost, což se hodí, když chcete vyzkoušet, jak výstup vypadá v úzkém terminálu.


Klikací odkazy
==============

Odkaz se skládá stejně jako barva. V terminálu z textu udělá `link()` odkaz, na který jde kliknout. V souboru, v rouře nebo bez barev vypíše text a za ním adresu v závorce, takže v logu adresa nezmizí:

```php
$console->writeLine('See ' . $console->link('https://nette.org'));
$console->writeLine($console->link('https://nette.org', 'the manual'));  // mimo terminál 'the manual (https://nette.org)'
```

`link()` odmítne adresu s řídicími znaky, protože by mohly odkaz předčasně ukončit a poslat terminálu vlastní příkazy. Znaky s diakritikou a jiné znaky mimo ASCII nevadí.


Průběh a stavové řádky
======================

Při delší práci se hodí ukazovat, co skript právě dělá a kolik má hotovo. Na to je `setStatus()`. Vykreslí řádek na konec výstupu a další výpis ho smaže, vypíše se nad ním a další `setStatus()` řádek nakreslí znovu. Uživatel tak vidí přibývající výsledky a pod nimi jeden řádek, který se průběžně mění:

```
photo.jpg converted
logo.png converted
3/10  holiday.jpg
```

V kódu to vypadá takhle:

```php
foreach ($files as $i => $file) {
	$console->setStatus(sprintf('%d/%d  %s', $i + 1, count($files), $file));
	// ... převod souboru
	$console->writeLine("$file converted");  // smaže stavový řádek a vypíše se nad něj
}

$console->clearStatus();
```

Stavový řádek se v terminálu jen kreslí, do výstupu se nezapisuje. Když výstup do terminálu nevede, `setStatus()` neudělá vůbec nic, takže v souboru s logem jsou jen skutečné řádky. Stavových řádků může být i víc najednou, každý znak nového řádku začne další. Příliš dlouhý řádek se zkrátí na šířku terminálu, protože zalomený by se nedal překreslit.

Dokud je stavový řádek vidět, kurzor je schovaný. Objeví se znovu, když stav smažete, když konzoli uvolníte a také na konci skriptu, i po `exit()`, nezachycené výjimce nebo fatální chybě. Jen Ctrl+C ukončí PHP okamžitě, takže ho musí ošetřit vaše aplikace sama.

Stavový řádek smaže jen konzole, která ho nakreslila. Máte-li zvlášť konzoli pro výsledky a zvlášť pro chyby, kreslete stav na tu, kam jdou chyby. Jinak by se varování vypsané během práce otisklo přes stavový řádek.


Měření textu
============

Když chcete vypisovat zarovnané sloupce, `strlen()` vám nepomůže. Barvy jsou v textu uložené jako neviditelné escape sekvence, které nezabírají žádné místo, a čínský znak nebo emoji zabere na obrazovce dva sloupce. Třída [api:Nette\CommandLine\Ansi] měří a zkracuje text tak, jak ho ukáže terminál:

```php
use Nette\CommandLine\Ansi;

Ansi::measure($console->color('red', 'error'));  // 5, escape sekvence se nepočítají
Ansi::pad($console->color('red', 'error'), 10);  // doplní na 10 sloupců, ne na 10 bajtů
Ansi::truncate($path, 30, keepEnd: true);        // zkrátí začátek, aby zůstalo jméno souboru
Ansi::strip($text);                              // text bez escape sekvencí
```

Šířky se řídí tabulkami Unicode, takže správně změří i emoji a vlajky. Řídicí znaky, třeba tabulátor, nezabírají žádný sloupec. `strip()` odstraní jen escape sekvence, ostatní řídicí znaky v textu nechá, takže z nedůvěryhodného textu bezpečný pro výpis neudělá.


Dotaz na uživatele
==================

Někdy skript potřebuje rozhodnutí, které může udělat jen člověk. Přepsat existující soubor? Přeskočit obrázek, který nejde přečíst? [api:Nette\CommandLine\Prompt] položí otázku a počká, až uživatel odpoví jednou z možností, které povolíte:

```php
use Nette\CommandLine\Prompt;

$prompt = new Prompt($console);
if ($prompt->isInteractive()) {
	$answer = $prompt->choose('Overwrite photo.webp?', ['y', 'n', 'a', 'q'], default: 'n');
}
```

Uživatel uvidí `Overwrite photo.webp? [y/N/a/q]` a výchozí možnost poznáte podle velkého písmena. Které možnosti nabídnete, je na vás, tady by to mohlo být ano, ne, ano pro všechny a konec. Dokud uživatel neodpoví jednou z nich, prompt se ptá znovu. Na velikosti písmen nezáleží a `choose()` vrátí možnost přesně tak, jak jste ji zadali. Enter na prázdném řádku vybere výchozí možnost, a pokud žádná není, prompt se zeptá znovu.

Když si každá možnost zaslouží pár slov, předejte je jako pole s popisy a prompt nad otázkou vypíše nabídku:

```php
$answer = $prompt->choose('What now?', [
	's' => 'Skip this image',
	'r' => 'Retry',
	'c' => 'Cancel the conversion',
], default: 'r');
```

```
  [s] Skip this image
  [r] Retry
  [c] Cancel the conversion
What now? [s/R/c]
```

Když vstup skončí, třeba protože uživatel stiskl Ctrl+D, vrátí `choose()` hodnotu `null`, nikdy výchozí možnost. Zavřený vstup tak nemůže potichu odpovědět ano. Co to pro skript znamená, rozhodnete sami, třeba `?? 'q'`.

Prompt se ptá jen tehdy, když vstup i konzole vedou do terminálu, takže nikdy nevezme odpověď z roury, u které nikdo nesedí. Jinak `choose()` vyhodí `LogicException` dřív, než cokoli vypíše nebo přečte. Proto se nejdřív zeptejte `isInteractive()` a rozhodněte, co skript udělá bez člověka, třeba skončí s radou použít volbu příkazové řádky, jako `--overwrite`. Předejte promptu konzoli, která kreslí stavový řádek: otázka ho pak smaže a další stav se po odpovědi nakreslí na čistý řádek.


{{description: Barevný výstup do terminálu v PHP, který se v rouře a v CI sám vypne, klikací odkazy, řádek s průběhem a dotazy na uživatele.}}
{{sitename: Nette Dokumentace}}

Výstup do terminálu

Barevný text, klikací odkazy, řádek s průběhem a dotazy na uživatele. Třída Console sama pozná, kam výstup míří, a zachová se podle toho: v terminálu je výstup barevný a živý, v souboru, v rouře nebo v CI zůstane čistý text. Nemusíte kvůli tomu psát jedinou podmínku.

Barevný výstup

Objekt Nette\CommandLine\Console píše do jednoho výstupu, standardně na obrazovku, a umí text obarvit:

use Nette\CommandLine\Console;

$console = new Console;
$console->writeLine('3 images converted', 'green');
$console->writeLine('logo.png skipped, it is already WebP', 'yellow');
$console->writeLine($console->color('gray', 'Results in ') . $outputDir);  // řádek z více barev

Konzoli obvykle vytvoříte jednou na začátku skriptu a předáte ji třídám, které vypisují, třeba v konstruktoru, jak ukazuje úvodní stránka.

Barvy přidávají jen metody, nikdy značky v textu. Některé knihovny v textu hledají značky jako <info>, takže XML, HTML nebo vstup od uživatele musíte nejdřív escapovat, jinak se rozbije. Tady se všechno vypíše přesně tak, jak to je.

Jestli bude text barevný, nezáleží na vašem kódu, ale na tom, kam výstup míří. V terminálu barevný je. Když výstup přesměrujete do souboru, pošlete rourou jinému programu nebo skript běží v CI, vypíše stejný kód prostý text, protože color() ani link() pak nic nepřidají. Pozor jen na text odjinud, který si barvy nese s sebou, třeba výstup jiného programu. Ty z něj případně odstraňte pomocí Ansi::strip().

Dva výstupy

Každý program má dva výstupy: standardní (STDOUT) pro výsledky a chybový (STDERR) pro hlášky a chyby. Uživatel je může přesměrovat každý jinam, třeba výsledky do souboru a chyby nechat na obrazovce. Proto má každý výstup vlastní konzoli, která sama pozná, jestli její výstup vede do terminálu:

$out = new Console(STDOUT);
$err = new Console(STDERR);

Bez parametru píše konzole do STDOUT. Když program, který čte váš výstup, skončí dřív, jako head v php images.php list | head -1, zbytek výstupu se tiše zahodí, místo aby každý zápis vyvolal chybu.

Barvy

Barvu zadáte jako 'popředí' nebo 'popředí/pozadí', třeba 'white/blue'. Základní jména black, gray, silver, white, navy, blue, green, lime, teal, aqua, maroon, red, purple, fuchsia, olive a yellow vykreslí terminál podle svého barevného motivu. Barvu, která má vypadat všude stejně, zapíšete hexadecimálně, '#87D7FF' nebo zkráceně '#8DF', případně jako číslo 0–255 rozšířené palety, třeba '117'.

Neznámá barva vyhodí výjimku, i když jsou barvy vypnuté, takže překlep odhalíte hned. Barva null znamená žádnou barvu, což se hodí pro podmínky: $console->color($ok ? 'green' : null, $text).

Ne každý terminál umí všechny barvy. Konzole to zjistí sama a barvu vykreslí tak věrně, jak to jde: v moderním terminálu přesně, ve starším jako nejbližší barvu, kterou umí. Kolik barev terminál zvládne, vyjadřuje Nette\CommandLine\ColorDepth: TrueColor (jakákoli barva), Ansi256 (paleta 256 barev), Ansi16 (16 základních barev) a None (bez barev).

Chování můžete ovlivnit proměnnými prostředí, které znají i jiné nástroje. NO_COLOR barvy vypne. FORCE_COLOR je zapne i při výstupu do souboru nebo roury, hodnota 2 nebo 3 přitom vyžádá aspoň Ansi256, případně TrueColor. COLORTERM s hodnotou truecolor nebo 24bit řekne, že terminál umí jakoukoli barvu.

Volba --no-color

Uživatelé často chtějí barvy vypnout přímo na příkazové řádce. Přidejte programu příznak --no-color a po zpracování řádky vypněte barvy svým konzolím metodou setColorDepth():

use Nette\CommandLine\ColorDepth;

$out = new Console(STDOUT);
$err = new Console(STDERR);
$tools = new ImageTools($out);

$program = new Command('images');
$program->addFlag('--no-color', 'Print plain output without colors');
$program->addCommandsFrom($tools);

$parser = new Parser;
$args = $parser->parse($program);
if ($args->get('--no-color')) {
	$out->setColorDepth(ColorDepth::None);
	$err->setColorDepth(ColorDepth::None);
}

Aby se volba týkala i nápovědy a chybových hlášek, vypisujte je přes renderery s těmito konzolemi: new HelpRenderer($out) pro nápovědu a new HelpRenderer($err) pro chyby. Jen chyba v samotné příkazové řádce se nahlásí dřív, než parser příznak přečte, a zůstane proto barevná. Úplně všude barvy vypne proměnná prostředí NO_COLOR.

Konzoli bez barev můžete vytvořit i rovnou, new Console(STDOUT, colorDepth: ColorDepth::None).

Konzoli se můžete i zeptat, jak na tom je:

$console->hasColors();      // jestli vypisuje barevně
$console->getColorDepth();  // kolik barev používá, třeba ColorDepth::Ansi256
$console->isTerminal();     // jestli výstup vede do terminálu, kde se někdo dívá
$console->getWidth();       // šířka terminálu ve sloupcích, mimo terminál 80

hasColors() a isTerminal() odpovídají na dvě různé otázky. Podle první se rozhodujte, jestli barvit, podle druhé, jestli ukazovat věci, které mají smysl jen pro člověka u obrazovky, třeba řádek s průběhem nebo dotaz. Uživatel totiž může barvy vypnout a přitom u terminálu sedět.

getWidth() se terminálu zeptá jen jednou a výsledek si pamatuje, takže změnu velikosti okna pozná až nová konzole. Proměnná prostředí COLUMNS má přednost, což se hodí, když chcete vyzkoušet, jak výstup vypadá v úzkém terminálu.

Klikací odkazy

Odkaz se skládá stejně jako barva. V terminálu z textu udělá link() odkaz, na který jde kliknout. V souboru, v rouře nebo bez barev vypíše text a za ním adresu v závorce, takže v logu adresa nezmizí:

$console->writeLine('See ' . $console->link('https://nette.org'));
$console->writeLine($console->link('https://nette.org', 'the manual'));  // mimo terminál 'the manual (https://nette.org)'

link() odmítne adresu s řídicími znaky, protože by mohly odkaz předčasně ukončit a poslat terminálu vlastní příkazy. Znaky s diakritikou a jiné znaky mimo ASCII nevadí.

Průběh a stavové řádky

Při delší práci se hodí ukazovat, co skript právě dělá a kolik má hotovo. Na to je setStatus(). Vykreslí řádek na konec výstupu a další výpis ho smaže, vypíše se nad ním a další setStatus() řádek nakreslí znovu. Uživatel tak vidí přibývající výsledky a pod nimi jeden řádek, který se průběžně mění:

photo.jpg converted
logo.png converted
3/10  holiday.jpg

V kódu to vypadá takhle:

foreach ($files as $i => $file) {
	$console->setStatus(sprintf('%d/%d  %s', $i + 1, count($files), $file));
	// ... převod souboru
	$console->writeLine("$file converted");  // smaže stavový řádek a vypíše se nad něj
}

$console->clearStatus();

Stavový řádek se v terminálu jen kreslí, do výstupu se nezapisuje. Když výstup do terminálu nevede, setStatus() neudělá vůbec nic, takže v souboru s logem jsou jen skutečné řádky. Stavových řádků může být i víc najednou, každý znak nového řádku začne další. Příliš dlouhý řádek se zkrátí na šířku terminálu, protože zalomený by se nedal překreslit.

Dokud je stavový řádek vidět, kurzor je schovaný. Objeví se znovu, když stav smažete, když konzoli uvolníte a také na konci skriptu, i po exit(), nezachycené výjimce nebo fatální chybě. Jen Ctrl+C ukončí PHP okamžitě, takže ho musí ošetřit vaše aplikace sama.

Stavový řádek smaže jen konzole, která ho nakreslila. Máte-li zvlášť konzoli pro výsledky a zvlášť pro chyby, kreslete stav na tu, kam jdou chyby. Jinak by se varování vypsané během práce otisklo přes stavový řádek.

Měření textu

Když chcete vypisovat zarovnané sloupce, strlen() vám nepomůže. Barvy jsou v textu uložené jako neviditelné escape sekvence, které nezabírají žádné místo, a čínský znak nebo emoji zabere na obrazovce dva sloupce. Třída Nette\CommandLine\Ansi měří a zkracuje text tak, jak ho ukáže terminál:

use Nette\CommandLine\Ansi;

Ansi::measure($console->color('red', 'error'));  // 5, escape sekvence se nepočítají
Ansi::pad($console->color('red', 'error'), 10);  // doplní na 10 sloupců, ne na 10 bajtů
Ansi::truncate($path, 30, keepEnd: true);        // zkrátí začátek, aby zůstalo jméno souboru
Ansi::strip($text);                              // text bez escape sekvencí

Šířky se řídí tabulkami Unicode, takže správně změří i emoji a vlajky. Řídicí znaky, třeba tabulátor, nezabírají žádný sloupec. strip() odstraní jen escape sekvence, ostatní řídicí znaky v textu nechá, takže z nedůvěryhodného textu bezpečný pro výpis neudělá.

Dotaz na uživatele

Někdy skript potřebuje rozhodnutí, které může udělat jen člověk. Přepsat existující soubor? Přeskočit obrázek, který nejde přečíst? Nette\CommandLine\Prompt položí otázku a počká, až uživatel odpoví jednou z možností, které povolíte:

use Nette\CommandLine\Prompt;

$prompt = new Prompt($console);
if ($prompt->isInteractive()) {
	$answer = $prompt->choose('Overwrite photo.webp?', ['y', 'n', 'a', 'q'], default: 'n');
}

Uživatel uvidí Overwrite photo.webp? [y/N/a/q] a výchozí možnost poznáte podle velkého písmena. Které možnosti nabídnete, je na vás, tady by to mohlo být ano, ne, ano pro všechny a konec. Dokud uživatel neodpoví jednou z nich, prompt se ptá znovu. Na velikosti písmen nezáleží a choose() vrátí možnost přesně tak, jak jste ji zadali. Enter na prázdném řádku vybere výchozí možnost, a pokud žádná není, prompt se zeptá znovu.

Když si každá možnost zaslouží pár slov, předejte je jako pole s popisy a prompt nad otázkou vypíše nabídku:

$answer = $prompt->choose('What now?', [
	's' => 'Skip this image',
	'r' => 'Retry',
	'c' => 'Cancel the conversion',
], default: 'r');
  [s] Skip this image
  [r] Retry
  [c] Cancel the conversion
What now? [s/R/c]

Když vstup skončí, třeba protože uživatel stiskl Ctrl+D, vrátí choose() hodnotu null, nikdy výchozí možnost. Zavřený vstup tak nemůže potichu odpovědět ano. Co to pro skript znamená, rozhodnete sami, třeba ?? 'q'.

Prompt se ptá jen tehdy, když vstup i konzole vedou do terminálu, takže nikdy nevezme odpověď z roury, u které nikdo nesedí. Jinak choose() vyhodí LogicException dřív, než cokoli vypíše nebo přečte. Proto se nejdřív zeptejte isInteractive() a rozhodněte, co skript udělá bez člověka, třeba skončí s radou použít volbu příkazové řádky, jako --overwrite. Předejte promptu konzoli, která kreslí stavový řádek: otázka ho pak smaže a další stav se po odpovědi nakreslí na čistý řádek.