249 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			PHP
		
	
	
			
		
		
	
	
			249 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 int      $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);
 | 
						|
    }
 | 
						|
}
 |