feature #25732 [Console] Add option to automatically run suggested command if there is only 1 alternative (pierredup)

This PR was squashed before being merged into the 4.1-dev branch (closes #25732).

Discussion
----------

[Console] Add option to automatically run suggested command if there is only 1 alternative

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets |
| License       | MIT
| Doc PR        |

When mistyping a console command, you get an error giving suggested commands.
If there is only 1 alternative suggestion, this PR will give you the option to run that command instead. This makes it easier to run the correct command without having to re-type/copy-paste/update the previous run command

![console](https://user-images.githubusercontent.com/144858/34724377-4b46c726-f556-11e7-94a3-a9d7c9d75e74.gif)

Commits
-------

83d52f02f9 [Console] Add option to automatically run suggested command if there is only 1 alternative
This commit is contained in:
Fabien Potencier 2018-02-22 07:25:56 +01:00
commit 7b8934baba
5 changed files with 147 additions and 14 deletions

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Console;
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\ConsoleErrorEvent;
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 @@ class Application
// 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 @@ class Application
*
* @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 @@ class Application
$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);

View File

@ -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
-----

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <pdples@gmail.com>
*/
class NamespaceNotFoundException extends CommandNotFoundException
{
}

View File

@ -16,6 +16,7 @@ use Symfony\Component\Console\Application;
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 @@ class ApplicationTest extends TestCase
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 @@ class ApplicationTest extends TestCase
$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 @@ class ApplicationTest extends TestCase
}
/**
* @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 @@ class ApplicationTest extends TestCase
$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(<<<OUTPUT
Command "foos" is not defined.
Do you want to run "foo" instead? (yes/no) [no]:
>
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
Command "foos" is not defined.
Do you want to run "foo" instead? (yes/no) [no]:
>
OUTPUT
, $tester->getDisplay(true));
}
public function provideInvalidCommandNamesSingle()
{
return array(
@ -574,7 +638,8 @@ class ApplicationTest extends TestCase
$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());

View File

@ -0,0 +1,21 @@
<?php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class FooWithoutAliasCommand extends Command
{
protected function configure()
{
$this
->setName('foo')
->setDescription('The foo command')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('called');
}
}