diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 00e941f13238..4020bd527fc6 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Exception\NamespaceNotFoundException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\DebugFormatterHelper; use Symfony\Component\Console\Helper\Helper; @@ -39,6 +40,7 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -223,18 +225,37 @@ public function doRun(InputInterface $input, OutputInterface $output) // the command name MUST be the first element of the input $command = $this->find($name); } catch (\Throwable $e) { - if (null !== $this->dispatcher) { - $event = new ConsoleErrorEvent($input, $output, $e); - $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event); + if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event); - if (0 === $event->getExitCode()) { - return 0; + if (0 === $event->getExitCode()) { + return 0; + } + + $e = $event->getError(); } - $e = $event->getError(); + throw $e; } - throw $e; + $alternative = $alternatives[0]; + + $style = new SymfonyStyle($input, $output); + $style->block(sprintf("\nCommand \"%s\" is not defined.\n", $name), null, 'error'); + if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event); + + return $event->getExitCode(); + } + + return 1; + } + + $command = $this->find($alternative); } $this->runningCommand = $command; @@ -533,7 +554,7 @@ public function getNamespaces() * * @return string A registered namespace * - * @throws CommandNotFoundException When namespace is incorrect or ambiguous + * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous */ public function findNamespace($namespace) { @@ -554,12 +575,12 @@ public function findNamespace($namespace) $message .= implode("\n ", $alternatives); } - throw new CommandNotFoundException($message, $alternatives); + throw new NamespaceNotFoundException($message, $alternatives); } $exact = in_array($namespace, $namespaces, true); if (count($namespaces) > 1 && !$exact) { - throw new CommandNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); + throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); } return $exact ? $namespace : reset($namespaces); diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 2437748ae56e..887ac2f83a8d 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.1.0 +----- + + * added option to run suggested command if command is not found and only 1 alternative is available + 4.0.0 ----- diff --git a/src/Symfony/Component/Console/Exception/NamespaceNotFoundException.php b/src/Symfony/Component/Console/Exception/NamespaceNotFoundException.php new file mode 100644 index 000000000000..dd16e45086a7 --- /dev/null +++ b/src/Symfony/Component/Console/Exception/NamespaceNotFoundException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents an incorrect namespace typed in the console. + * + * @author Pierre du Plessis + */ +class NamespaceNotFoundException extends CommandNotFoundException +{ +} diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index e9432c522d6f..2c36acb651a6 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; +use Symfony\Component\Console\Exception\NamespaceNotFoundException; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Input\ArgvInput; @@ -56,6 +57,7 @@ public static function setUpBeforeClass() require_once self::$fixturesPath.'/BarBucCommand.php'; require_once self::$fixturesPath.'/FooSubnamespaced1Command.php'; require_once self::$fixturesPath.'/FooSubnamespaced2Command.php'; + require_once self::$fixturesPath.'/FooWithoutAliasCommand.php'; require_once self::$fixturesPath.'/TestTiti.php'; require_once self::$fixturesPath.'/TestToto.php'; } @@ -275,10 +277,10 @@ public function testFindAmbiguousNamespace() $expectedMsg = "The namespace \"f\" is ambiguous.\nDid you mean one of these?\n foo\n foo1"; if (method_exists($this, 'expectException')) { - $this->expectException(CommandNotFoundException::class); + $this->expectException(NamespaceNotFoundException::class); $this->expectExceptionMessage($expectedMsg); } else { - $this->setExpectedException(CommandNotFoundException::class, $expectedMsg); + $this->setExpectedException(NamespaceNotFoundException::class, $expectedMsg); } $application->findNamespace('f'); @@ -293,7 +295,7 @@ public function testFindNonAmbiguous() } /** - * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException + * @expectedException \Symfony\Component\Console\Exception\NamespaceNotFoundException * @expectedExceptionMessage There are no commands defined in the "bar" namespace. */ public function testFindInvalidNamespace() @@ -457,6 +459,68 @@ public function testFindAlternativeExceptionMessageSingle($name) $application->find($name); } + public function testDontRunAlternativeNamespaceName() + { + $application = new Application(); + $application->add(new \Foo1Command()); + $application->setAutoExit(false); + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'foos:bar1'), array('decorated' => false)); + $this->assertSame(' + + There are no commands defined in the "foos" namespace. + + Did you mean this? + foo + + +', $tester->getDisplay(true)); + } + + public function testCanRunAlternativeCommandName() + { + $application = new Application(); + $application->add(new \FooWithoutAliasCommand()); + $application->setAutoExit(false); + $tester = new ApplicationTester($application); + $tester->setInputs(array('y')); + $tester->run(array('command' => 'foos'), array('decorated' => false)); + $this->assertSame(<< +called + +OUTPUT +, $tester->getDisplay(true)); + } + + public function testDontRunAlternativeCommandName() + { + $application = new Application(); + $application->add(new \FooWithoutAliasCommand()); + $application->setAutoExit(false); + $tester = new ApplicationTester($application); + $tester->setInputs(array('n')); + $exitCode = $tester->run(array('command' => 'foos'), array('decorated' => false)); + $this->assertSame(1, $exitCode); + $this->assertSame(<< + +OUTPUT + , $tester->getDisplay(true)); + } + public function provideInvalidCommandNamesSingle() { return array( @@ -574,7 +638,8 @@ public function testFindAlternativeNamespace() $application->find('foo2:command'); $this->fail('->find() throws a CommandNotFoundException if namespace does not exist'); } catch (\Exception $e) { - $this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, '->find() throws a CommandNotFoundException if namespace does not exist'); + $this->assertInstanceOf('Symfony\Component\Console\Exception\NamespaceNotFoundException', $e, '->find() throws a NamespaceNotFoundException if namespace does not exist'); + $this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, 'NamespaceNotFoundException extends from CommandNotFoundException'); $this->assertCount(3, $e->getAlternatives()); $this->assertContains('foo', $e->getAlternatives()); $this->assertContains('foo1', $e->getAlternatives()); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/FooWithoutAliasCommand.php b/src/Symfony/Component/Console/Tests/Fixtures/FooWithoutAliasCommand.php new file mode 100644 index 000000000000..e301cc5f5b3a --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/FooWithoutAliasCommand.php @@ -0,0 +1,21 @@ +setName('foo') + ->setDescription('The foo command') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('called'); + } +}