feature #20869 [Console] Improve UX on not found namespace/command (Seldaek)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Console] Improve UX on not found namespace/command

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

This improves the DX/UX when you don't remember what a command is called.. Traditionally you get this message saying "command x is ambiguous (Y, Z or 6 more)" and if the one you are looking for is in the 6 more you are out of luck. You then have to run the console without arg again, get 50 commands displayed, then have to scroll up to find which one it is you meant.

With this patch you get all suggestions always, even with description, so you can make an informed decision right away. See before/after on the screenshot below.

![image](https://cloud.githubusercontent.com/assets/183678/21080350/c3d446ea-bfac-11e6-934b-ba3d7c3dd34d.png)

Commits
-------

aae5fb1 Improve UX on not found namespace/command
This commit is contained in:
Fabien Potencier 2016-12-13 09:06:17 +01:00
commit 462a02b3c6
3 changed files with 52 additions and 10 deletions

View File

@ -30,6 +30,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Command\ListCommand;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
@ -503,7 +504,7 @@ class Application
$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 CommandNotFoundException(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);
@ -559,9 +560,20 @@ class Application
$exact = in_array($name, $commands, true);
if (count($commands) > 1 && !$exact) {
$suggestions = $this->getAbbreviationSuggestions(array_values($commands));
$usableWidth = $this->terminal->getWidth() - 10;
$abbrevs = array_values($commands);
$maxLen = 0;
foreach ($abbrevs as $abbrev) {
$maxLen = max(Helper::strlen($abbrev), $maxLen);
}
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) {
$abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
throw new CommandNotFoundException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions), array_values($commands));
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
}, array_values($commands));
$suggestions = $this->getAbbreviationSuggestions($abbrevs);
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
}
return $this->get($exact ? $name : reset($commands));
@ -944,7 +956,7 @@ class Application
*/
private function getAbbreviationSuggestions($abbrevs)
{
return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
return ' '.implode("\n ", $abbrevs);
}
/**

View File

@ -58,6 +58,24 @@ abstract class Helper implements HelperInterface
return mb_strwidth($string, $encoding);
}
/**
* Returns the subset of a string, using mb_substr if it is available.
*
* @param string $string String to subset
* @param int $from Start offset
* @param int|null $length Length to read
*
* @return string The string subset
*/
public static function substr($string, $from, $length = null)
{
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return substr($string);
}
return mb_substr($string, $from, $length, $encoding);
}
public static function formatTime($secs)
{
static $timeFormats = array(

View File

@ -28,6 +28,7 @@ use Symfony\Component\Console\Tester\ApplicationTester;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\EventDispatcher\EventDispatcher;
class ApplicationTest extends \PHPUnit_Framework_TestCase
@ -211,16 +212,15 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns commands even if the commands are only contained in subnamespaces');
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
* @expectedExceptionMessage The namespace "f" is ambiguous (foo, foo1).
*/
public function testFindAmbiguousNamespace()
{
$application = new Application();
$application->add(new \BarBucCommand());
$application->add(new \FooCommand());
$application->add(new \Foo2Command());
$expectedMsg = "The namespace \"f\" is ambiguous.\nDid you mean one of these?\n foo\n foo1";
$this->setExpectedException(CommandNotFoundException::class, $expectedMsg);
$application->findNamespace('f');
}
@ -279,8 +279,20 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
{
return array(
array('f', 'Command "f" is not defined.'),
array('a', 'Command "a" is ambiguous (afoobar, afoobar1 and 1 more).'),
array('foo:b', 'Command "foo:b" is ambiguous (foo:bar, foo:bar1 and 1 more).'),
array(
'a',
"Command \"a\" is ambiguous.\nDid you mean one of these?\n".
" afoobar The foo:bar command\n".
" afoobar1 The foo:bar1 command\n".
' afoobar2 The foo1:bar command',
),
array(
'foo:b',
"Command \"foo:b\" is ambiguous.\nDid you mean one of these?\n".
" foo:bar The foo:bar command\n".
" foo:bar1 The foo:bar1 command\n".
' foo1:bar The foo1:bar command',
),
);
}