Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 1567f44

Browse filesBrowse files
committed
[Command] Added question helper for unknown or ambiguous commands
1 parent d9f1a72 commit 1567f44
Copy full SHA for 1567f44
Expand file treeCollapse file tree

10 files changed

+265
-41
lines changed

‎src/Symfony/Component/Console/Application.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Application.php
+40-32Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@
1111

1212
namespace Symfony\Component\Console;
1313

14+
use Symfony\Component\Console\Exception\AmbiguousCommandException;
15+
use Symfony\Component\Console\Exception\AmbiguousNamespaceException;
1416
use Symfony\Component\Console\Exception\ExceptionInterface;
17+
use Symfony\Component\Console\Exception\InvalidArgumentException;
18+
use Symfony\Component\Console\Exception\InvalidCommandNameException;
19+
use Symfony\Component\Console\Exception\UnknownCommandException;
20+
use Symfony\Component\Console\Exception\UnknownNamespaceException;
1521
use Symfony\Component\Console\Helper\DebugFormatterHelper;
1622
use Symfony\Component\Console\Helper\ProcessHelper;
1723
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -36,6 +42,8 @@
3642
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
3743
use Symfony\Component\Console\Exception\CommandNotFoundException;
3844
use Symfony\Component\Console\Exception\LogicException;
45+
use Symfony\Component\Console\Question\ChoiceQuestion;
46+
use Symfony\Component\Console\Question\ConfirmationQuestion;
3947
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
4048

4149
/**
@@ -181,7 +189,30 @@ public function doRun(InputInterface $input, OutputInterface $output)
181189
}
182190

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

186217
$this->runningCommand = $command;
187218
$exitCode = $this->doRunCommand($command, $input, $output);
@@ -470,7 +501,8 @@ public function getNamespaces()
470501
*
471502
* @return string A registered namespace
472503
*
473-
* @throws CommandNotFoundException When namespace is incorrect or ambiguous
504+
* @throws UnknownNamespaceException When namespace is incorrect
505+
* @throws AmbiguousNamespaceException When namespace is ambiguous
474506
*/
475507
public function findNamespace($namespace)
476508
{
@@ -479,24 +511,12 @@ public function findNamespace($namespace)
479511
$namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
480512

481513
if (empty($namespaces)) {
482-
$message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
483-
484-
if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
485-
if (1 == count($alternatives)) {
486-
$message .= "\n\nDid you mean this?\n ";
487-
} else {
488-
$message .= "\n\nDid you mean one of these?\n ";
489-
}
490-
491-
$message .= implode("\n ", $alternatives);
492-
}
493-
494-
throw new CommandNotFoundException($message, $alternatives);
514+
throw new UnknownNamespaceException($namespace, $this->findAlternatives($namespace, $allNamespaces, array()));
495515
}
496516

497517
$exact = in_array($namespace, $namespaces, true);
498518
if (count($namespaces) > 1 && !$exact) {
499-
throw new CommandNotFoundException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
519+
throw new AmbiguousNamespaceException($namespace, $namespaces);
500520
}
501521

502522
return $exact ? $namespace : reset($namespaces);
@@ -512,7 +532,8 @@ public function findNamespace($namespace)
512532
*
513533
* @return Command A Command instance
514534
*
515-
* @throws CommandNotFoundException When command name is incorrect or ambiguous
535+
* @throws UnknownCommandException When command name is incorrect
536+
* @throws AmbiguousCommandException When command name is ambiguous
516537
*/
517538
public function find($name)
518539
{
@@ -526,18 +547,7 @@ public function find($name)
526547
$this->findNamespace(substr($name, 0, $pos));
527548
}
528549

529-
$message = sprintf('Command "%s" is not defined.', $name);
530-
531-
if ($alternatives = $this->findAlternatives($name, $allCommands)) {
532-
if (1 == count($alternatives)) {
533-
$message .= "\n\nDid you mean this?\n ";
534-
} else {
535-
$message .= "\n\nDid you mean one of these?\n ";
536-
}
537-
$message .= implode("\n ", $alternatives);
538-
}
539-
540-
throw new CommandNotFoundException($message, $alternatives);
550+
throw new UnknownCommandException($name, $this->findAlternatives($name, $allCommands, array()));
541551
}
542552

543553
// filter out aliases for commands which are already on the list
@@ -552,9 +562,7 @@ public function find($name)
552562

553563
$exact = in_array($name, $commands, true);
554564
if (count($commands) > 1 && !$exact) {
555-
$suggestions = $this->getAbbreviationSuggestions(array_values($commands));
556-
557-
throw new CommandNotFoundException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions), array_values($commands));
565+
throw new AmbiguousCommandException($name, array_values($commands));
558566
}
559567

560568
return $this->get($exact ? $name : reset($commands));
+36Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
* @author Martin Hasoň <martin.hason@gmail.com>
16+
*/
17+
class AmbiguousCommandException extends CommandNotFoundException
18+
{
19+
private $command;
20+
21+
public function __construct($command, $alternatives = array(), $code = null, $previous = null)
22+
{
23+
$this->command = $command;
24+
$message = sprintf('Command "%s" is ambiguous (%s).', $command, $this->getAbbreviationSuggestions($alternatives));
25+
26+
parent::__construct($message, $alternatives, $code, $previous);
27+
}
28+
29+
/**
30+
* @return string
31+
*/
32+
public function getCommand()
33+
{
34+
return $this->command;
35+
}
36+
}
+37Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
* @author Martin Hasoň <martin.hason@gmail.com>
16+
*/
17+
class AmbiguousNamespaceException extends CommandNotFoundException
18+
{
19+
private $namespace;
20+
21+
public function __construct($namespace, $alternatives = array(), $code = null, $previous = null)
22+
{
23+
$this->command = $namespace;
24+
25+
$message = sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions($alternatives));
26+
27+
parent::__construct($message, $alternatives, $code, $previous);
28+
}
29+
30+
/**
31+
* @return string
32+
*/
33+
public function getNamespace()
34+
{
35+
return $this->namespace;
36+
}
37+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Exception/CommandNotFoundException.php
+12Lines changed: 12 additions & 0 deletions
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
}
+47Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
* @author Martin Hasoň <martin.hason@gmail.com>
16+
*/
17+
class UnknownCommandException extends CommandNotFoundException
18+
{
19+
private $command;
20+
21+
public function __construct($command, $alternatives = array(), $code = null, $previous = null)
22+
{
23+
$this->command = $command;
24+
25+
$message = sprintf('Command "%s" is not defined.', $command);
26+
27+
if ($alternatives) {
28+
if (1 == count($alternatives)) {
29+
$message .= "\n\nDid you mean this?\n ";
30+
} else {
31+
$message .= "\n\nDid you mean one of these?\n ";
32+
}
33+
34+
$message .= implode("\n ", $alternatives);
35+
}
36+
37+
parent::__construct($message, $alternatives, $code, $previous);
38+
}
39+
40+
/**
41+
* @return string
42+
*/
43+
public function getCommand()
44+
{
45+
return $this->command;
46+
}
47+
}
+47Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
* @author Martin Hasoň <martin.hason@gmail.com>
16+
*/
17+
class UnknownNamespaceException extends CommandNotFoundException
18+
{
19+
private $namespace;
20+
21+
public function __construct($namespace, $alternatives = array(), $code = null, $previous = null)
22+
{
23+
$this->namespace = $namespace;
24+
25+
$message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
26+
27+
if ($alternatives) {
28+
if (1 == count($alternatives)) {
29+
$message .= "\n\nDid you mean this?\n ";
30+
} else {
31+
$message .= "\n\nDid you mean one of these?\n ";
32+
}
33+
34+
$message .= implode("\n ", $alternatives);
35+
}
36+
37+
parent::__construct($message, $alternatives, $code, $previous);
38+
}
39+
40+
/**
41+
* @return string
42+
*/
43+
public function getNamespace()
44+
{
45+
return $this->namespace;
46+
}
47+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Tests/ApplicationTest.php
+27Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,33 @@ public function testFindAlternativeCommands()
405405
}
406406
}
407407

408+
public function testFindAlternativeCommandsWithQuestion()
409+
{
410+
$application = new Application();
411+
$application->setAutoExit(false);
412+
putenv('COLUMNS=120');
413+
putenv('SHELL_INTERACTIVE=1');
414+
$application->add(new \FooCommand());
415+
$application->add(new \Foo1Command());
416+
$application->add(new \Foo2Command());
417+
418+
$input = new ArrayInput(array('command' => 'foo'));
419+
420+
$inputStream = fopen('php://memory', 'r+', false);
421+
fwrite($inputStream, "1\n");
422+
rewind($inputStream);
423+
$input->setStream($inputStream);
424+
425+
$output = new StreamOutput(fopen('php://memory', 'w', false), StreamOutput::VERBOSITY_NORMAL, false);
426+
427+
$application->run($input, $output);
428+
429+
rewind($output->getStream());
430+
$display = str_replace(PHP_EOL, "\n", stream_get_contents($output->getStream()));
431+
432+
$this->assertStringEqualsFile(self::$fixturesPath.'/application_unknown_command_question.txt', $display);
433+
}
434+
408435
public function testFindAlternativeCommandsWithAnAlias()
409436
{
410437
$fooCommand = new \FooCommand();
+4-4Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

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

+5-5Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

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

+10Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Command "foo" is not defined. Please select one of these suggested commands:
2+
[0] foo:bar1
3+
[1] foo:bar
4+
[2] foo1:bar
5+
[3] afoobar
6+
[4] afoobar1
7+
[5] afoobar2
8+
> 1
9+
interact called
10+
called

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.