| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 | <?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;    }    /**     * {@inheritdoc}     */    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 (null !== $option && $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 (null !== $previousOption && $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 $e) {            // 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);    }}
 |