diff --git a/lib/Enterprise/CliFramework.php b/lib/Enterprise/CliFramework.php index fe68f33..91a9a0b 100644 --- a/lib/Enterprise/CliFramework.php +++ b/lib/Enterprise/CliFramework.php @@ -141,6 +141,26 @@ abstract class CliFramework /** @var float Script start time for elapsed-time reporting. */ private float $startTime; + // ========================================================================= + // Display output — all decorative output goes to stderr + // ========================================================================= + + /** + * Write decorative/diagnostic output to stderr. + * + * All non-data output (banners, progress bars, section headers, status + * lines, log messages) MUST use this method so that stdout is reserved + * for machine-readable data. This ensures that shell captures like + * VERSION=$(php version_read.php --path .) + * only receive the actual data, not decorative text. + * + * @since 04.00.16 + */ + protected function display(string $text): void + { + fwrite(STDERR, $text); + } + // ========================================================================= // Constructor // ========================================================================= @@ -326,14 +346,14 @@ abstract class CliFramework protected function printHelp(): void { $w = $this->termWidth(); - echo $this->c(self::C_BOLD . self::C_CYAN, $this->scriptName); + $this->display($this->c(self::C_BOLD . self::C_CYAN, $this->scriptName)); if ($this->description !== '') { - echo ' — ' . $this->description; + $this->display(' — ' . $this->description); } - echo "\n"; - echo $this->c(self::C_DIM, str_repeat(self::BOX_H, $w)) . "\n\n"; - echo $this->c(self::C_BOLD, 'Usage:') . " php {$this->scriptName}.php [options]\n\n"; - echo $this->c(self::C_BOLD, 'Options:') . "\n"; + $this->display("\n"); + $this->display($this->c(self::C_DIM, str_repeat(self::BOX_H, $w)) . "\n\n"); + $this->display($this->c(self::C_BOLD, 'Usage:') . " php {$this->scriptName}.php [options]\n\n"); + $this->display($this->c(self::C_BOLD, 'Options:') . "\n"); $builtIn = [ '--help' => ['desc' => 'Show this help message', 'default' => null], @@ -348,16 +368,16 @@ abstract class CliFramework $hint = ($default !== null && $default !== false) ? $this->c(self::C_DIM, " (default: {$default})") : ''; - printf( + $this->display(sprintf( " %s%-22s%s%s%s\n", self::C_CYAN, $name, self::C_RESET, $def['desc'], $hint - ); + )); } - echo "\n"; + $this->display("\n"); } // ========================================================================= @@ -378,23 +398,23 @@ abstract class CliFramework $titleLine = $this->padRight($titleStyled, $inner, strlen($titleRaw)); $descLine = ($desc !== '') ? $this->padRight(" {$desc}", $inner) : null; - echo "\n"; - echo $this->c( + $this->display("\n"); + $this->display($this->c( self::C_CYAN, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR - ) . "\n"; - echo $this->c(self::C_CYAN, self::BOX_V) + ) . "\n"); + $this->display($this->c(self::C_CYAN, self::BOX_V) . $this->c(self::C_BOLD, $titleLine) - . $this->c(self::C_CYAN, self::BOX_V) . "\n"; + . $this->c(self::C_CYAN, self::BOX_V) . "\n"); if ($descLine !== null) { - echo $this->c(self::C_CYAN, self::BOX_V) + $this->display($this->c(self::C_CYAN, self::BOX_V) . $this->c(self::C_DIM, $descLine) - . $this->c(self::C_CYAN, self::BOX_V) . "\n"; + . $this->c(self::C_CYAN, self::BOX_V) . "\n"); } - echo $this->c( + $this->display($this->c( self::C_CYAN, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR - ) . "\n\n"; + ) . "\n\n"); } /** Print the dry-run notice box. */ @@ -403,18 +423,18 @@ abstract class CliFramework $w = min($this->termWidth(), 70); $msg = ' ' . self::ICON_DRY . ' DRY-RUN MODE — no changes will be written '; $row = $this->padRight($msg, $w - 2); - echo $this->c( + $this->display($this->c( self::C_YELLOW . self::C_BOLD, self::BOX_TL . str_repeat(self::BOX_H, $w - 2) . self::BOX_TR - ) . "\n"; - echo $this->c( + ) . "\n"); + $this->display($this->c( self::C_YELLOW . self::C_BOLD, self::BOX_V . $row . self::BOX_V - ) . "\n"; - echo $this->c( + ) . "\n"); + $this->display($this->c( self::C_YELLOW . self::C_BOLD, self::BOX_BL . str_repeat(self::BOX_H, $w - 2) . self::BOX_BR - ) . "\n\n"; + ) . "\n\n"); } // ========================================================================= @@ -435,11 +455,11 @@ abstract class CliFramework $w = $this->termWidth(); $text = " {$title} "; $fill = max(0, $w - strlen($text) - 4); - echo "\n"; - echo $this->c( + $this->display("\n"); + $this->display($this->c( self::C_CYAN, str_repeat(self::BOX_H, 2) . $text . str_repeat(self::BOX_H, $fill) - ) . "\n\n"; + ) . "\n\n"); } /** Print a plain horizontal divider. */ @@ -449,7 +469,7 @@ abstract class CliFramework return; } $this->clearProgress(); - echo $this->c(self::C_DIM, str_repeat(self::BOX_H, $this->termWidth())) . "\n"; + $this->display($this->c(self::C_DIM, str_repeat(self::BOX_H, $this->termWidth())) . "\n"); } // ========================================================================= @@ -495,11 +515,7 @@ abstract class CliFramework $line = "{$ts} {$icon} {$badge} {$text}\n"; - if ($level === 'ERROR') { - fwrite(STDERR, $line); - } else { - echo $line; - } + $this->display($line); } /** Log a success message. */ @@ -564,7 +580,7 @@ abstract class CliFramework ? ' ' . $this->c(self::C_DIM, "— {$detail}") : ''; - echo ' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n"; + $this->display(' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n"); } // ========================================================================= @@ -601,10 +617,10 @@ abstract class CliFramework $line = " [{$bar}] {$percent} {$counter}{$suffix}"; if ($newline) { - echo "\r{$line}\n"; + $this->display("\r{$line}\n"); $this->progressActive = false; } else { - echo "\r{$line}"; + $this->display("\r{$line}"); $this->progressActive = true; } } @@ -613,7 +629,7 @@ abstract class CliFramework protected function clearProgress(): void { if ($this->progressActive) { - echo "\r" . str_repeat(' ', $this->termWidth()) . "\r"; + $this->display("\r" . str_repeat(' ', $this->termWidth()) . "\r"); $this->progressActive = false; } } @@ -644,8 +660,8 @@ abstract class CliFramework $maxKey = max(array_map('strlen', array_keys($rows))); $inner = $maxKey + 20; - echo "\n"; - echo $this->c($color, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR) . "\n"; + $this->display("\n"); + $this->display($this->c($color, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR) . "\n"); foreach ($rows as $label => $value) { $valStr = (string) $value; @@ -653,10 +669,10 @@ abstract class CliFramework $padding = $inner - strlen($label) - $valVis - 4; $row = ' ' . $this->c(self::C_BOLD, $label) . str_repeat(' ', max(1, $padding)) . $valStr . ' '; - echo $this->c($color, self::BOX_V) . $row . $this->c($color, self::BOX_V) . "\n"; + $this->display($this->c($color, self::BOX_V) . $row . $this->c($color, self::BOX_V) . "\n"); } - echo $this->c($color, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR) . "\n\n"; + $this->display($this->c($color, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR) . "\n\n"); } /** @@ -702,7 +718,7 @@ abstract class CliFramework $this->clearProgress(); $badge = $this->c(self::C_BOLD . self::C_MAGENTA, "Step {$current}/{$total}"); $arrow = $this->c(self::C_DIM, self::ICON_INFO); - echo "\n{$badge} {$arrow} {$title}\n"; + $this->display("\n{$badge} {$arrow} {$title}\n"); } // ========================================================================= @@ -964,13 +980,13 @@ abstract class CliFramework } // Header. - echo $sep . "\n"; + $this->display($sep . "\n"); $headerLine = '|'; foreach ($headers as $i => $h) { $headerLine .= ' ' . $this->c(self::C_BOLD, str_pad($h, $widths[$i])) . ' |'; } - echo $headerLine . "\n"; - echo $sep . "\n"; + $this->display($headerLine . "\n"); + $this->display($sep . "\n"); // Rows. foreach ($rows as $row) { @@ -978,9 +994,9 @@ abstract class CliFramework foreach ($row as $i => $cell) { $line .= ' ' . str_pad((string) $cell, $widths[$i]) . ' |'; } - echo $line . "\n"; + $this->display($line . "\n"); } - echo $sep . "\n"; + $this->display($sep . "\n"); } }