diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 108c1ab20b839..7b885efb02cc8 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -11,7 +11,12 @@ namespace Symfony\Component\Console; +use Symfony\Component\Console\Exception\AmbiguousCommandException; +use Symfony\Component\Console\Exception\AmbiguousNamespaceException; use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\UnknownCommandException; +use Symfony\Component\Console\Exception\UnknownNamespaceException; use Symfony\Component\Console\Helper\DebugFormatterHelper; use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Helper\QuestionHelper; @@ -36,6 +41,7 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -185,7 +191,30 @@ public function doRun(InputInterface $input, OutputInterface $output) } // the command name MUST be the first element of the input - $command = $this->find($name); + do { + try { + $command = $this->find($name); + } catch (CommandNotFoundException $e) { + $alternatives = $e->getAlternatives(); + if (0 === count($alternatives) || !$input->isInteractive() || !$this->getHelperSet()->has('question')) { + throw $e; + } + + $helper = $this->getHelperSet()->get('question'); + $question = new ChoiceQuestion(strtok($e->getMessage(), "\n").' Please select one of these suggested commands:', $alternatives); + $question->setMaxAttempts(1); + + try { + $name = $helper->ask($input, $output, $question); + } catch (InvalidArgumentException $ex) { + throw $e; + } + + if (null === $name) { + throw $e; + } + } + } while (!isset($command)); $this->runningCommand = $command; $exitCode = $this->doRunCommand($command, $input, $output); @@ -477,7 +506,8 @@ public function getNamespaces() * * @return string A registered namespace * - * @throws CommandNotFoundException When namespace is incorrect or ambiguous + * @throws UnknownNamespaceException When namespace is incorrect + * @throws AmbiguousNamespaceException When namespace is ambiguous */ public function findNamespace($namespace) { @@ -486,24 +516,12 @@ public function findNamespace($namespace) $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); if (empty($namespaces)) { - $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); - - if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { - if (1 == count($alternatives)) { - $message .= "\n\nDid you mean this?\n "; - } else { - $message .= "\n\nDid you mean one of these?\n "; - } - - $message .= implode("\n ", $alternatives); - } - - throw new CommandNotFoundException($message, $alternatives); + throw new UnknownNamespaceException($namespace, $this->findAlternatives($namespace, $allNamespaces, array())); } $exact = in_array($namespace, $namespaces, true); if (count($namespaces) > 1 && !$exact) { - throw new CommandNotFoundException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); + throw new AmbiguousNamespaceException($namespace, $namespaces); } return $exact ? $namespace : reset($namespaces); @@ -519,7 +537,8 @@ public function findNamespace($namespace) * * @return Command A Command instance * - * @throws CommandNotFoundException When command name is incorrect or ambiguous + * @throws UnknownCommandException When command name is incorrect + * @throws AmbiguousCommandException When command name is ambiguous */ public function find($name) { @@ -533,18 +552,7 @@ public function find($name) $this->findNamespace(substr($name, 0, $pos)); } - $message = sprintf('Command "%s" is not defined.', $name); - - if ($alternatives = $this->findAlternatives($name, $allCommands)) { - if (1 == count($alternatives)) { - $message .= "\n\nDid you mean this?\n "; - } else { - $message .= "\n\nDid you mean one of these?\n "; - } - $message .= implode("\n ", $alternatives); - } - - throw new CommandNotFoundException($message, $alternatives); + throw new UnknownCommandException($name, $this->findAlternatives($name, $allCommands, array())); } // filter out aliases for commands which are already on the list @@ -559,9 +567,7 @@ public function find($name) $exact = in_array($name, $commands, true); if (count($commands) > 1 && !$exact) { - $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); - - throw new CommandNotFoundException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions), array_values($commands)); + throw new AmbiguousCommandException($name, array_values($commands)); } return $this->get($exact ? $name : reset($commands)); diff --git a/src/Symfony/Component/Console/Exception/AmbiguousCommandException.php b/src/Symfony/Component/Console/Exception/AmbiguousCommandException.php new file mode 100644 index 0000000000000..0fa8fb514fd05 --- /dev/null +++ b/src/Symfony/Component/Console/Exception/AmbiguousCommandException.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Martin Hasoň + */ +class AmbiguousCommandException extends CommandNotFoundException +{ + private $command; + + public function __construct($command, $alternatives = array(), $code = null, $previous = null) + { + $this->command = $command; + $message = sprintf('Command "%s" is ambiguous (%s).', $command, $this->getAbbreviationSuggestions($alternatives)); + + parent::__construct($message, $alternatives, $code, $previous); + } + + /** + * @return string + */ + public function getCommand() + { + return $this->command; + } +} diff --git a/src/Symfony/Component/Console/Exception/AmbiguousNamespaceException.php b/src/Symfony/Component/Console/Exception/AmbiguousNamespaceException.php new file mode 100644 index 0000000000000..a54dd799a2990 --- /dev/null +++ b/src/Symfony/Component/Console/Exception/AmbiguousNamespaceException.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Martin Hasoň + */ +class AmbiguousNamespaceException extends CommandNotFoundException +{ + private $namespace; + + public function __construct($namespace, $alternatives = array(), $code = null, $previous = null) + { + $this->command = $namespace; + + $message = sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions($alternatives)); + + parent::__construct($message, $alternatives, $code, $previous); + } + + /** + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } +} diff --git a/src/Symfony/Component/Console/Exception/CommandNotFoundException.php b/src/Symfony/Component/Console/Exception/CommandNotFoundException.php index 54f1a5b0ce848..65cc3fc1de24a 100644 --- a/src/Symfony/Component/Console/Exception/CommandNotFoundException.php +++ b/src/Symfony/Component/Console/Exception/CommandNotFoundException.php @@ -40,4 +40,16 @@ public function getAlternatives() { return $this->alternatives; } + + /** + * Returns abbreviated suggestions in string format. + * + * @param array $abbrevs Abbreviated suggestions to convert + * + * @return string A formatted string of abbreviated suggestions + */ + protected function getAbbreviationSuggestions($abbrevs) + { + return sprintf('%s, %s%s', reset($abbrevs), next($abbrevs), count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); + } } diff --git a/src/Symfony/Component/Console/Exception/UnknownCommandException.php b/src/Symfony/Component/Console/Exception/UnknownCommandException.php new file mode 100644 index 0000000000000..a095be0791009 --- /dev/null +++ b/src/Symfony/Component/Console/Exception/UnknownCommandException.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Martin Hasoň + */ +class UnknownCommandException extends CommandNotFoundException +{ + private $command; + + public function __construct($command, $alternatives = array(), $code = null, $previous = null) + { + $this->command = $command; + + $message = sprintf('Command "%s" is not defined.', $command); + + if ($alternatives) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + parent::__construct($message, $alternatives, $code, $previous); + } + + /** + * @return string + */ + public function getCommand() + { + return $this->command; + } +} diff --git a/src/Symfony/Component/Console/Exception/UnknownNamespaceException.php b/src/Symfony/Component/Console/Exception/UnknownNamespaceException.php new file mode 100644 index 0000000000000..11b799ca86f8d --- /dev/null +++ b/src/Symfony/Component/Console/Exception/UnknownNamespaceException.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Martin Hasoň + */ +class UnknownNamespaceException extends CommandNotFoundException +{ + private $namespace; + + public function __construct($namespace, $alternatives = array(), $code = null, $previous = null) + { + $this->namespace = $namespace; + + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + parent::__construct($message, $alternatives, $code, $previous); + } + + /** + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } +} diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 9dcdbb8f37f65..6c126199565d4 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -405,6 +405,33 @@ public function testFindAlternativeCommands() } } + public function testFindAlternativeCommandsWithQuestion() + { + $application = new Application(); + $application->setAutoExit(false); + putenv('COLUMNS=120'); + putenv('SHELL_INTERACTIVE=1'); + $application->add(new \FooCommand()); + $application->add(new \Foo1Command()); + $application->add(new \Foo2Command()); + + $input = new ArrayInput(array('command' => 'foo')); + + $inputStream = fopen('php://memory', 'r+', false); + fwrite($inputStream, "1\n"); + rewind($inputStream); + $input->setStream($inputStream); + + $output = new StreamOutput(fopen('php://memory', 'w', false), StreamOutput::VERBOSITY_NORMAL, false); + + $application->run($input, $output); + + rewind($output->getStream()); + $display = str_replace(PHP_EOL, "\n", stream_get_contents($output->getStream())); + + $this->assertStringEqualsFile(self::$fixturesPath.'/application_unknown_command_question.txt', $display); + } + public function testFindAlternativeCommandsWithAnAlias() { $fooCommand = new \FooCommand(); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception1.txt index 919cec4214a97..0189b26ef2a8b 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception1.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception1.txt @@ -1,6 +1,6 @@ - - [Symfony\Component\Console\Exception\CommandNotFoundException] - Command "foo" is not defined. - + + [Symfony\Component\Console\Exception\UnknownCommandException] + Command "foo" is not defined. + diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception4.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception4.txt index cb080e9cb53b8..c4e139be27a49 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception4.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception4.txt @@ -1,7 +1,7 @@ - - [Symfony\Component\Console\Exception\CommandNotFoundException] - Command "foo" is not define - d. - + + [Symfony\Component\Console\Exception\UnknownCommandException] + Command "foo" is not define + d. + diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_unknown_command_question.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_unknown_command_question.txt new file mode 100644 index 0000000000000..2d88cbe6e73db --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_unknown_command_question.txt @@ -0,0 +1,10 @@ +Command "foo" is not defined. Please select one of these suggested commands: + [0] foo:bar1 + [1] foo:bar + [2] foo1:bar + [3] afoobar + [4] afoobar1 + [5] afoobar2 + > 1 +interact called +called