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
169 changes: 142 additions & 27 deletions src/Commands/Help.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,39 +82,154 @@ protected function showCommand(string $commandName) : void
ForegroundColor::green
) . $value);
}
$value = $command->getOptions();
if ($value) {
CLI::write(
$this->console->getLanguage()->render('cli', 'options') . ': ',
ForegroundColor::green
);
$newOptions = [];
foreach ($value as $options => $description) {
$options = $this->sortOptions($options);
$newOptions[$options] = $description;
$this->showArguments($command);
$this->showOptions($command);
}

/**
* Print the Arguments block from the command definitions.
*
* @param Command $command The command being documented
*/
protected function showArguments(Command $command) : void
{
$definitions = $command->getArgumentDefinitions();
if (!$definitions) {
return;
}
CLI::write(
$this->console->getLanguage()->render('cli', 'arguments') . ':',
ForegroundColor::green
);
$lastKey = \array_key_last($definitions);
foreach ($definitions as $position => $definition) {
CLI::write(' ' . $position . ' ' . $this->describeDefinition($definition));
$raw = $definition['description'] ?? null;
$description = \is_string($raw) ? \trim($raw) : '';
if ($description !== '') {
CLI::write(' ' . $this->finishSentence($description));
}
if ($position !== $lastKey) {
CLI::newLine();
}
}
CLI::newLine();
}

/**
* Print the Options block, derived from the option definitions when the
* command declares them and completed with the legacy free text options
* map for entries that are not covered by a definition.
*
* @param Command $command The command being documented
*/
protected function showOptions(Command $command) : void
{
$definitions = $command->getOptionDefinitions();
$legacy = $command->getOptions();
if (!$definitions && !$legacy) {
return;
}
CLI::write(
$this->console->getLanguage()->render('cli', 'options') . ':',
ForegroundColor::green
);
$entries = [];
foreach ($definitions as $key => $definition) {
$raw = $definition['description'] ?? null;
$description = \is_string($raw) ? \trim($raw) : '';
if ($description === '') {
$description = \trim((string) ($this->findLegacyDescription((string) $key, $legacy) ?? ''));
}
$entries[$this->formatOptionKey((string) $key)] = [
'meta' => $this->describeDefinition($definition),
'description' => $description,
];
}
foreach ($legacy as $key => $description) {
$display = $this->formatOptionKey((string) $key);
if (!\array_key_exists($display, $entries)) {
$entries[$display] = [
'meta' => '',
'description' => \trim((string) $description),
];
}
}
\ksort($entries);
$lastKey = \array_key_last($entries);
foreach ($entries as $option => $entry) {
CLI::write(' ' . $this->setColor($option));
if ($entry['meta'] !== '') {
CLI::write(' ' . $entry['meta']);
}
\ksort($newOptions);
$lastKey = \array_key_last($newOptions);
foreach ($newOptions as $option => $description) {
CLI::write(' ' . $this->setColor($option));
$description = \trim($description);
if (!\str_ends_with($description, '.')) {
$description .= '.';
}
CLI::write(' ' . $description);
if ($option !== $lastKey) {
CLI::newLine();
}
if ($entry['description'] !== '') {
CLI::write(' ' . $this->finishSentence($entry['description']));
}
if ($option !== $lastKey) {
CLI::newLine();
}
}
}

/**
* Build the meta description of a definition, like
* "required, int, default \"5\"".
*
* @param array<string,mixed> $definition The argument or option definition
*/
protected function describeDefinition(array $definition) : string
{
$parts = [!empty($definition['required']) ? 'required' : 'optional'];
$type = $definition['type'] ?? 'string';
if (\is_string($type) && $type !== '') {
$parts[] = $type;
}
$default = $definition['default'] ?? null;
if (\is_scalar($default)) {
$parts[] = 'default ' . (\is_bool($default)
? ($default ? 'true' : 'false')
: '"' . $default . '"');
}
return \implode(', ', $parts);
}

protected function sortOptions(string $text) : string
/**
* Normalize an option key for display, adding dashes to definition keys
* and keeping legacy keys as they were written.
*/
protected function formatOptionKey(string $key) : string
{
$cleaned = \preg_replace('/\s+/', '', $text);
$text = \explode(',', \is_string($cleaned) ? $cleaned : '');
\sort($text);
return \implode(',', $text);
if ($key !== '' && $key[0] === '-') {
return $key;
}
return \strlen($key) > 1 ? '--' . $key : '-' . $key;
}

/**
* Find the legacy free text description of a definition key.
*
* @param array<string,bool|string> $legacy The legacy options map
*/
protected function findLegacyDescription(string $key, array $legacy) : ?string
{
foreach ([$key, '-' . $key, '--' . $key] as $candidate) {
if (\array_key_exists($candidate, $legacy)) {
$value = $legacy[$candidate];
return \is_bool($value) ? '' : $value;
}
}
return null;
}

/**
* Make sure a description ends with a sentence dot.
*/
protected function finishSentence(string $description) : string
{
if (!\str_ends_with($description, '.')) {
$description .= '.';
}
return $description;
}

protected function setColor(string $text) : string
Expand Down
1 change: 1 addition & 0 deletions src/Languages/en/cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'about.line5' => 'Thanks for using Webisters!',
'aliases' => 'Aliases',
'argument' => 'argument',
'arguments' => 'Arguments',
'availableCommands' => 'Available Commands',
'command' => 'Command',
'commandNotFound' => 'Command not found: "{0}"',
Expand Down
1 change: 1 addition & 0 deletions src/Languages/es/cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'availableCommands' => 'Comandos Disponibles',
'aliases' => 'Alias',
'argument' => 'argumento',
'arguments' => 'Argumentos',
'command' => 'Comando',
'commandNotFound' => 'Comando no encontrado: "{0}"',
'commands' => 'Comandos',
Expand Down
1 change: 1 addition & 0 deletions src/Languages/pt-br/cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'availableCommands' => 'Comandos Disponíveis',
'aliases' => 'Aliases',
'argument' => 'argumento',
'arguments' => 'Argumentos',
'command' => 'Comando',
'commandNotFound' => 'Comando não encontrado: "{0}"',
'commands' => 'Comandos',
Expand Down
32 changes: 32 additions & 0 deletions tests/ConsoleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -523,4 +523,36 @@ public function testNoAnsiOptionIsScopedToTheDispatch() : void
$this->console->exec('index --no-ansi');
self::assertTrue(CLI::isAnsi());
}

public function testHelpShowsArgumentAndOptionDefinitions() : void
{
$command = new CommandMock($this->console);
$command->setArgumentDefinitions([
0 => ['type' => 'int', 'required' => true, 'description' => 'The record id'],
]);
$command->setOptionDefinitions([
'count' => ['type' => 'int', 'default' => 5, 'description' => 'How many records'],
]);
$this->console->addCommand($command);
Stdout::reset();
$this->console->exec('test --help');
$contents = Stdout::getContents();
self::assertStringContainsString('Arguments', $contents);
self::assertStringContainsString('The record id.', $contents);
self::assertStringContainsString('Options', $contents);
self::assertStringContainsString('--count', $contents);
self::assertStringContainsString('required, int', $contents);
self::assertStringContainsString('default "5"', $contents);
self::assertStringContainsString('How many records.', $contents);
}

public function testHelpFallsBackToLegacyOptionsMap() : void
{
$this->console->addCommand(new CommandMock($this->console));
Stdout::reset();
$this->console->exec('test --help');
$contents = Stdout::getContents();
self::assertStringContainsString('Options', $contents);
self::assertStringContainsString('foo bar', $contents);
}
}
Loading