From aae5fb15ec60ecc8f7bfd577f4cd2348170b6d9b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 11 Dec 2016 14:18:07 +0100 Subject: [PATCH] Improve UX on not found namespace/command --- src/Symfony/Component/Console/Application.php | 20 ++++++++++++---- .../Component/Console/Helper/Helper.php | 18 ++++++++++++++ .../Console/Tests/ApplicationTest.php | 24 ++++++++++++++----- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index f2746c896c149..3b4566b0e5081 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -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; @@ -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); @@ -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)); @@ -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); } /** diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 43f9a169461c1..5bc52a6ebcce4 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -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( diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index b8f99f26fb3d1..b0582bb591462 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -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 @@ -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'); } @@ -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', + ), ); }