8000 [Command] Added question helper for unknown or ambiguous commands · symfony/symfony@1567f44 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1567f44

Browse files
committed
[Command] Added question helper for unknown or ambiguous commands
1 parent d9f1a72 commit 1567f44

10 files changed

+265
-41
lines changed

src/Symfony/Component/Console/Application.php

Lines 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->isInteracti A93C ve() || !$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));
Lines 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+
}
Lines 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

Lines 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
}
Lines 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+
}
Lines 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

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

Lines 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

Lines 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