feature #33412 [Console] Do not leak hidden console commands (m-vo)

This PR was merged into the 4.4 branch.

Discussion
----------

[Console] Do not leak hidden console commands

| Q             | A
| ------------- | ---
| Branch?       | 4.4 (updated)
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #33398
| License       | MIT

This PR attempts to fix hidden console commands to be leaked when interacting with the console.

These are the changes:
* Hidden commands won't be shown anymore in the list of commands in a namespace as well as the list of  suggestions ("Did you mean...") for invalid or ambiguous commands.
* Hidden commands therefore now need to be always entered with their full name.
* If an abbreviated command is entered that was previously ambiguous with (only) hidden commands, it's now executed directly (not ambiguous anymore).

Side note: When implementing the tests & changes I realized that `Application->get()` isn't side effect free (when redirecting to the help command) and behaves differently when called multiple times. It therefore must not be used from inside `find()`. Maybe we should change this? Here are the relevant bits:
f71f74b36a/src/Symfony/Component/Console/Application.php (L495-L502)

Commits
-------

f3406338e6 [Console] Deprecate abbreviating hidden command names using  Application->find()
This commit is contained in:
Robin Chalas 2019-09-28 17:03:31 +02:00
commit e627989089
6 changed files with 97 additions and 4 deletions

View File

@ -6,6 +6,11 @@ Cache
* Added argument `$prefix` to `AdapterInterface::clear()`
Console
-------
* Deprecated finding hidden commands using an abbreviation, use the full name instead
Debug
-----

View File

@ -31,6 +31,7 @@ Config
Console
-------
* Removed support for finding hidden commands using an abbreviation, use the full name instead
* Removed the `setCrossingChar()` method in favor of the `setDefaultCrossingChar()` method in `TableStyle`.
* Removed the `setHorizontalBorderChar()` method in favor of the `setDefaultCrossingChars()` method in `TableStyle`.
* Removed the `getHorizontalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`.

View File

@ -692,12 +692,14 @@ class Application implements ResetInterface
foreach ($abbrevs as $abbrev) {
$maxLen = max(Helper::strlen($abbrev), $maxLen);
}
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) {
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
if (!$commandList[$cmd] instanceof Command) {
$commandList[$cmd] = $this->commandLoader->get($cmd);
}
if ($commandList[$cmd]->isHidden()) {
unset($commands[array_search($cmd, $commands)]);
return false;
}
@ -705,12 +707,21 @@ class Application implements ResetInterface
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
}, array_values($commands));
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
if (\count($commands) > 1) {
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
}
}
return $this->get(reset($commands));
$command = $this->get(reset($commands));
if ($command->isHidden()) {
@trigger_error(sprintf('Command "%s" is hidden, finding it using an abbreviation is deprecated since Symfony 4.4, use its full name instead.', $command->getName()), E_USER_DEPRECATED);
}
return $command;
}
/**

View File

@ -4,6 +4,7 @@ CHANGELOG
4.4.0
-----
* deprecated finding hidden commands using an abbreviation, use the full name instead
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
* added method `preventRedrawFasterThan()` and `forceRedrawSlowerThan()` on `ProgressBar`
* `Application` implements `ResetInterface`

View File

@ -76,6 +76,7 @@ class ApplicationTest extends TestCase
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering.php';
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering2.php';
require_once self::$fixturesPath.'/FooHiddenCommand.php';
require_once self::$fixturesPath.'/BarHiddenCommand.php';
}
protected function normalizeLineBreaks($text)
@ -441,6 +442,16 @@ class ApplicationTest extends TestCase
];
}
public function testFindWithAmbiguousAbbreviationsFindsCommandIfAlternativesAreHidden()
{
$application = new Application();
$application->add(new \FooCommand());
$application->add(new \FooHiddenCommand());
$this->assertInstanceOf('FooCommand', $application->find('foo:'));
}
public function testFindCommandEqualNamespace()
{
$application = new Application();
@ -708,6 +719,49 @@ class ApplicationTest extends TestCase
$application->find('foo::bar');
}
public function testFindHiddenWithExactName()
{
$application = new Application();
$application->add(new \FooHiddenCommand());
$this->assertInstanceOf('FooHiddenCommand', $application->find('foo:hidden'));
$this->assertInstanceOf('FooHiddenCommand', $application->find('afoohidden'));
}
/**
* @group legacy
* @expectedDeprecation Command "%s:hidden" is hidden, finding it using an abbreviation is deprecated since Symfony 4.4, use its full name instead.
* @dataProvider provideAbbreviationsForHiddenCommands
*/
public function testFindHiddenWithAbbreviatedName($name)
{
$application = new Application();
$application->add(new \FooHiddenCommand());
$application->add(new \BarHiddenCommand());
$application->find($name);
}
public function provideAbbreviationsForHiddenCommands()
{
return [
['foo:hidde'],
['afoohidd'],
['bar:hidde'],
];
}
public function testFindAmbiguousCommandsIfAllAlternativesAreHidden()
{
$application = new Application();
$application->add(new \FooCommand());
$application->add(new \FooHiddenCommand());
$this->assertInstanceOf('FooCommand', $application->find('foo:'));
}
public function testSetCatchExceptions()
{
$application = new Application();

View File

@ -0,0 +1,21 @@
<?php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class BarHiddenCommand extends Command
{
protected function configure()
{
$this
->setName('bar:hidden')
->setAliases(['abarhidden'])
->setHidden(true)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
}
}