ANSI escape codes in PHP
PHP CLI mode (`php -r`, `php script.php`) is byte-clean on STDOUT — `echo "\e[31mred\e[0m"` works since PHP 5.4 (the version that introduced `\e` in double-quoted strings; older code uses `"\x1b"` or `chr(27)`). On Windows, Conhost since Windows 10 1709 parses VT natively, but PHP recommends explicitly flipping the mode with `sapi_windows_vt100_support(STDOUT, true)` (available since PHP 7.2) so behaviour is uniform across hosts and CGI fallbacks. For more than ad-hoc echoes reach for **Symfony Console** — the canonical PHP CLI framework that powers Composer, Laravel `artisan`, Symfony `console`, and Drupal `drush`. Its `OutputFormatter` uses XML-like tags (`<info>...</info>`, `<fg=red;bg=white;options=bold>...</>`) that compile to ANSI under the hood and auto-strip when the stream is non-TTY. **Termwind** ships a Tailwind-CSS-for-the-terminal layer (class names like `text-red-500`, `bg-blue-500`, `font-bold`) and underpins PHPStan, Pest, Laravel Zero. **laravel/prompts** offers modern interactive prompts that work standalone outside Laravel.
Recommended libraries
- symfony/console
The canonical PHP CLI framework — argv parsing, formatted output, progress bars, table rendering, interactive `QuestionHelper` prompts. Output uses XML-like tags: `<info>green</info>`, `<error>red bg</error>`, `<fg=blue;bg=white;options=bold>...</>`. The formatter auto-strips ANSI when stdout is non-TTY. Used by Composer, Laravel `artisan`, Symfony `console`, Drupal `drush`.
- nunomaduro/termwind
Tailwind-CSS-for-the-terminal — render output with class names (`text-red-500`, `bg-blue-500`, `font-bold`, `mt-2`, `px-2`, `rounded`) and HTML-like markup (`render('<div class="text-red-500">oops</div>')`). Built atop Symfony Console's `OutputFormatter`. Underpins PHPStan, Pest, Laravel Zero, Lambdish.
- laravel/prompts
Modern interactive prompts — `text`, `password`, `confirm`, `select`, `multiselect`, `search`, `suggest`, with a polished visual design and keyboard navigation. The Laravel 10+ successor to Symfony Console's `QuestionHelper`, but installable and usable from any PHP project.
- league/climate
PHP League's CLI toolkit — coloured output, tables, progress bars, animations, padding, draw helpers — with a fluent API (`$climate->red()->bold()->out('oops')`). A lighter alternative to Symfony Console for scripts that don't need full command-routing scaffolding.
Idiomatic patterns
<?php
echo "\e[1;31merror:\e[0m permission denied\n";
// PHP 5.3 fallback (no \e literal) — use \x1b or chr(27):
echo "\x1b[1;31merror:\x1b[0m permission denied\n";<?php
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
$out = new ConsoleOutput();
// <info>, <comment>, <question>, <error> ship by default. Define your own:
$out->getFormatter()->setStyle(
'fire',
new OutputFormatterStyle('red', null, ['bold'])
);
$out->writeln('<info>ok:</info> all 142 tests passed');
$out->writeln('<fire>error:</fire> permission denied');
$out->writeln('<fg=blue;bg=white;options=bold>highlighted</> back to normal');
// Auto-strips ANSI when stdout is piped (`php script.php | cat`).<?php
// Stack the canonical signals in priority order:
// --no-color flag → NO_COLOR env → FORCE_COLOR → !isatty → Win VT opt-in
function shouldUseColor(): bool {
if (in_array('--no-color', $GLOBALS['argv'] ?? [], true)) return false;
if (getenv('NO_COLOR') !== false) return false;
if (getenv('FORCE_COLOR') !== false && getenv('FORCE_COLOR') !== '0') return true;
// Windows: PHP 7.2+ exposes a switch for Conhost VT mode. Returns false
// on legacy hosts (Windows 7/8) so the caller correctly falls back.
if (DIRECTORY_SEPARATOR === '\\') {
return function_exists('sapi_windows_vt100_support')
&& sapi_windows_vt100_support(STDOUT, true);
}
// POSIX: STDOUT must be a real TTY.
return function_exists('posix_isatty') && posix_isatty(STDOUT);
}
$useColor = shouldUseColor();
$paint = fn(string $sgr, string $text): string =>
$useColor ? "\e[{$sgr}m{$text}\e[0m" : $text;
echo $paint('1;31', 'error:'), ' permission denied', "\n";<?php
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\ConsoleOutput;
$out = new ConsoleOutput();
$progress = new ProgressBar($out, 100);
$progress->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%');
$progress->setBarCharacter('<fg=green>=</>');
$progress->setProgressCharacter('<fg=green>></>');
$progress->setEmptyBarCharacter('<fg=gray>-</>');
$progress->start();
for ($i = 0; $i < 100; $i++) {
usleep(20_000);
$progress->advance();
}
$progress->finish();
$out->writeln('');