diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 46080820e064b..1958a4fcc9a6f 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -268,6 +268,34 @@ public function getHelp() return implode(PHP_EOL, $messages); } + /** + * Gets the help message in Markdown. + * + * @return string A help message in Markdown. + */ + public function getHelpInMarkdown() + { + $messages = array( + $this->getLongVersionInMarkdown(), + '', + 'Usage', + '-----', + sprintf(" [options] command [arguments]\n"), + 'Options', + '-------' + ); + + foreach ($this->getDefinition()->getOptions() as $option) { + $messages[] = sprintf(' %-16s %s %s', + '--'.$option->getName(), + $option->getShortcut() ? '(-'.$option->getShortcut().')' : ' ', + $option->getDescription() + ); + } + + return implode(PHP_EOL, $messages); + } + /** * Sets whether to catch exceptions or not during commands execution. * @@ -356,6 +384,22 @@ public function getLongVersion() return 'Console Tool'; } + /** + * Returns the long version of the application in Markdown. + * + * @return string The long application version in Markdown + * + * @api + */ + public function getLongVersionInMarkdown() + { + if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { + return sprintf('%s version %s', $this->getName(), $this->getVersion()); + } + + return implode(PHP_EOL, array('Console Tool', '============')); + } + /** * Registers a new command. * @@ -680,11 +724,7 @@ public function asText($namespace = null, $raw = false) { $commands = $namespace ? $this->all($this->findNamespace($namespace)) : $this->commands; - $width = 0; - foreach ($commands as $command) { - $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; - } - $width += 2; + $width = $this->getMaximumCommandNameLength($commands) + 2; if ($raw) { $messages = array(); @@ -718,6 +758,43 @@ public function asText($namespace = null, $raw = false) return implode(PHP_EOL, $messages); } + /** + * Returns Markdown representation of the Application. + * + * @param string $namespace An optional namespace name + * + * @return string Markdown representing the Application + */ + public function asMarkdown($namespace = null) + { + $commands = $namespace ? $this->all($this->findNamespace($namespace)) : $this->commands; + + $width = $this->getMaximumCommandNameLength($commands) + 2; + + $messages = array($this->getHelpInMarkdown(), ''); + if ($namespace) { + $message = sprintf("Available commands for the \"%s\" namespace", $namespace); + $messages[] = $message; + $messages[] = str_repeat('-', mb_strlen($message)); + } else { + $messages[] = 'Available commands'; + $messages[] = '------------------'; + } + + // add commands by namespace + foreach ($this->sortCommands($commands) as $space => $commands) { + if (!$namespace && '_global' !== $space) { + $messages[] = ''; + } + + foreach ($commands as $name => $command) { + $messages[] = sprintf(" %-${width}s %s", $name, $command->getDescription()); + } + } + + return implode(PHP_EOL, $messages); + } + /** * Returns an XML representation of the Application. * @@ -954,6 +1031,15 @@ protected function getDefaultHelperSet() )); } + private function getMaximumCommandNameLength(array $commands) { + $width = 0; + foreach ($commands as $command) { + $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; + } + + return $width; + } + /** * Runs and parses stty -a if it's available, suppressing any error output * diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index 1b11bb8105d5e..98c9ec6e7534f 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -583,6 +583,48 @@ public function asText() return implode("\n", $messages); } + /** + * Returns Markdown representation of the command. + * + * @return string Markdown representing the command + */ + public function asMarkdown() + { + if ($this->application && !$this->applicationDefinitionMerged) { + $this->getSynopsis(); + $this->mergeApplicationDefinition(false); + } + + $markdown = array(); + $markdown[] = $this->name; + $markdown[] = \str_repeat('=', mb_strlen($this->name)); + $markdown[] = ''; + $markdown[] = $this->description; + $markdown[] = ''; + $markdown[] = 'Usage'; + $markdown[] = '-----'; + $markdown[] = ' '.$this->getSynopsis(); + $markdown[] = ''; + + if ($this->getAliases()) { + $markdown[] = 'Aliases'; + $markdown[] = '-------'; + foreach ($this->getAliases() as $alias) { + $markdown[] = '* '.$alias; + } + $markdown[] = ''; + } + + $markdown[] = $this->getNativeDefinition()->asMarkdown(); + if ($help = $this->getProcessedHelp()) { + $markdown[] = 'Help'; + $markdown[] = '----'; + $markdown[] = ' '.strip_tags(str_replace("\n", "\n ", $help))."\n"; + } + + return implode("\n", $markdown); + } + /** * Returns an XML representation of the command. * diff --git a/src/Symfony/Component/Console/Command/HelpCommand.php b/src/Symfony/Component/Console/Command/HelpCommand.php index ac4dd3d54d772..fd26bda2c268a 100644 --- a/src/Symfony/Component/Console/Command/HelpCommand.php +++ b/src/Symfony/Component/Console/Command/HelpCommand.php @@ -37,6 +37,7 @@ protected function configure() ->setName('help') ->setDefinition(array( new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('markdown', null, InputOption::VALUE_NONE, 'To output help as Markdown'), new InputOption('xml', null, InputOption::VALUE_NONE, 'To output help as XML'), )) ->setDescription('Displays help for a command') @@ -76,6 +77,8 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($input->getOption('xml')) { $output->writeln($this->command->asXml(), OutputInterface::OUTPUT_RAW); + } elseif ($input->getOption('markdown')) { + $output->writeln($this->command->asMarkdown(), OutputInterface::OUTPUT_RAW); } else { $output->writeln($this->command->asText()); } diff --git a/src/Symfony/Component/Console/Input/InputDefinition.php b/src/Symfony/Component/Console/Input/InputDefinition.php index 69d2f1efe125b..fb596fc3d1cc0 100644 --- a/src/Symfony/Component/Console/Input/InputDefinition.php +++ b/src/Symfony/Component/Console/Input/InputDefinition.php @@ -407,20 +407,7 @@ public function getSynopsis() */ public function asText() { - // find the largest option or argument name - $max = 0; - foreach ($this->getOptions() as $option) { - $nameLength = strlen($option->getName()) + 2; - if ($option->getShortcut()) { - $nameLength += strlen($option->getShortcut()) + 3; - } - - $max = max($max, $nameLength); - } - foreach ($this->getArguments() as $argument) { - $max = max($max, strlen($argument->getName())); - } - ++$max; + $max = $this->getMaximumOptionOrArgumentNameLength() + 1; $text = array(); @@ -470,6 +457,61 @@ public function asText() return implode("\n", $text); } + /** + * Returns Markdown representation of the InputDefinition. + * + * @return string Markdown string representing the InputDefinition + */ + public function asMarkdown() + { + $max = $this->getMaximumOptionOrArgumentNameLength() + 1; + + $markdown = array(); + if ($this->getArguments()) { + $markdown[] = 'Arguments'; + $markdown[] = '---------'; + foreach ($this->getArguments() as $argument) { + if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) { + $default = sprintf(' (default: %s)', $this->formatDefaultValue($argument->getDefault())); + } else { + $default = ''; + } + + $description = str_replace("\n", "\n".str_pad('', $max + 2, ' '), $argument->getDescription()); + + $markdown[] = sprintf(" %-${max}s %s%s", $argument->getName(), $description, $default); + } + $markdown[] = ''; + } + + if ($this->getOptions()) { + $markdown[] = 'Options'; + $markdown[] = '-------'; + foreach ($this->getOptions() as $option) { + if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) { + $default = sprintf(' (default: %s)', $this->formatDefaultValue($option->getDefault())); + } else { + $default = ''; + } + + $multiple = $option->isArray() ? ' (multiple values allowed)' : ''; + $description = str_replace("\n", "\n".str_pad('', $max + 2, ' '), $option->getDescription()); + + $optionMax = $max - strlen($option->getName()) - 2; + $markdown[] = sprintf(" %s %-${optionMax}s%s%s%s", + '--'.$option->getName(), + $option->getShortcut() ? sprintf('(-%s) ', $option->getShortcut()) : '', + $description, + $default, + $multiple + ); + } + $markdown[] = ''; + } + + return implode("\n", $markdown); + } + /** * Returns an XML representation of the InputDefinition. * @@ -524,6 +566,30 @@ public function asXml($asDom = false) return $asDom ? $dom : $dom->saveXml(); } + /** + * Finds the longest option or argument name and returns its length. + * + * @return integer Length of the longest option or argument name + */ + private function getMaximumOptionOrArgumentNameLength() + { + // find the largest option or argument name + $max = 0; + foreach ($this->getOptions() as $option) { + $nameLength = strlen($option->getName()) + 2; + if ($option->getShortcut()) { + $nameLength += strlen($option->getShortcut()) + 3; + } + + $max = max($max, $nameLength); + } + foreach ($this->getArguments() as $argument) { + $max = max($max, strlen($argument->getName())); + } + + return $max; + } + private function formatDefaultValue($default) { if (version_compare(PHP_VERSION, '5.4', '<')) { diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 23edd490b3658..30a41add25ad1 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -377,6 +377,15 @@ public function testAsText() $this->assertStringEqualsFile(self::$fixturesPath.'/application_astext2.txt', $this->normalizeLineBreaks($application->asText('foo')), '->asText() returns a text representation of the application'); } + public function testAsMarkdown() + { + $application = new Application(); + $application->add(new \FooCommand); + $this->ensureStaticCommandHelp($application); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_asmarkdown1.txt', $application->asMarkdown(), '->asMarkdown() returns Markdown representation of the application'); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_asmarkdown2.txt', $application->asMarkdown('foo'), '->asMarkdown() returns Markdown representation of the application'); + } + public function testAsXml() { $application = new Application(); diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php index 166ce7879ea30..11c1e9c44d835 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -274,6 +274,15 @@ public function testAsText() $this->assertStringEqualsFile(self::$fixturesPath.'/command_astext.txt', $command->asText(), '->asText() returns a text representation of the command'); } + public function testAsMarkdown() + { + $command = new \TestCommand(); + $command->setApplication(new Application()); + $tester = new CommandTester($command); + $tester->execute(array('command' => $command->getName())); + $this->assertStringEqualsFile(self::$fixturesPath.'/command_asmarkdown.txt', $command->asMarkdown(), '->asMarkdown() returns Markdown representation of the command'); + } + public function testAsXml() { $command = new \TestCommand(); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_asmarkdown1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_asmarkdown1.txt new file mode 100644 index 0000000000000..8d4d623f58767 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_asmarkdown1.txt @@ -0,0 +1,24 @@ +Console Tool +============ + +Usage +----- + [options] command [arguments] + +Options +------- + --help (-h) Display this help message. + --quiet (-q) Do not output any message. + --verbose (-v) Increase verbosity of messages. + --version (-V) Display this application version. + --ansi Force ANSI output. + --no-ansi Disable ANSI output. + --no-interaction (-n) Do not ask any interactive question. + +Available commands +------------------ + afoobar The foo:bar command + help Displays help for a command + list Lists commands + + foo:bar The foo:bar command \ No newline at end of file diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_asmarkdown2.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_asmarkdown2.txt new file mode 100644 index 0000000000000..7a958c822276d --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_asmarkdown2.txt @@ -0,0 +1,20 @@ +Console Tool +============ + +Usage +----- + [options] command [arguments] + +Options +------- + --help (-h) Display this help message. + --quiet (-q) Do not output any message. + --verbose (-v) Increase verbosity of messages. + --version (-V) Display this application version. + --ansi Force ANSI output. + --no-ansi Disable ANSI output. + --no-interaction (-n) Do not ask any interactive question. + +Available commands for the "foo" namespace +------------------------------------------ + foo:bar The foo:bar command \ No newline at end of file diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_asxml1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_asxml1.txt index ed168f25cbcda..fb9f10785c7a6 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_asxml1.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_asxml1.txt @@ -2,7 +2,7 @@ - help [--xml] [command_name] + help [--markdown] [--xml] [command_name] Displays help for a command The <info>help</info> command displays help for a given command: @@ -23,6 +23,9 @@ + diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_run2.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_run2.txt index 6831afdd128c1..7fc9438bc0600 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_run2.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_run2.txt @@ -1,11 +1,12 @@ Usage: - help [--xml] [command_name] + help [--markdown] [--xml] [command_name] Arguments: command The command to execute command_name The command name (default: "help") Options: + --markdown To output help as Markdown --xml To output help as XML --help (-h) Display this help message. --quiet (-q) Do not output any message. diff --git a/src/Symfony/Component/Console/Tests/Fixtures/command_asmarkdown.txt b/src/Symfony/Component/Console/Tests/Fixtures/command_asmarkdown.txt new file mode 100644 index 0000000000000..29ca05c1916c0 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/command_asmarkdown.txt @@ -0,0 +1,30 @@ +namespace:name +============== + +description + +Usage +----- + namespace:name + +Aliases +------- +* name + +Arguments +--------- + command The command to execute + +Options +------- + --help (-h) Display this help message. + --quiet (-q) Do not output any message. + --verbose (-v) Increase verbosity of messages. + --version (-V) Display this application version. + --ansi Force ANSI output. + --no-ansi Disable ANSI output. + --no-interaction (-n) Do not ask any interactive question. + +Help +---- + help diff --git a/src/Symfony/Component/Console/Tests/Fixtures/definition_asmarkdown.txt b/src/Symfony/Component/Console/Tests/Fixtures/definition_asmarkdown.txt new file mode 100644 index 0000000000000..81d5b0dc69515 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/definition_asmarkdown.txt @@ -0,0 +1,13 @@ +Arguments +--------- + foo The foo argument + baz The baz argument (default: true) + bar The bar argument (default: ["http://foo.com/"]) + +Options +------- + --foo (-f) The foo option + --baz The baz option (default: false) + --bar (-b) The bar option (default: "bar") + --qux The qux option (default: ["http://foo.com/","bar"]) (multiple values allowed) + --qux2 The qux2 option (default: {"foo":"bar"}) (multiple values allowed) diff --git a/src/Symfony/Component/Console/Tests/Input/InputDefinitionTest.php b/src/Symfony/Component/Console/Tests/Input/InputDefinitionTest.php index c322ed42bca68..61cd3e2cdc8ca 100644 --- a/src/Symfony/Component/Console/Tests/Input/InputDefinitionTest.php +++ b/src/Symfony/Component/Console/Tests/Input/InputDefinitionTest.php @@ -332,6 +332,21 @@ public function testAsText() $this->assertStringEqualsFile(self::$fixtures.'/definition_astext.txt', $definition->asText(), '->asText() returns a textual representation of the InputDefinition'); } + public function testAsMarkdown() + { + $definition = new InputDefinition(array( + new InputArgument('foo', InputArgument::OPTIONAL, 'The foo argument'), + new InputArgument('baz', InputArgument::OPTIONAL, 'The baz argument', true), + new InputArgument('bar', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'The bar argument', array('http://foo.com/')), + new InputOption('foo', 'f', InputOption::VALUE_REQUIRED, 'The foo option'), + new InputOption('baz', null, InputOption::VALUE_OPTIONAL, 'The baz option', false), + new InputOption('bar', 'b', InputOption::VALUE_OPTIONAL, 'The bar option', 'bar'), + new InputOption('qux', '', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The qux option', array('http://foo.com/', 'bar')), + new InputOption('qux2', '', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The qux2 option', array('foo' => 'bar')), + )); + $this->assertStringEqualsFile(self::$fixtures.'/definition_asmarkdown.txt', $definition->asMarkdown(), '->asMarkdown() returns Markdown representation of the InputDefinition'); + } + public function testAsXml() { $definition = new InputDefinition(array(