From dd0d97e643d722c0a4d7df42116ec0076a47d6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Sat, 11 Feb 2012 02:58:39 +0100 Subject: [PATCH 1/5] [Console] Added suggest on bad command name --- src/Symfony/Component/Console/Application.php | 32 ++++++++++++++- .../Component/Console/ApplicationTest.php | 41 ++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 219111737e..ce6e2ce8db 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -557,7 +557,14 @@ class Application $abbrevs = static::getAbbreviations(array_unique($aliases)); if (!isset($abbrevs[$searchName])) { - throw new \InvalidArgumentException(sprintf('Command "%s" is not defined.', $name)); + $message = sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternativeCommands($searchName)) { + $message .= PHP_EOL.'Did you mean one of these?'.PHP_EOL.' '; + $message .= implode(PHP_EOL.' ', $alternatives); + } + + throw new \InvalidArgumentException($message); } if (count($abbrevs[$searchName]) > 1) { @@ -915,4 +922,27 @@ class Application return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); } + + /** + * Finds alternative commands of $name + * + * @param string $name The full name of the command + * @return array A sorted array of similar commands + */ + private function findAlternativeCommands($name) + { + $alternatives = array(); + + foreach ($this->commands as $command) { + $commandName = $command->getName(); + $lev = levenshtein($name, $commandName); + if ($lev <= strlen($name) / 3 || false !== strpos($commandName, $name)) { + $alternatives[$commandName] = $lev; + } + } + + asort($alternatives); + + return array_keys($alternatives); + } } diff --git a/tests/Symfony/Tests/Component/Console/ApplicationTest.php b/tests/Symfony/Tests/Component/Console/ApplicationTest.php index b24add6502..5a8186f57c 100644 --- a/tests/Symfony/Tests/Component/Console/ApplicationTest.php +++ b/tests/Symfony/Tests/Component/Console/ApplicationTest.php @@ -183,7 +183,7 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase $this->fail('->find() throws an \InvalidArgumentException if the abbreviation is ambiguous for a namespace'); } catch (\Exception $e) { $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if the abbreviation is ambiguous for a namespace'); - $this->assertEquals('Command "f" is not defined.', $e->getMessage(), '->find() throws an \InvalidArgumentException if the abbreviation is ambiguous for a namespace'); + $this->assertRegExp('/Command "f" is not defined./', $e->getMessage(), '->find() throws an \InvalidArgumentException if the abbreviation is ambiguous for a namespace'); } try { @@ -203,6 +203,45 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase } } + public function testFindAlternativeCommands() + { + $application = new Application(); + + $application->add(new \FooCommand()); + $application->add(new \Foo1Command()); + $application->add(new \Foo2Command()); + + try { + $application->find($commandName = 'Unknow command'); + $this->fail('->find() throws an \InvalidArgumentException if command does not exist'); + } catch (\Exception $e) { + $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if command does not exist'); + $this->assertEquals(sprintf('Command "%s" is not defined.', $commandName), $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, without alternatives'); + } + + try { + $application->find($commandName = 'foo'); + $this->fail('->find() throws an \InvalidArgumentException if command does not exist'); + } catch (\Exception $e) { + $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if command does not exist'); + $this->assertRegExp(sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternatives'); + $this->assertRegExp('/foo:bar/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternative : "foo:bar"'); + $this->assertRegExp('/foo1:bar/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternative : "foo1:bar"'); + $this->assertRegExp('/foo:bar1/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternative : "foo:bar1"'); + } + + // Test if "foo1" command throw an "\InvalidArgumentException" and does not contain + // "foo:bar" as alternative because "foo1" is too far from "foo:bar" + try { + $application->find($commandName = 'foo1'); + $this->fail('->find() throws an \InvalidArgumentException if command does not exist'); + } catch (\Exception $e) { + $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if command does not exist'); + $this->assertRegExp(sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternatives'); + $this->assertFalse(strpos($e->getMessage(), 'foo:bar'), '->find() throws an \InvalidArgumentException if command does not exist, without "foo:bar" alternative'); + } + } + public function testSetCatchExceptions() { $application = new Application(); From 117359a196c18a1280344549633d180a1d298303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Sat, 11 Feb 2012 16:33:06 +0100 Subject: [PATCH 2/5] [Console] fixed CS according to PR comment --- src/Symfony/Component/Console/Application.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index ce6e2ce8db..9f21b934dc 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -560,8 +560,8 @@ class Application $message = sprintf('Command "%s" is not defined.', $name); if ($alternatives = $this->findAlternativeCommands($searchName)) { - $message .= PHP_EOL.'Did you mean one of these?'.PHP_EOL.' '; - $message .= implode(PHP_EOL.' ', $alternatives); + $message .= "\nDid you mean one of these?\n "; + $message .= implode("\n ", $alternatives); } throw new \InvalidArgumentException($message); From c6203bcffaa8ecd1b34d1c881dcdc62c14d7198c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Sat, 11 Feb 2012 20:38:08 +0100 Subject: [PATCH 3/5] [Console] Added namespace suggest on bad namespace name --- src/Symfony/Component/Console/Application.php | 54 ++++++++++++++++--- .../Component/Console/ApplicationTest.php | 29 ++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 9f21b934dc..03c7d18a6c 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -489,7 +489,14 @@ class Application $abbrevs = static::getAbbreviations(array_unique(array_values(array_filter(array_map(function ($p) use ($i) { return isset($p[$i]) ? $p[$i] : ''; }, $allNamespaces))))); if (!isset($abbrevs[$part])) { - throw new \InvalidArgumentException(sprintf('There are no commands defined in the "%s" namespace.', $namespace)); + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternativeNamespace($namespace)) { + $message .= "\n\nDid you mean one of these?\n "; + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); } if (count($abbrevs[$part]) > 1) { @@ -560,7 +567,7 @@ class Application $message = sprintf('Command "%s" is not defined.', $name); if ($alternatives = $this->findAlternativeCommands($searchName)) { - $message .= "\nDid you mean one of these?\n "; + $message .= "\n\nDid you mean one of these?\n "; $message .= implode("\n ", $alternatives); } @@ -927,17 +934,50 @@ class Application * Finds alternative commands of $name * * @param string $name The full name of the command + * * @return array A sorted array of similar commands */ private function findAlternativeCommands($name) { + $getNameCallback = function($command) { + return $command->getName(); + }; + + return $this->findAlternatives($name, $this->commands, $getNameCallback); + } + + /** + * Finds alternative namespace of $name + * + * @param string $name The full name of the namespace + * + * @return array A sorted array of similar namespace + */ + private function findAlternativeNamespace($name) + { + return $this->findAlternatives($name, $this->getNamespaces()); + } + + /** + * Finds alternative of $name among $collection + * + * @param string $name The string + * @param array|Traversable $collection The collection + * @param Closure|string|array $callback The callable to transform item before comparison + * + * @return array A sorted array of similar string + */ + private function findAlternatives($name, $collection, $callback = null) { $alternatives = array(); - foreach ($this->commands as $command) { - $commandName = $command->getName(); - $lev = levenshtein($name, $commandName); - if ($lev <= strlen($name) / 3 || false !== strpos($commandName, $name)) { - $alternatives[$commandName] = $lev; + foreach ($collection as $item) { + if (null !== $callback) { + $item = call_user_func($callback, $item); + } + + $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = $lev; } } diff --git a/tests/Symfony/Tests/Component/Console/ApplicationTest.php b/tests/Symfony/Tests/Component/Console/ApplicationTest.php index 5a8186f57c..4e4e2fa59f 100644 --- a/tests/Symfony/Tests/Component/Console/ApplicationTest.php +++ b/tests/Symfony/Tests/Component/Console/ApplicationTest.php @@ -242,6 +242,35 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase } } + public function testFindAlternativeNamespace() + { + $application = new Application(); + + $application->add(new \FooCommand()); + $application->add(new \Foo1Command()); + $application->add(new \Foo2Command()); + $application->add(new \foo3Command()); + + try { + $application->find('Unknow-namespace:Unknow-command'); + $this->fail('->find() throws an \InvalidArgumentException if namespace does not exist'); + } catch (\Exception $e) { + $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if namespace does not exist'); + $this->assertEquals('There are no commands defined in the "Unknow-namespace" namespace.', $e->getMessage(), '->find() throws an \InvalidArgumentException if namespace does not exist, without alternatives'); + } + + try { + $application->find('foo2:command'); + $this->fail('->find() throws an \InvalidArgumentException if namespace does not exist'); + } catch (\Exception $e) { + $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if namespace does not exist'); + $this->assertRegExp('/There are no commands defined in the "foo2" namespace./', $e->getMessage(), '->find() throws an \InvalidArgumentException if namespace does not exist, with alternative'); + $this->assertRegExp('/foo/', $e->getMessage(), '->find() throws an \InvalidArgumentException if namespace does not exist, with alternative : "foo"'); + $this->assertRegExp('/foo1/', $e->getMessage(), '->find() throws an \InvalidArgumentException if namespace does not exist, with alternative : "foo1"'); + $this->assertRegExp('/foo3/', $e->getMessage(), '->find() throws an \InvalidArgumentException if namespace does not exist, with alternative : "foo3"'); + } + } + public function testSetCatchExceptions() { $application = new Application(); From 8abf50639a245bd4d71b772541c5e515f1a287aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Sun, 12 Feb 2012 17:53:56 +0100 Subject: [PATCH 4/5] [Console] Added abbreviation into search for bad command / namespace --- src/Symfony/Component/Console/Application.php | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 03c7d18a6c..ab91a81206 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -491,7 +491,11 @@ class Application if (!isset($abbrevs[$part])) { $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); - if ($alternatives = $this->findAlternativeNamespace($namespace)) { + if (1 <= $i) { + $part = implode(':',$found).':'.$part; + } + + if ($alternatives = $this->findAlternativeNamespace($part, $abbrevs)) { $message .= "\n\nDid you mean one of these?\n "; $message .= implode("\n ", $alternatives); } @@ -562,11 +566,11 @@ class Application } } - $abbrevs = static::getAbbreviations(array_unique($aliases)); - if (!isset($abbrevs[$searchName])) { + $aliases = static::getAbbreviations(array_unique($aliases)); + if (!isset($aliases[$searchName])) { $message = sprintf('Command "%s" is not defined.', $name); - if ($alternatives = $this->findAlternativeCommands($searchName)) { + if ($alternatives = $this->findAlternativeCommands($searchName, $abbrevs)) { $message .= "\n\nDid you mean one of these?\n "; $message .= implode("\n ", $alternatives); } @@ -574,11 +578,11 @@ class Application throw new \InvalidArgumentException($message); } - if (count($abbrevs[$searchName]) > 1) { - throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $this->getAbbreviationSuggestions($abbrevs[$searchName]))); + if (count($aliases[$searchName]) > 1) { + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $this->getAbbreviationSuggestions($aliases[$searchName]))); } - return $this->get($abbrevs[$searchName][0]); + return $this->get($aliases[$searchName][0]); } /** @@ -933,45 +937,49 @@ class Application /** * Finds alternative commands of $name * - * @param string $name The full name of the command + * @param string $name The full name of the command + * @param array $abbrevs The abbreviations * * @return array A sorted array of similar commands */ - private function findAlternativeCommands($name) + private function findAlternativeCommands($name, $abbrevs) { - $getNameCallback = function($command) { - return $command->getName(); + $callback = function($item) { + return $item->getName(); }; - return $this->findAlternatives($name, $this->commands, $getNameCallback); + return $this->findAlternatives($name, $this->commands, $abbrevs, $callback); } /** * Finds alternative namespace of $name * - * @param string $name The full name of the namespace + * @param string $name The full name of the namespace + * @param array $abbrevs The abbreviations * * @return array A sorted array of similar namespace */ - private function findAlternativeNamespace($name) + private function findAlternativeNamespace($name, $abbrevs) { - return $this->findAlternatives($name, $this->getNamespaces()); + return $this->findAlternatives($name, $this->getNamespaces(), $abbrevs); } /** - * Finds alternative of $name among $collection + * Finds alternative of $name among $collection, + * if nothing is found in $collection, try in $abbrevs * * @param string $name The string - * @param array|Traversable $collection The collection - * @param Closure|string|array $callback The callable to transform item before comparison + * @param array|Traversable $collection The collecion + * @param array $abbrevs The abbreviations + * @param Closure|string|array $callback The callable to transform collection item before comparison * * @return array A sorted array of similar string */ - private function findAlternatives($name, $collection, $callback = null) { + private function findAlternatives($name, $collection, $abbrevs, $callback = null) { $alternatives = array(); foreach ($collection as $item) { - if (null !== $callback) { + if(null !== $callback) { $item = call_user_func($callback, $item); } @@ -981,6 +989,17 @@ class Application } } + if (!$alternatives) { + foreach ($abbrevs as $key => $values) { + $lev = levenshtein($name, $key); + if ($lev <= strlen($name) / 3 || false !== strpos($key, $name)) { + foreach ($values as $value) { + $alternatives[$value] = $lev; + } + } + } + } + asort($alternatives); return array_keys($alternatives); From e5edf5ab86d2634b5d07279bac683a81b57826b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Mon, 13 Feb 2012 00:29:28 +0100 Subject: [PATCH 5/5] [Console] Fixed CS --- src/Symfony/Component/Console/Application.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index ab91a81206..e04940ab11 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -492,7 +492,7 @@ class Application $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); if (1 <= $i) { - $part = implode(':',$found).':'.$part; + $part = implode(':', $found).':'.$part; } if ($alternatives = $this->findAlternativeNamespace($part, $abbrevs)) { @@ -979,7 +979,7 @@ class Application $alternatives = array(); foreach ($collection as $item) { - if(null !== $callback) { + if (null !== $callback) { $item = call_user_func($callback, $item); }