diff --git a/src/Commands/Help.php b/src/Commands/Help.php index 9bf8d19..8329dc9 100644 --- a/src/Commands/Help.php +++ b/src/Commands/Help.php @@ -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 $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 $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 diff --git a/src/Languages/en/cli.php b/src/Languages/en/cli.php index 1991349..790ba6b 100644 --- a/src/Languages/en/cli.php +++ b/src/Languages/en/cli.php @@ -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}"', diff --git a/src/Languages/es/cli.php b/src/Languages/es/cli.php index 9d6e08a..61997a4 100644 --- a/src/Languages/es/cli.php +++ b/src/Languages/es/cli.php @@ -17,6 +17,7 @@ 'availableCommands' => 'Comandos Disponibles', 'aliases' => 'Alias', 'argument' => 'argumento', + 'arguments' => 'Argumentos', 'command' => 'Comando', 'commandNotFound' => 'Comando no encontrado: "{0}"', 'commands' => 'Comandos', diff --git a/src/Languages/pt-br/cli.php b/src/Languages/pt-br/cli.php index 4c5446a..5839010 100644 --- a/src/Languages/pt-br/cli.php +++ b/src/Languages/pt-br/cli.php @@ -17,6 +17,7 @@ 'availableCommands' => 'Comandos Disponíveis', 'aliases' => 'Aliases', 'argument' => 'argumento', + 'arguments' => 'Argumentos', 'command' => 'Comando', 'commandNotFound' => 'Comando não encontrado: "{0}"', 'commands' => 'Comandos', diff --git a/tests/ConsoleTest.php b/tests/ConsoleTest.php index 9957c61..15c06db 100644 --- a/tests/ConsoleTest.php +++ b/tests/ConsoleTest.php @@ -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); + } }