Skip to content

Commit

Permalink
feature #20869 [Console] Improve UX on not found namespace/command (S…
Browse files Browse the repository at this point in the history
…eldaek)

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
  • Loading branch information
fabpot committed Dec 13, 2016
2 parents 795a240 + aae5fb1 commit 462a02b
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 10 deletions.
20 changes: 16 additions & 4 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -30,6 +30,7 @@
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;
Expand Down Expand Up @@ -503,7 +504,7 @@ public function findNamespace($namespace)

$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);
Expand Down Expand Up @@ -559,9 +560,20 @@ public function find($name)

$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();

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 (%s).', $name, $suggestions), array_values($commands));
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));
Expand Down Expand Up @@ -944,7 +956,7 @@ protected function getDefaultHelperSet()
*/
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);
}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/Symfony/Component/Console/Helper/Helper.php
Expand Up @@ -58,6 +58,24 @@ public static function strlen($string)
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(
Expand Down
24 changes: 18 additions & 6 deletions src/Symfony/Component/Console/Tests/ApplicationTest.php
Expand Up @@ -28,6 +28,7 @@
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
Expand Down Expand Up @@ -211,16 +212,15 @@ public function testFindNamespaceWithSubnamespaces()
$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');
}

Expand Down Expand Up @@ -279,8 +279,20 @@ public function provideAmbiguousAbbreviations()
{
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',
),
);
}

Expand Down

0 comments on commit 462a02b

Please sign in to comment.