Skip to content

Commit de70473

Browse files
committed
feat: send spark error output to STDERR
1 parent 102bc17 commit de70473

11 files changed

Lines changed: 139 additions & 12 deletions

File tree

‎system/CLI/BaseCommand.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ protected function showError(Throwable $e)
150150
$message = $e->getMessage();
151151
$config = config(Exceptions::class);
152152

153-
require $config->errorViewPath . '/cli/error_exception.php';
153+
CLI::redirectToStderr(function () use ($exception, $message, $config): void {
154+
require $config->errorViewPath . '/cli/error_exception.php';
155+
});
154156
}
155157

156158
/**

‎system/CLI/CLI.php‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ class CLI
136136
*/
137137
protected static ?InputOutput $io = null;
138138

139+
protected static bool $stdoutToStderr = false;
140+
139141
/**
140142
* Static "constructor".
141143
*
@@ -1138,23 +1140,51 @@ public static function table(array $tbody, array $thead = [])
11381140
*/
11391141
protected static function fwrite($handle, string $string)
11401142
{
1143+
if (static::$stdoutToStderr && $handle === STDOUT) {
1144+
$handle = STDERR;
1145+
}
1146+
11411147
static::$io->fwrite($handle, $string);
11421148
}
11431149

1150+
/**
1151+
* Runs the callback with its STDOUT writes sent to STDERR.
1152+
*
1153+
* @param callable(): void $callback
1154+
*
1155+
* @internal
1156+
*/
1157+
public static function redirectToStderr(callable $callback): void
1158+
{
1159+
$stdoutToStderr = static::$stdoutToStderr;
1160+
$isColored = static::$isColored;
1161+
1162+
static::$stdoutToStderr = true;
1163+
static::$isColored = static::hasColorSupport(STDERR);
1164+
1165+
try {
1166+
$callback();
1167+
} finally {
1168+
static::$stdoutToStderr = $stdoutToStderr;
1169+
static::$isColored = $isColored;
1170+
}
1171+
}
1172+
11441173
/**
11451174
* Testing purpose only
11461175
*
11471176
* @internal
11481177
*/
11491178
public static function reset(): void
11501179
{
1151-
static::$initialized = false;
1152-
static::$segments = [];
1153-
static::$options = [];
1154-
static::$lastWrite = null;
1155-
static::$height = null;
1156-
static::$width = null;
1157-
static::$isColored = static::hasColorSupport(STDOUT);
1180+
static::$initialized = false;
1181+
static::$segments = [];
1182+
static::$options = [];
1183+
static::$lastWrite = null;
1184+
static::$height = null;
1185+
static::$width = null;
1186+
static::$isColored = static::hasColorSupport(STDOUT);
1187+
static::$stdoutToStderr = false;
11581188

11591189
static::resetInputOutput();
11601190
}

‎system/Debug/ExceptionHandler.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use Closure;
1717
use CodeIgniter\API\ResponseTrait;
18+
use CodeIgniter\CLI\CLI;
1819
use CodeIgniter\Exceptions\PageNotFoundException;
1920
use CodeIgniter\HTTP\CLIRequest;
2021
use CodeIgniter\HTTP\Exceptions\HTTPException;
@@ -121,7 +122,11 @@ public function handle(
121122
}
122123

123124
// Displays the HTML or CLI error code.
124-
$this->render($exception, $statusCode, $viewFile);
125+
if ($request instanceof IncomingRequest) {
126+
$this->render($exception, $statusCode, $viewFile);
127+
} else {
128+
CLI::redirectToStderr(fn () => $this->render($exception, $statusCode, $viewFile));
129+
}
125130

126131
if (! service('environment')->isTesting()) {
127132
exit($exitCode); // @codeCoverageIgnore

‎tests/system/CLI/AbstractCommandTest.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use CodeIgniter\Commands\Help;
2727
use CodeIgniter\Exceptions\LogicException;
2828
use CodeIgniter\Test\CIUnitTestCase;
29+
use CodeIgniter\Test\Filters\CITestStreamFilter;
2930
use CodeIgniter\Test\Mock\MockInputOutput;
3031
use CodeIgniter\Test\StreamFilterTrait;
3132
use Config\App;
@@ -227,6 +228,8 @@ public function testRenderThrowable(): void
227228
{
228229
$command = new AppAboutCommand(new Commands());
229230

231+
CITestStreamFilter::removeOutputFilter();
232+
230233
$this->assertSame(EXIT_ERROR, $command->bomb());
231234
$this->assertStringContainsString('[CodeIgniter\CLI\Exceptions\CLIException]', $this->getStreamFilterBuffer());
232235
$this->assertStringContainsString('Invalid "background" color: "Background".', $this->getStreamFilterBuffer());

‎tests/system/CLI/BaseCommandTest.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use CodeIgniter\CodeIgniter;
1818
use CodeIgniter\Log\Logger;
1919
use CodeIgniter\Test\CIUnitTestCase;
20+
use CodeIgniter\Test\Filters\CITestStreamFilter;
2021
use CodeIgniter\Test\StreamFilterTrait;
2122
use PHPUnit\Framework\Attributes\After;
2223
use PHPUnit\Framework\Attributes\Before;
@@ -89,6 +90,8 @@ public function testShowError(): void
8990
{
9091
$command = new AppInfo(single_service('logger'), single_service('commands'));
9192

93+
CITestStreamFilter::removeOutputFilter();
94+
9295
$this->assertSame(1, $command->bomb());
9396
$this->assertStringContainsString('[CodeIgniter\CLI\Exceptions\CLIException]', $this->getStreamFilterBuffer());
9497
$this->assertStringContainsString('Invalid "background" color: "Background".', $this->getStreamFilterBuffer());

‎tests/system/CLI/CLITest.php‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,55 @@ public function testMixedWriteError(): void
623623
);
624624
}
625625

626+
public function testRedirectToStderrRoutesStdoutWritesOnlyWhileTheCallbackRuns(): void
627+
{
628+
$io = new class () extends InputOutput {
629+
/**
630+
* @var list<array{string, string}>
631+
*/
632+
public array $writes = [];
633+
634+
public function fwrite($handle, string $string): void
635+
{
636+
$this->writes[] = [$handle === STDERR ? 'STDERR' : 'STDOUT', $string];
637+
}
638+
};
639+
CLI::setInputOutput($io);
640+
641+
CLI::redirectToStderr(static function (): void {
642+
CLI::write('inside');
643+
});
644+
645+
try {
646+
CLI::redirectToStderr(static function (): void {
647+
throw new RuntimeException('Callback failed.');
648+
});
649+
} catch (RuntimeException) {
650+
}
651+
652+
CLI::write('after');
653+
654+
$this->assertSame([['STDERR', "\ninside\n"], ['STDOUT', "after\n"]], $io->writes);
655+
}
656+
657+
public function testRedirectToStderrUsesTheColorSupportOfStderr(): void
658+
{
659+
(new ReflectionProperty(CLI::class, 'isColored'))->setValue(null, true);
660+
service('superglobals')->setServer('NO_COLOR', '1');
661+
662+
try {
663+
CLI::redirectToStderr(static function (): void {
664+
CLI::write('inside', 'red');
665+
});
666+
} finally {
667+
service('superglobals')->unsetServer('NO_COLOR');
668+
}
669+
670+
CLI::write('after', 'red');
671+
672+
$this->assertSame("\ninside\n\033[0;31mafter\033[0m\n", $this->getStreamFilterBuffer());
673+
}
674+
626675
public function testErrorForeground(): void
627676
{
628677
CLI::error('test', 'purple');

‎tests/system/Debug/ExceptionHandlerTest.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use CodeIgniter\HTTP\SiteURI;
2222
use CodeIgniter\HTTP\UserAgent;
2323
use CodeIgniter\Test\CIUnitTestCase;
24+
use CodeIgniter\Test\Filters\CITestStreamFilter;
2425
use CodeIgniter\Test\IniTestTrait;
2526
use CodeIgniter\Test\StreamFilterTrait;
2627
use Config\App;
@@ -253,6 +254,21 @@ public function testHandleCLIPageNotFoundException(): void
253254
$this->resetStreamFilterBuffer();
254255
}
255256

257+
public function testHandleCLIExceptionWritesToStderr(): void
258+
{
259+
$request = Services::clirequest(null, false);
260+
/** @var Response $response */
261+
$response = service('response', null, false);
262+
$response->pretend();
263+
264+
CITestStreamFilter::removeOutputFilter();
265+
266+
$this->handler->handle(new RuntimeException('Something broke.'), $request, $response, 500, EXIT_ERROR);
267+
268+
$this->assertStringContainsString('[' . RuntimeException::class . ']', $this->getStreamFilterBuffer());
269+
$this->assertStringContainsString('Something broke.', $this->getStreamFilterBuffer());
270+
}
271+
256272
public function testMaskSensitiveData(): void
257273
{
258274
$maskSensitiveData = self::getPrivateMethodInvoker($this->handler, 'maskSensitiveData');

‎user_guide_src/source/changelogs/v4.8.0.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Behavior Changes
2525

2626
- The static ``Boot::initializeConsole()`` method no longer handles the display of the console header. This is now handled within ``Console::run()``.
2727
If you have overridden ``Boot::initializeConsole()``, you should remove any code related to displaying the console header, as this is now the responsibility of the ``Console`` class.
28+
- **CLI:** Uncaught exceptions in ``spark``, and errors shown with ``BaseCommand::showError()`` or ``AbstractCommand::renderThrowable()`` (as ``migrate`` and
29+
``db:seed`` do), are now printed to STDERR instead of STDOUT. The exit code is unchanged.
30+
See :ref:`upgrade-480-cli-error-output`.
2831
- **Commands:** The ``-h`` option to ``routes`` command which was mapped previously to the ``--sort-by-handler`` option is now removed.
2932
Use ``--sort-by-handler`` instead to sort the routes by handler when running the ``routes`` command.
3033
- **Commands:** The ``filter:check`` command now requires the HTTP method argument to be uppercase (e.g., ``spark filter:check GET /`` instead of ``spark filter:check get /``).

‎user_guide_src/source/cli/cli_commands.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ be familiar with when creating your own commands. It also has a :doc:`Logger </g
127127
128128
:param Throwable $e: The exception to use for error reporting.
129129

130-
A convenience method to maintain a consistent and clear error output to the CLI:
130+
A convenience method to maintain a consistent and clear error output on STDERR:
131131

132132
.. literalinclude:: cli_commands/006.php
133133

‎user_guide_src/source/cli/cli_modern_commands.rst‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,8 @@ covered in the sections above and are not listed here.
595595
596596
:param Throwable $e: The throwable to render.
597597

598-
Produces the same formatted output the framework uses for uncaught
599-
exceptions. Safe to call from any command.
598+
Produces the same formatted output on STDERR that the framework uses
599+
for uncaught exceptions. Safe to call from any command.
600600

601601
.. php:method:: hasArgument(string $name): bool
602602

0 commit comments

Comments
 (0)