247 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			PHP
		
	
	
			
		
		
	
	
			247 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 $tokens;
 | |
|     private $currentIndex;
 | |
|     private $completionType;
 | |
|     private $completionName = null;
 | |
|     private $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
 | |
|      *
 | |
|      * @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component
 | |
|      */
 | |
|     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);
 | |
|     }
 | |
| }
 |