Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Views/errors/cli/error_exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
}

// The backtrace
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
if ((defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) || CLI::getOutput()->isVerbose()) {
$backtraces = $last->getTrace();

if ($backtraces) {
Expand Down
11 changes: 8 additions & 3 deletions system/CLI/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ public function hasNegation(string $name): bool
* Resolution order:
* 1. An explicit `setInteractive()` call wins.
* 2. Otherwise, the `--no-interaction` / `-N` flag from the current `run()`
* forces non-interactive.
* or a quiet output forces non-interactive.
* 3. Otherwise, the command is interactive when STDIN is a TTY.
*
* Non-CLI contexts (e.g., a controller invoking `command()`) don't expose
Expand Down Expand Up @@ -458,7 +458,7 @@ final public function run(array $arguments, array $options, ?Output $output = nu
}

// Reset per-run interactive state from the current options.
$this->runtimeInteractive = $this->hasUnboundOption('no-interaction', $options) ? false : null;
$this->runtimeInteractive = $this->hasUnboundOption('no-interaction', $options) || $this->getOutput()->isQuiet() ? false : null;

$this->initialize($arguments, $options);

Expand Down Expand Up @@ -771,6 +771,8 @@ protected function getValidatedOption(string $name): array|bool|string|null
* - `--no-header`: `Console` strips it before rendering the banner.
* - `--no-interaction` / `-N`: `run()` folds it into the interactive state
* and `resolveChildInteractiveState()` reads it to drive the `call()` cascade.
* - `--silent`, `--quiet` / `-q` and `--verbose` / `-v`: `Console` reads them into the
* verbosity of the output.
*
* Subclasses that override this hook should re-register these options or
* accept that the corresponding framework features will be broken for
Expand All @@ -781,7 +783,10 @@ protected function provideDefaultOptions(): void
$this
->addOption(new Option(name: 'help', shortcut: 'h', description: 'Display help for the given command.'))
->addOption(new Option(name: 'no-header', description: 'Do not display the banner when running the command.'))
->addOption(new Option(name: 'no-interaction', shortcut: 'N', description: 'Do not ask any interactive questions.'));
->addOption(new Option(name: 'no-interaction', shortcut: 'N', description: 'Do not ask any interactive questions.'))
->addOption(new Option(name: 'silent', description: 'Do not output any message, not even errors.'))
->addOption(new Option(name: 'quiet', shortcut: 'q', description: 'Do not output any message except errors.'))
->addOption(new Option(name: 'verbose', shortcut: 'v', description: 'Increase the verbosity of messages (-v, -vv, or -vvv).'));
}

/**
Expand Down
188 changes: 167 additions & 21 deletions system/CLI/Console.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use CodeIgniter\CodeIgniter;
use Config\App;
use Config\Services;
use Throwable;

class Console
{
Expand All @@ -37,15 +38,85 @@ class Console
*/
public function run(array $tokens = [])
{
if ($tokens === []) {
$fromArgv = $tokens === [];

if ($fromArgv) {
$tokens = service('superglobals')->server('argv', []);
}

$parser = new CommandLineParser($tokens);

$arguments = $parser->getArguments();
$this->options = $parser->getOptions();
$noHeader = $this->hasParameterOption(['no-header']);

$output = CLI::getOutput();
$previousVerbosity = $output->getVerbosity();
$previousShellVerbosity = getenv('SHELL_VERBOSITY');
$previousEnvShellVerbosity = $_ENV['SHELL_VERBOSITY'] ?? null;
$previousServerShellVerbosity = service('superglobals')->server('SHELL_VERBOSITY');

$output->setVerbosity($this->resolveVerbosity());
$this->setShellVerbosity($output->getVerbosity()->value);

try {
$exitCode = $this->dispatch($arguments);
} catch (Throwable $e) {
// The spark run keeps the level, so the exception handler renders the uncaught throwable at it.
if (! $fromArgv) {
$output->setVerbosity($previousVerbosity);
}

throw $e;
} finally {
$this->restoreShellVerbosity($previousShellVerbosity, $previousEnvShellVerbosity, $previousServerShellVerbosity);
}

$output->setVerbosity($previousVerbosity);

return $exitCode;
}

public function initialize(): static
{
Services::createRequest(config(App::class), true);
service('routes')->loadRoutes();

return $this;
}

/**
* Returns the command that is being executed.
*/
public function getCommand(): string
{
return $this->command;
}

/**
* Displays basic information about the Console.
*
* @return void
*/
public function showHeader(bool $suppress = false)
{
if ($suppress) {
return;
}

CLI::write(sprintf(
'CodeIgniter v%s Command Line Tool - Server Time: %s',
CodeIgniter::CI_VERSION,
date('Y-m-d H:i:s \\U\\T\\CP'),
), 'green');
CLI::newLine();
}

/**
* @param list<string> $arguments
*/
private function dispatch(array $arguments): int
{
$noHeader = $this->hasParameterOption(['no-header']);

if ($this->hasParameterOption(['help', 'h'])) {
if ($arguments === []) {
Expand Down Expand Up @@ -92,42 +163,116 @@ public function run(array $tokens = [])
return $commands->runLegacy($this->command, array_merge($arguments, $legacyOptions));
}

return $commands->runCommand($this->command, $arguments, $this->options, CLI::getOutput());
return $commands->runCommand($this->command, $arguments, $this->normalizeVerbosityOptions(), CLI::getOutput());
}

public function initialize(): static
/**
* Resolves the level from `--silent`, `--quiet`, the count of `-v`, or else `SHELL_VERBOSITY`.
*/
private function resolveVerbosity(): Verbosity
{
Services::createRequest(config(App::class), true);
service('routes')->loadRoutes();
$quiet = false;
$count = 0;

return $this;
foreach ($this->options as $name => $value) {
$option = $this->resolveVerbosityOption($name);

if ($option === 'silent') {
return Verbosity::Silent;
}

if ($option === 'quiet') {
$quiet = true;
} elseif ($option === 'verbose') {
$count += ($name === 'verbose' ? 1 : strlen($name)) * (is_array($value) ? count($value) : 1);
}
}

if ($quiet) {
return Verbosity::Quiet;
}

if ($count > 0) {
return Verbosity::from(min($count, Verbosity::Debug->value));
}

return Verbosity::tryFrom((int) env('SHELL_VERBOSITY', 0)) ?? Verbosity::Normal;
}

/**
* Returns the command that is being executed.
* Collapses repeated `--silent`, `-q` and `-v` tokens into the single `silent`, `quiet` and `verbose` flags of a modern command, keeping the first value so that validation rejects it.
*
* @return array<string, list<string|null>|string|null>
*/
public function getCommand(): string
private function normalizeVerbosityOptions(): array
{
return $this->command;
$options = [];
$flags = [];

foreach ($this->options as $name => $value) {
$option = $this->resolveVerbosityOption($name);

if ($option === null) {
$options[$name] = $value;

continue;
}

$flags[$option] ??= null;

foreach (is_array($value) ? $value : [$value] as $occurrence) {
$flags[$option] ??= $occurrence;
}
}

return $options + $flags;
}

/**
* Displays basic information about the Console.
* Returns `null` for an option unrelated to verbosity.
*
* @return void
* @return 'quiet'|'silent'|'verbose'|null
*/
public function showHeader(bool $suppress = false)
private function resolveVerbosityOption(int|string $name): ?string
{
if ($suppress) {
return;
return match (true) {
$name === 'silent' => 'silent',
$name === 'quiet', $name === 'q' => 'quiet',
$name === 'verbose', preg_match('/^v+$/', (string) $name) === 1 => 'verbose',
default => null,
};
}

/**
* Mirrors the level to `SHELL_VERBOSITY` for child processes.
*/
private function setShellVerbosity(int $value): void
{
putenv(sprintf('SHELL_VERBOSITY=%d', $value));
$_ENV['SHELL_VERBOSITY'] = $value;
service('superglobals')->setServer('SHELL_VERBOSITY', $value);
}

/**
* Puts back each store of `SHELL_VERBOSITY`, removing the variable where it was absent (`false` or `null`).
*
* @param array<array-key, mixed>|float|int|string|null $server
*/
private function restoreShellVerbosity(false|string $process, mixed $env, array|float|int|string|null $server): void
{
putenv($process === false ? 'SHELL_VERBOSITY' : sprintf('SHELL_VERBOSITY=%s', $process));

if ($env === null) {
unset($_ENV['SHELL_VERBOSITY']);
} else {
$_ENV['SHELL_VERBOSITY'] = $env;
}

CLI::write(sprintf(
'CodeIgniter v%s Command Line Tool - Server Time: %s',
CodeIgniter::CI_VERSION,
date('Y-m-d H:i:s \\U\\T\\CP'),
), 'green');
CLI::newLine();
if ($server === null) {
service('superglobals')->unsetServer('SHELL_VERBOSITY');
} else {
service('superglobals')->setServer('SHELL_VERBOSITY', $server);
}
}

/**
Expand Down Expand Up @@ -168,6 +313,7 @@ private function hasParameterOption(array $options): bool
private function isInteractive(): bool
{
return ! $this->hasParameterOption(['no-interaction', 'N'])
&& ! CLI::getOutput()->isQuiet()
&& ! CLI::getInputOutput() instanceof NullInputOutput
&& defined('STDIN')
&& CLI::streamSupports('stream_isatty', STDIN);
Expand Down
Loading
Loading