Skip to content

Commit

Permalink
feature #25732 [Console] Add option to automatically run suggested co…
Browse files Browse the repository at this point in the history
…mmand 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
-------

83d52f0 [Console] Add option to automatically run suggested command if there is only 1 alternative
  • Loading branch information
fabpot committed Feb 22, 2018
2 parents cf045c0 + 83d52f0 commit 7b8934b
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 14 deletions.
41 changes: 31 additions & 10 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions 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
-----

Expand Down
@@ -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
{
}
73 changes: 69 additions & 4 deletions src/Symfony/Component/Console/Tests/ApplicationTest.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
}
Expand Down Expand Up @@ -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');
Expand All @@ -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()
Expand Down Expand Up @@ -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(<<<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(
Expand Down Expand Up @@ -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());
Expand Down
@@ -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');
}
}

0 comments on commit 7b8934b

Please sign in to comment.