248 lines
7.9 KiB
PHP
248 lines
7.9 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Symfony\Component\Console\Completion;
|
|
|
|
use Symfony\Component\Console\Exception\RuntimeException;
|
|
use Symfony\Component\Console\Input\ArgvInput;
|
|
use Symfony\Component\Console\Input\InputDefinition;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
|
|
/**
|
|
* An input specialized for shell completion.
|
|
*
|
|
* This input allows unfinished option names or values and exposes what kind of
|
|
* completion is expected.
|
|
*
|
|
* @author Wouter de Jong <wouter@wouterj.nl>
|
|
*/
|
|
final class CompletionInput extends ArgvInput
|
|
{
|
|
public const TYPE_ARGUMENT_VALUE = 'argument_value';
|
|
public const TYPE_OPTION_VALUE = 'option_value';
|
|
public const TYPE_OPTION_NAME = 'option_name';
|
|
public const TYPE_NONE = 'none';
|
|
|
|
private array $tokens;
|
|
private int $currentIndex;
|
|
private string $completionType;
|
|
private ?string $completionName = null;
|
|
private string $completionValue = '';
|
|
|
|
/**
|
|
* Converts a terminal string into tokens.
|
|
*
|
|
* This is required for shell completions without COMP_WORDS support.
|
|
*/
|
|
public static function fromString(string $inputStr, int $currentIndex): self
|
|
{
|
|
preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?<!\\\\)\1(?=$|\s)/', $inputStr, $tokens);
|
|
|
|
return self::fromTokens($tokens[0], $currentIndex);
|
|
}
|
|
|
|
/**
|
|
* Create an input based on an COMP_WORDS token list.
|
|
*
|
|
* @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv)
|
|
* @param $currentIndex the index of the cursor (e.g. COMP_CWORD)
|
|
*/
|
|
public static function fromTokens(array $tokens, int $currentIndex): self
|
|
{
|
|
$input = new self($tokens);
|
|
$input->tokens = $tokens;
|
|
$input->currentIndex = $currentIndex;
|
|
|
|
return $input;
|
|
}
|
|
|
|
public function bind(InputDefinition $definition): void
|
|
{
|
|
parent::bind($definition);
|
|
|
|
$relevantToken = $this->getRelevantToken();
|
|
if ('-' === $relevantToken[0]) {
|
|
// the current token is an input option: complete either option name or option value
|
|
[$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', ''];
|
|
|
|
$option = $this->getOptionFromToken($optionToken);
|
|
if (null === $option && !$this->isCursorFree()) {
|
|
$this->completionType = self::TYPE_OPTION_NAME;
|
|
$this->completionValue = $relevantToken;
|
|
|
|
return;
|
|
}
|
|
|
|
if ($option?->acceptValue()) {
|
|
$this->completionType = self::TYPE_OPTION_VALUE;
|
|
$this->completionName = $option->getName();
|
|
$this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : '');
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
$previousToken = $this->tokens[$this->currentIndex - 1];
|
|
if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) {
|
|
// check if previous option accepted a value
|
|
$previousOption = $this->getOptionFromToken($previousToken);
|
|
if ($previousOption?->acceptValue()) {
|
|
$this->completionType = self::TYPE_OPTION_VALUE;
|
|
$this->completionName = $previousOption->getName();
|
|
$this->completionValue = $relevantToken;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// complete argument value
|
|
$this->completionType = self::TYPE_ARGUMENT_VALUE;
|
|
|
|
foreach ($this->definition->getArguments() as $argumentName => $argument) {
|
|
if (!isset($this->arguments[$argumentName])) {
|
|
break;
|
|
}
|
|
|
|
$argumentValue = $this->arguments[$argumentName];
|
|
$this->completionName = $argumentName;
|
|
if (\is_array($argumentValue)) {
|
|
$this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null;
|
|
} else {
|
|
$this->completionValue = $argumentValue;
|
|
}
|
|
}
|
|
|
|
if ($this->currentIndex >= \count($this->tokens)) {
|
|
if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) {
|
|
$this->completionName = $argumentName;
|
|
$this->completionValue = '';
|
|
} else {
|
|
// we've reached the end
|
|
$this->completionType = self::TYPE_NONE;
|
|
$this->completionName = null;
|
|
$this->completionValue = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the type of completion required.
|
|
*
|
|
* TYPE_ARGUMENT_VALUE when completing the value of an input argument
|
|
* TYPE_OPTION_VALUE when completing the value of an input option
|
|
* TYPE_OPTION_NAME when completing the name of an input option
|
|
* TYPE_NONE when nothing should be completed
|
|
*
|
|
* TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component.
|
|
*
|
|
* @return self::TYPE_*
|
|
*/
|
|
public function getCompletionType(): string
|
|
{
|
|
return $this->completionType;
|
|
}
|
|
|
|
/**
|
|
* The name of the input option or argument when completing a value.
|
|
*
|
|
* @return string|null returns null when completing an option name
|
|
*/
|
|
public function getCompletionName(): ?string
|
|
{
|
|
return $this->completionName;
|
|
}
|
|
|
|
/**
|
|
* The value already typed by the user (or empty string).
|
|
*/
|
|
public function getCompletionValue(): string
|
|
{
|
|
return $this->completionValue;
|
|
}
|
|
|
|
public function mustSuggestOptionValuesFor(string $optionName): bool
|
|
{
|
|
return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName();
|
|
}
|
|
|
|
public function mustSuggestArgumentValuesFor(string $argumentName): bool
|
|
{
|
|
return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
|
|
}
|
|
|
|
protected function parseToken(string $token, bool $parseOptions): bool
|
|
{
|
|
try {
|
|
return parent::parseToken($token, $parseOptions);
|
|
} catch (RuntimeException) {
|
|
// suppress errors, completed input is almost never valid
|
|
}
|
|
|
|
return $parseOptions;
|
|
}
|
|
|
|
private function getOptionFromToken(string $optionToken): ?InputOption
|
|
{
|
|
$optionName = ltrim($optionToken, '-');
|
|
if (!$optionName) {
|
|
return null;
|
|
}
|
|
|
|
if ('-' === ($optionToken[1] ?? ' ')) {
|
|
// long option name
|
|
return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null;
|
|
}
|
|
|
|
// short option name
|
|
return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null;
|
|
}
|
|
|
|
/**
|
|
* The token of the cursor, or the last token if the cursor is at the end of the input.
|
|
*/
|
|
private function getRelevantToken(): string
|
|
{
|
|
return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex];
|
|
}
|
|
|
|
/**
|
|
* Whether the cursor is "free" (i.e. at the end of the input preceded by a space).
|
|
*/
|
|
private function isCursorFree(): bool
|
|
{
|
|
$nrOfTokens = \count($this->tokens);
|
|
if ($this->currentIndex > $nrOfTokens) {
|
|
throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.');
|
|
}
|
|
|
|
return $this->currentIndex >= $nrOfTokens;
|
|
}
|
|
|
|
public function __toString()
|
|
{
|
|
$str = '';
|
|
foreach ($this->tokens as $i => $token) {
|
|
$str .= $token;
|
|
|
|
if ($this->currentIndex === $i) {
|
|
$str .= '|';
|
|
}
|
|
|
|
$str .= ' ';
|
|
}
|
|
|
|
if ($this->currentIndex > $i) {
|
|
$str .= '|';
|
|
}
|
|
|
|
return rtrim($str);
|
|
}
|
|
}
|