Skip to content

Commit

Permalink
Fix command not set correctly when options precede the command name
Browse files Browse the repository at this point in the history
The CompletionHandler code made an assumption that the command name
was always the second word on the command line:

    $ [program-name] [command-name] [...arguments and options]

This now searches for the first non-option-like word and stores its
index for other parts of CompletionHandler to test against.

The way ArrayInput is built in `CompletionHandler::getInput()` doesn't
align with how the docs for that class expect it to be used, so calling
`getFirstArgument()` was always returning the first value.

Fixes #83
  • Loading branch information
stecman committed Apr 29, 2019
1 parent b71718c commit 7bfa9b9
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 9 deletions.
63 changes: 54 additions & 9 deletions src/CompletionHandler.php
Expand Up @@ -34,6 +34,12 @@ class CompletionHandler
*/
protected $helpers = array();

/**
* Index the command name was detected at
* @var int
*/
private $commandWordIndex;

public function __construct(Application $application, CompletionContext $context = null)
{
$this->application = $application;
Expand Down Expand Up @@ -100,13 +106,8 @@ public function runCompletion()
throw new \RuntimeException('A CompletionContext must be set before requesting completion.');
}

$cmdName = $this->getInput()->getFirstArgument();

try {
$this->command = $this->application->find($cmdName);
} catch (\InvalidArgumentException $e) {
// Exception thrown, when multiple or none commands are found.
}
// Set the command to query options and arugments from
$this->command = $this->detectCommand();

$process = array(
'completeForOptionValues',
Expand All @@ -132,6 +133,9 @@ public function runCompletion()
/**
* Get an InputInterface representation of the completion context
*
* @deprecated Incorrectly uses the ArrayInput API and is no longer needed.
* This will be removed in the next major version.
*
* @return ArrayInput
*/
public function getInput()
Expand Down Expand Up @@ -256,7 +260,7 @@ protected function completeForOptionValues()
*/
protected function completeForCommandName()
{
if (!$this->command || (count($this->context->getWords()) == 2 && $this->context->getWordIndex() == 1)) {
if (!$this->command || $this->context->getWordIndex() == $this->commandWordIndex) {
return $this->getCommandNames();
}

Expand Down Expand Up @@ -362,7 +366,8 @@ protected function mapArgumentsToWords($argumentDefinitions)

foreach ($this->context->getWords() as $wordIndex => $word) {
// Skip program name, command name, options, and option values
if ($wordIndex < 2
if ($wordIndex == 0
|| $wordIndex === $this->commandWordIndex
|| ($word && '-' === $word[0])
|| in_array($previousWord, $optionsWithArgs)) {
$previousWord = $word;
Expand Down Expand Up @@ -469,4 +474,44 @@ protected function getCommandNames()
return array_keys($commands);
}
}

/**
* Find the current command name in the command-line
*
* Note this only cares about flag-type options. Options with values cannot
* appear before a command name in Symfony Console application.
*
* @return Command|null
*/
private function detectCommand()
{
// Always skip the first word (program name)
$skipNext = true;

foreach ($this->context->getWords() as $index => $word) {

// Skip word if flagged
if ($skipNext) {
$skipNext = false;
continue;
}

// Skip empty words and words that look like options
if (strlen($word) == 0 || $word[0] === '-') {
continue;
}

// Return the first unambiguous match to argument-like words
try {
$cmd = $this->application->find($word);
$this->commandWordIndex = $index;
return $cmd;
} catch (\InvalidArgumentException $e) {
// Exception thrown, when multiple or no commands are found.
}
}

// No command found
return null;
}
}
Expand Up @@ -68,6 +68,17 @@ public function testCompleteOptionShortcut()
$this->assertEquals(array('-j'), $this->getTerms($handler->runCompletion()));
}

public function testCompleteOptionShortcutFirst()
{
// Check command options complete
$handler = $this->createHandler('app -v wave --');
$this->assertArraySubset(array('--vigorous', '--jazz-hands'), $this->getTerms($handler->runCompletion()));

// Check unambiguous command name still completes
$handler = $this->createHandler('app --quiet wav');
$this->assertEquals(array('wave'), $this->getTerms($handler->runCompletion()));
}

public function testCompleteDoubleDash()
{
$handler = $this->createHandler('app wave --');
Expand Down

0 comments on commit 7bfa9b9

Please sign in to comment.