Skip to content

Commit d766c60

Browse files
committed
[Command] Added question helper for unknown or ambiguous commands
1 parent 72c6c61 commit d766c60

9 files changed

+240
-57
lines changed

src/Symfony/Component/Console/Application.php

+40-44
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@
1313

1414
use Symfony\Component\Console\Descriptor\TextDescriptor;
1515
use Symfony\Component\Console\Descriptor\XmlDescriptor;
16+
use Symfony\Component\Console\Exception\AmbiguousCommandException;
17+
use Symfony\Component\Console\Exception\AmbiguousNamespaceException;
1618
use Symfony\Component\Console\Exception\ExceptionInterface;
19+
use Symfony\Component\Console\Exception\InvalidArgumentException;
20+
use Symfony\Component\Console\Exception\InvalidCommandNameException;
21+
use Symfony\Component\Console\Exception\UnknownCommandException;
22+
use Symfony\Component\Console\Exception\UnknownNamespaceException;
1723
use Symfony\Component\Console\Helper\DebugFormatterHelper;
1824
use Symfony\Component\Console\Helper\ProcessHelper;
1925
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -41,6 +47,8 @@
4147
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
4248
use Symfony\Component\Console\Exception\CommandNotFoundException;
4349
use Symfony\Component\Console\Exception\LogicException;
50+
use Symfony\Component\Console\Question\ChoiceQuestion;
51+
use Symfony\Component\Console\Question\ConfirmationQuestion;
4452
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
4553

4654
/**
@@ -186,7 +194,30 @@ public function doRun(InputInterface $input, OutputInterface $output)
186194
}
187195

188196
// the command name MUST be the first element of the input
189-
$command = $this->find($name);
197+
do {
198+
try {
199+
$command = $this->find($name);
200+
} catch (CommandNotFoundException $e) {
201+
$alternatives = $e->getAlternatives();
202+
if (0 === count($alternatives) || !$input->isInteractive() || !$this->getHelperSet()->has('question')) {
203+
throw $e;
204+
}
205+
206+
$helper = $this->getHelperSet()->get('question');
207+
$question = new ChoiceQuestion(strtok($e->getMessage(), "\n").' Please select one of these suggested commands?', $alternatives);
208+
$question->setMaxAttempts(1);
209+
210+
try {
211+
$name = $helper->ask($input, $output, $question);
212+
} catch (InvalidArgumentException $ex) {
213+
throw $e;
214+
}
215+
216+
if (null === $name) {
217+
throw $e;
218+
}
219+
}
220+
} while (!isset($command));
190221

191222
$this->runningCommand = $command;
192223
$exitCode = $this->doRunCommand($command, $input, $output);
@@ -444,7 +475,8 @@ public function getNamespaces()
444475
*
445476
* @return string A registered namespace
446477
*
447-
* @throws CommandNotFoundException When namespace is incorrect or ambiguous
478+
* @throws UnknownNamespaceException When namespace is incorrect
479+
* @throws AmbiguousNamespaceException When namespace is ambiguous
448480
*/
449481
public function findNamespace($namespace)
450482
{
@@ -453,24 +485,12 @@ public function findNamespace($namespace)
453485
$namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
454486

455487
if (empty($namespaces)) {
456-
$message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
457-
458-
if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
459-
if (1 == count($alternatives)) {
460-
$message .= "\n\nDid you mean this?\n ";
461-
} else {
462-
$message .= "\n\nDid you mean one of these?\n ";
463-
}
464-
465-
$message .= implode("\n ", $alternatives);
466-
}
467-
468-
throw new CommandNotFoundException($message, $alternatives);
488+
throw new UnknownNamespaceException($namespace, $this->findAlternatives($namespace, $allNamespaces, array()));
469489
}
470490

471491
$exact = in_array($namespace, $namespaces, true);
472492
if (count($namespaces) > 1 && !$exact) {
473-
throw new CommandNotFoundException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
493+
throw new AmbiguousNamespaceException($namespace, $namespaces);
474494
}
475495

476496
return $exact ? $namespace : reset($namespaces);
@@ -486,7 +506,8 @@ public function findNamespace($namespace)
486506
*
487507
* @return Command A Command instance
488508
*
489-
* @throws CommandNotFoundException When command name is incorrect or ambiguous
509+
* @throws UnknownCommandException When command name is incorrect
510+
* @throws AmbiguousCommandException When command name is ambiguous
490511
*/
491512
public function find($name)
492513
{
@@ -500,18 +521,7 @@ public function find($name)
500521
$this->findNamespace(substr($name, 0, $pos));
501522
}
502523

503-
$message = sprintf('Command "%s" is not defined.', $name);
504-
505-
if ($alternatives = $this->findAlternatives($name, $allCommands)) {
506-
if (1 == count($alternatives)) {
507-
$message .= "\n\nDid you mean this?\n ";
508-
} else {
509-
$message .= "\n\nDid you mean one of these?\n ";
510-
}
511-
$message .= implode("\n ", $alternatives);
512-
}
513-
514-
throw new CommandNotFoundException($message, $alternatives);
524+
throw new UnknownCommandException($name, $this->findAlternatives($name, $allCommands, array()));
515525
}
516526

517527
// filter out aliases for commands which are already on the list
@@ -526,9 +536,7 @@ public function find($name)
526536

527537
$exact = in_array($name, $commands, true);
528538
if (count($commands) > 1 && !$exact) {
529-
$suggestions = $this->getAbbreviationSuggestions(array_values($commands));
530-
531-
throw new CommandNotFoundException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions), array_values($commands));
539+
throw new AmbiguousCommandException($name, array_values($commands));
532540
}
533541

534542
return $this->get($exact ? $name : reset($commands));
@@ -984,18 +992,6 @@ private function getConsoleMode()
984992
}
985993
}
986994

987-
/**
988-
* Returns abbreviated suggestions in string format.
989-
*
990-
* @param array $abbrevs Abbreviated suggestions to convert
991-
*
992-
* @return string A formatted string of abbreviated suggestions
993-
*/
994-
private function getAbbreviationSuggestions($abbrevs)
995-
{
996-
return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
997-
}
998-
999995
/**
1000996
* Returns the namespace part of the command name.
1001997
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Exception;
13+
14+
/**
15+
* Exception class for when a circular reference is detected when importing resources.
16+
*
17+
* @author Martin Hasoň <martin.hason@gmail.com>
18+
*/
19+
class AmbiguousCommandException extends CommandNotFoundException
20+
{
21+
private $command;
22+
23+
public function __construct($command, $alternatives = array(), $code = null, $previous = null)
24+
{
25+
$this->command = $command;
26+
$message = sprintf('Command "%s" is ambiguous (%s).', $command, $this->getAbbreviationSuggestions($alternatives));
27+
28+
parent::__construct($message, $alternatives, $code, $previous);
29+
}
30+
31+
/**
32+
* @return string
33+
*/
34+
public function getCommand()
35+
{
36+
return $this->command;
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Exception;
13+
14+
/**
15+
* Exception class for when a circular reference is detected when importing resources.
16+
*
17+
* @author Martin Hasoň <martin.hason@gmail.com>
18+
*/
19+
class AmbiguousNamespaceException extends CommandNotFoundException
20+
{
21+
private $namespace;
22+
23+
public function __construct($namespace, $alternatives = array(), $code = null, $previous = null)
24+
{
25+
$this->command = $namespace;
26+
27+
$message = sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions($alternatives));
28+
29+
parent::__construct($message, $alternatives, $code, $previous);
30+
}
31+
32+
/**
33+
* @return string
34+
*/
35+
public function getNamespace()
36+
{
37+
return $this->namespace;
38+
}
39+
}

src/Symfony/Component/Console/Exception/CommandNotFoundException.php

+12
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,16 @@ public function getAlternatives()
4040
{
4141
return $this->alternatives;
4242
}
43+
44+
/**
45+
* Returns abbreviated suggestions in string format.
46+
*
47+
* @param array $abbrevs Abbreviated suggestions to convert
48+
*
49+
* @return string A formatted string of abbreviated suggestions
50+
*/
51+
protected function getAbbreviationSuggestions($abbrevs)
52+
{
53+
return sprintf('%s, %s%s', reset($abbrevs), next($abbrevs), count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
54+
}
4355
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Exception;
13+
14+
/**
15+
* Exception class for when a circular reference is detected when importing resources.
16+
*
17+
* @author Martin Hasoň <martin.hason@gmail.com>
18+
*/
19+
class UnknownCommandException extends CommandNotFoundException
20+
{
21+
private $command;
22+
23+
public function __construct($command, $alternatives = array(), $code = null, $previous = null)
24+
{
25+
$this->command = $command;
26+
27+
$message = sprintf('Command "%s" is not defined.', $command);
28+
29+
if ($alternatives) {
30+
if (1 == count($alternatives)) {
31+
$message .= "\n\nDid you mean this?\n ";
32+
} else {
33+
$message .= "\n\nDid you mean one of these?\n ";
34+
}
35+
36+
$message .= implode("\n ", $alternatives);
37+
}
38+
39+
parent::__construct($message, $alternatives, $code, $previous);
40+
}
41+
42+
/**
43+
* @return string
44+
*/
45+
public function getCommand()
46+
{
47+
return $this->command;
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Exception;
13+
14+
/**
15+
* Exception class for when a circular reference is detected when importing resources.
16+
*
17+
* @author Martin Hasoň <martin.hason@gmail.com>
18+
*/
19+
class UnknownNamespaceException extends CommandNotFoundException
20+
{
21+
private $namespace;
22+
23+
public function __construct($namespace, $alternatives = array(), $code = null, $previous = null)
24+
{
25+
$this->namespace = $namespace;
26+
27+
$message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
28+
29+
if ($alternatives) {
30+
if (1 == count($alternatives)) {
31+
$message .= "\n\nDid you mean this?\n ";
32+
} else {
33+
$message .= "\n\nDid you mean one of these?\n ";
34+
}
35+
36+
$message .= implode("\n ", $alternatives);
37+
}
38+
39+
parent::__construct($message, $alternatives, $code, $previous);
40+
}
41+
42+
/**
43+
* @return string
44+
*/
45+
public function getNamespace()
46+
{
47+
return $this->namespace;
48+
}
49+
}

src/Symfony/Component/Console/Tests/ApplicationTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -484,12 +484,12 @@ public function testSetCatchExceptions()
484484
$tester = new ApplicationTester($application);
485485

486486
$application->setCatchExceptions(true);
487-
$tester->run(array('command' => 'foo'), array('decorated' => false));
487+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'interactive' => false));
488488
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getDisplay(true), '->setCatchExceptions() sets the catch exception flag');
489489

490490
$application->setCatchExceptions(false);
491491
try {
492-
$tester->run(array('command' => 'foo'), array('decorated' => false));
492+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'interactive' => false));
493493
$this->fail('->setCatchExceptions() sets the catch exception flag');
494494
} catch (\Exception $e) {
495495
$this->assertInstanceOf('\Exception', $e, '->setCatchExceptions() sets the catch exception flag');
@@ -530,7 +530,7 @@ public function testRenderException()
530530
->will($this->returnValue(120));
531531
$tester = new ApplicationTester($application);
532532

533-
$tester->run(array('command' => 'foo'), array('decorated' => false));
533+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'interactive' => false));
534534
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getDisplay(true), '->renderException() renders a pretty exception');
535535

536536
$tester->run(array('command' => 'foo'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE));
@@ -554,7 +554,7 @@ public function testRenderException()
554554
->will($this->returnValue(32));
555555
$tester = new ApplicationTester($application);
556556

557-
$tester->run(array('command' => 'foo'), array('decorated' => false));
557+
$tester->run(array('command' => 'foo'), array('decorated' => false, 'interactive' => false));
558558
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal');
559559
}
560560

Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11

22

3-
4-
[Symfony\Component\Console\Exception\CommandNotFoundException]
5-
Command "foo" is not defined.
6-
3+
4+
[Symfony\Component\Console\Exception\UnknownCommandException]
5+
Command "foo" is not defined.
6+
77

88

0 commit comments

Comments
 (0)