Skip to content

Commit

Permalink
Merge pull request #10974 from HavokInspiration/3.next-shell-unknown-…
Browse files Browse the repository at this point in the history
…tokens

Shell : improve unknown tokens error messages
  • Loading branch information
markstory committed Aug 6, 2017
2 parents 65df178 + c169d26 commit 4ab9aa9
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 20 deletions.
147 changes: 136 additions & 11 deletions src/Console/ConsoleOptionParser.php
Expand Up @@ -731,6 +731,18 @@ public function parse($argv)
*/
public function help($subcommand = null, $format = 'text', $width = 72)
{
if ($subcommand === null) {
$formatter = new HelpFormatter($this);
$formatter->setAlias($this->rootName);

if ($format === 'text') {
return $formatter->text($width);
}
if ($format === 'xml') {
return $formatter->xml();
}
}

if (isset($this->_subcommands[$subcommand])) {
$command = $this->_subcommands[$subcommand];
$subparser = $command->parser();
Expand All @@ -746,15 +758,7 @@ public function help($subcommand = null, $format = 'text', $width = 72)
return $subparser->help(null, $format, $width);
}

$formatter = new HelpFormatter($this);
$formatter->setAlias($this->rootName);

if ($format === 'text') {
return $formatter->text($width);
}
if ($format === 'xml') {
return $formatter->xml();
}
return $this->getCommandError($subcommand);
}

/**
Expand Down Expand Up @@ -782,6 +786,127 @@ public function setRootName($name)
return $this;
}

/**
* Get the message output in the console stating that the command can not be found and tries to guess what the user
* wanted to say. Output a list of available subcommands as well.
*
* @param string $command Unknown command name trying to be dispatched.
* @return string The message to be displayed in the console.
*/
protected function getCommandError($command)
{
$rootCommand = $this->getCommand();
$subcommands = array_keys((array)$this->subcommands());
$bestGuess = $this->findClosestItem($command, $subcommands);

$out = [
sprintf(
'Unable to find the `%s %s` subcommand. See `bin/%s %s --help`.',
$rootCommand,
$command,
$this->rootName,
$rootCommand
),
''
];

if ($bestGuess !== null) {
$out[] = sprintf('Did you mean : `%s %s` ?', $rootCommand, $bestGuess);
$out[] = '';
}
$out[] = sprintf('Available subcommands for the `%s` command are : ', $rootCommand);
$out[] = '';
foreach ($subcommands as $subcommand) {
$out[] = ' - ' . $subcommand;
}

return implode("\n", $out);
}

/**
* Get the message output in the console stating that the option can not be found and tries to guess what the user
* wanted to say. Output a list of available options as well.
*
* @param string $option Unknown option name trying to be used.
* @return string The message to be displayed in the console.
*/
protected function getOptionError($option)
{
$availableOptions = array_keys($this->_options);
$bestGuess = $this->findClosestItem($option, $availableOptions);
$out = [
sprintf('Unknown option `%s`.', $option),
''
];

if ($bestGuess !== null) {
$out[] = sprintf('Did you mean `%s` ?', $bestGuess);
$out[] = '';
}

$out[] = 'Available options are :';
$out[] = '';
foreach ($availableOptions as $availableOption) {
$out[] = ' - ' . $availableOption;
}

return implode("\n", $out);
}

/**
* Get the message output in the console stating that the short option can not be found. Output a list of available
* short options and what option they refer to as well.
*
* @param string $option Unknown short option name trying to be used.
* @return string The message to be displayed in the console.
*/
protected function getShortOptionError($option)
{
$out = [sprintf('Unknown short option `%s`', $option)];
$out[] = '';
$out[] = 'Available short options are :';
$out[] = '';

foreach ($this->_shortOptions as $short => $long) {
$out[] = sprintf(' - `%s` (short for `--%s`)', $short, $long);
}

return implode("\n", $out);
}

/**
* Tries to guess the item name the user originally wanted using the some regex pattern and the levenshtein
* algorithm.
*
* @param string $needle Unknown item (either a subcommand name or an option for instance) trying to be used.
* @param array $haystack List of items available for the type $needle belongs to.
* @return string|null The closest name to the item submitted by the user.
*/
protected function findClosestItem($needle, $haystack)
{
$bestGuess = null;
foreach ($haystack as $item) {
if (preg_match('/^' . $needle . '/', $item)) {
return $item;
}
}

foreach ($haystack as $item) {
if (preg_match('/' . $needle . '/', $item)) {
return $item;
}

$score = levenshtein($needle, $item);

if (!isset($bestScore) || $score < $bestScore) {
$bestScore = $score;
$bestGuess = $item;
}
}

return $bestGuess;
}

/**
* Parse the value for a long option out of $this->_tokens. Will handle
* options with an `=` in them.
Expand Down Expand Up @@ -822,7 +947,7 @@ protected function _parseShortOption($option, $params)
}
}
if (!isset($this->_shortOptions[$key])) {
throw new ConsoleException(sprintf('Unknown short option `%s`', $key));
throw new ConsoleException($this->getShortOptionError($key));
}
$name = $this->_shortOptions[$key];

Expand All @@ -840,7 +965,7 @@ protected function _parseShortOption($option, $params)
protected function _parseOption($name, $params)
{
if (!isset($this->_options[$name])) {
throw new ConsoleException(sprintf('Unknown option `%s`', $name));
throw new ConsoleException($this->getOptionError($name));
}
$option = $this->_options[$name];
$isBoolean = $option->isBoolean();
Expand Down
6 changes: 4 additions & 2 deletions src/Console/Shell.php
Expand Up @@ -459,7 +459,6 @@ public function runCommand($argv, $autoMethod = false, $extra = [])
list($this->params, $this->args) = $this->OptionParser->parse($argv);
} catch (ConsoleException $e) {
$this->err('Error: ' . $e->getMessage());
$this->out($this->OptionParser->help($command));

return false;
}
Expand Down Expand Up @@ -507,7 +506,7 @@ public function runCommand($argv, $autoMethod = false, $extra = [])
return $this->main(...$this->args);
}

$this->out($this->OptionParser->help($command));
$this->err($this->OptionParser->help($command));

return false;
}
Expand Down Expand Up @@ -549,6 +548,9 @@ protected function _displayHelp($command)
$this->_welcome();
}

$subcommands = $this->OptionParser->subcommands();
$command = isset($subcommands[$command]) ? $command : null;

return $this->out($this->OptionParser->help($command, $format));
}

Expand Down
48 changes: 47 additions & 1 deletion tests/TestCase/Console/ConsoleOptionParserTest.php
Expand Up @@ -359,6 +359,8 @@ public function testOptionWithBooleanParam()
* test parsing options that do not exist.
*
* @expectedException \Cake\Console\Exception\ConsoleException
* @expectedExceptionMessageRegexp /Unknown option `fail`.\n\nDid you mean `help` \?\n\nAvailable options are :\n\n
* - help\n - no-commit/
* @return void
*/
public function testOptionThatDoesNotExist()
Expand All @@ -373,12 +375,16 @@ public function testOptionThatDoesNotExist()
* test parsing short options that do not exist.
*
* @expectedException \Cake\Console\Exception\ConsoleException
* @expectedExceptionMessageRegexp /Unknown short option `f`.\n\nAvailable short options are :\n\n
* - `n` (short for `--no-commit`)\n - `c` (short for `--clear`)/
* @return void
*/
public function testShortOptionThatDoesNotExist()
{
$parser = new ConsoleOptionParser('test', false);
$parser->addOption('no-commit', ['boolean' => true]);
$parser->addOption('no-commit', ['boolean' => true, 'short' => 'n']);
$parser->addOption('construct', ['boolean' => true]);
$parser->addOption('clear', ['boolean' => true, 'short' => 'c']);

$parser->parse(['-f']);
}
Expand Down Expand Up @@ -795,6 +801,46 @@ public function testHelpWithRootName()
--help, -h Display this help.
--test A test option.
TEXT;
$this->assertTextEquals($expected, $result, 'Help is not correct.');
}

/**
* test that getCommandError() with an unknown subcommand param shows a helpful message
*
* @return void
*/
public function testHelpUnknownSubcommand()
{
$subParser = [
'options' => [
'foo' => [
'short' => 'f',
'help' => 'Foo.',
'boolean' => true,
]
],
];

$parser = new ConsoleOptionParser('mycommand', false);
$parser
->addSubcommand('method', [
'help' => 'This is a subcommand',
'parser' => $subParser
])
->addOption('test', ['help' => 'A test option.'])
->addSubcommand('unstash');

$result = $parser->help('unknown');
$expected = <<<TEXT
Unable to find the `mycommand unknown` subcommand. See `bin/cake mycommand --help`.
Did you mean : `mycommand unstash` ?
Available subcommands for the `mycommand` command are :
- method
- unstash
TEXT;
$this->assertTextEquals($expected, $result, 'Help is not correct.');
}
Expand Down
12 changes: 6 additions & 6 deletions tests/TestCase/Console/ShellTest.php
Expand Up @@ -1049,7 +1049,7 @@ public function testRunCommandWithMissingMethodInSubcommands()
public function testRunCommandBaseClassMethod()
{
$shell = $this->getMockBuilder('Cake\Console\Shell')
->setMethods(['startup', 'getOptionParser', 'out', 'hr'])
->setMethods(['startup', 'getOptionParser', 'err', 'hr'])
->disableOriginalConstructor()
->getMock();

Expand All @@ -1062,7 +1062,7 @@ public function testRunCommandBaseClassMethod()
$shell->expects($this->once())->method('getOptionParser')
->will($this->returnValue($parser));
$shell->expects($this->never())->method('hr');
$shell->expects($this->once())->method('out');
$shell->expects($this->once())->method('err');

$shell->runCommand(['hr']);
}
Expand All @@ -1075,7 +1075,7 @@ public function testRunCommandBaseClassMethod()
public function testRunCommandMissingMethod()
{
$shell = $this->getMockBuilder('Cake\Console\Shell')
->setMethods(['startup', 'getOptionParser', 'out', 'hr'])
->setMethods(['startup', 'getOptionParser', 'err', 'hr'])
->disableOriginalConstructor()
->getMock();
$shell->io($this->getMockBuilder('Cake\Console\ConsoleIo')->getMock());
Expand All @@ -1086,7 +1086,7 @@ public function testRunCommandMissingMethod()
$parser->expects($this->once())->method('help');
$shell->expects($this->once())->method('getOptionParser')
->will($this->returnValue($parser));
$shell->expects($this->once())->method('out');
$shell->expects($this->once())->method('err');

$result = $shell->runCommand(['idontexist']);
$this->assertFalse($result);
Expand Down Expand Up @@ -1127,7 +1127,7 @@ public function testRunCommandTriggeringHelp()
public function testRunCommandNotCallUnexposedTask()
{
$shell = $this->getMockBuilder('Cake\Console\Shell')
->setMethods(['startup', 'hasTask', 'out'])
->setMethods(['startup', 'hasTask', 'err'])
->disableOriginalConstructor()
->getMock();
$shell->io($this->getMockBuilder('Cake\Console\ConsoleIo')->getMock());
Expand All @@ -1143,7 +1143,7 @@ public function testRunCommandNotCallUnexposedTask()
->method('hasTask')
->will($this->returnValue(true));
$shell->expects($this->never())->method('startup');
$shell->expects($this->once())->method('out');
$shell->expects($this->once())->method('err');
$shell->RunCommand = $task;

$result = $shell->runCommand(['run_command', 'one']);
Expand Down

0 comments on commit 4ab9aa9

Please sign in to comment.