CompletionInput.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console\Completion;
  11. use Symfony\Component\Console\Exception\RuntimeException;
  12. use Symfony\Component\Console\Input\ArgvInput;
  13. use Symfony\Component\Console\Input\InputDefinition;
  14. use Symfony\Component\Console\Input\InputOption;
  15. /**
  16. * An input specialized for shell completion.
  17. *
  18. * This input allows unfinished option names or values and exposes what kind of
  19. * completion is expected.
  20. *
  21. * @author Wouter de Jong <wouter@wouterj.nl>
  22. */
  23. final class CompletionInput extends ArgvInput
  24. {
  25. public const TYPE_ARGUMENT_VALUE = 'argument_value';
  26. public const TYPE_OPTION_VALUE = 'option_value';
  27. public const TYPE_OPTION_NAME = 'option_name';
  28. public const TYPE_NONE = 'none';
  29. private $tokens;
  30. private $currentIndex;
  31. private $completionType;
  32. private $completionName = null;
  33. private $completionValue = '';
  34. /**
  35. * Converts a terminal string into tokens.
  36. *
  37. * This is required for shell completions without COMP_WORDS support.
  38. */
  39. public static function fromString(string $inputStr, int $currentIndex): self
  40. {
  41. preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?<!\\\\)\1(?=$|\s)/', $inputStr, $tokens);
  42. return self::fromTokens($tokens[0], $currentIndex);
  43. }
  44. /**
  45. * Create an input based on an COMP_WORDS token list.
  46. *
  47. * @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv)
  48. * @param $currentIndex the index of the cursor (e.g. COMP_CWORD)
  49. */
  50. public static function fromTokens(array $tokens, int $currentIndex): self
  51. {
  52. $input = new self($tokens);
  53. $input->tokens = $tokens;
  54. $input->currentIndex = $currentIndex;
  55. return $input;
  56. }
  57. /**
  58. * {@inheritdoc}
  59. */
  60. public function bind(InputDefinition $definition): void
  61. {
  62. parent::bind($definition);
  63. $relevantToken = $this->getRelevantToken();
  64. if ('-' === $relevantToken[0]) {
  65. // the current token is an input option: complete either option name or option value
  66. [$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', ''];
  67. $option = $this->getOptionFromToken($optionToken);
  68. if (null === $option && !$this->isCursorFree()) {
  69. $this->completionType = self::TYPE_OPTION_NAME;
  70. $this->completionValue = $relevantToken;
  71. return;
  72. }
  73. if (null !== $option && $option->acceptValue()) {
  74. $this->completionType = self::TYPE_OPTION_VALUE;
  75. $this->completionName = $option->getName();
  76. $this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : '');
  77. return;
  78. }
  79. }
  80. $previousToken = $this->tokens[$this->currentIndex - 1];
  81. if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) {
  82. // check if previous option accepted a value
  83. $previousOption = $this->getOptionFromToken($previousToken);
  84. if (null !== $previousOption && $previousOption->acceptValue()) {
  85. $this->completionType = self::TYPE_OPTION_VALUE;
  86. $this->completionName = $previousOption->getName();
  87. $this->completionValue = $relevantToken;
  88. return;
  89. }
  90. }
  91. // complete argument value
  92. $this->completionType = self::TYPE_ARGUMENT_VALUE;
  93. foreach ($this->definition->getArguments() as $argumentName => $argument) {
  94. if (!isset($this->arguments[$argumentName])) {
  95. break;
  96. }
  97. $argumentValue = $this->arguments[$argumentName];
  98. $this->completionName = $argumentName;
  99. if (\is_array($argumentValue)) {
  100. $this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null;
  101. } else {
  102. $this->completionValue = $argumentValue;
  103. }
  104. }
  105. if ($this->currentIndex >= \count($this->tokens)) {
  106. if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) {
  107. $this->completionName = $argumentName;
  108. $this->completionValue = '';
  109. } else {
  110. // we've reached the end
  111. $this->completionType = self::TYPE_NONE;
  112. $this->completionName = null;
  113. $this->completionValue = '';
  114. }
  115. }
  116. }
  117. /**
  118. * Returns the type of completion required.
  119. *
  120. * TYPE_ARGUMENT_VALUE when completing the value of an input argument
  121. * TYPE_OPTION_VALUE when completing the value of an input option
  122. * TYPE_OPTION_NAME when completing the name of an input option
  123. * TYPE_NONE when nothing should be completed
  124. *
  125. * @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component
  126. */
  127. public function getCompletionType(): string
  128. {
  129. return $this->completionType;
  130. }
  131. /**
  132. * The name of the input option or argument when completing a value.
  133. *
  134. * @return string|null returns null when completing an option name
  135. */
  136. public function getCompletionName(): ?string
  137. {
  138. return $this->completionName;
  139. }
  140. /**
  141. * The value already typed by the user (or empty string).
  142. */
  143. public function getCompletionValue(): string
  144. {
  145. return $this->completionValue;
  146. }
  147. public function mustSuggestOptionValuesFor(string $optionName): bool
  148. {
  149. return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName();
  150. }
  151. public function mustSuggestArgumentValuesFor(string $argumentName): bool
  152. {
  153. return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
  154. }
  155. protected function parseToken(string $token, bool $parseOptions): bool
  156. {
  157. try {
  158. return parent::parseToken($token, $parseOptions);
  159. } catch (RuntimeException $e) {
  160. // suppress errors, completed input is almost never valid
  161. }
  162. return $parseOptions;
  163. }
  164. private function getOptionFromToken(string $optionToken): ?InputOption
  165. {
  166. $optionName = ltrim($optionToken, '-');
  167. if (!$optionName) {
  168. return null;
  169. }
  170. if ('-' === ($optionToken[1] ?? ' ')) {
  171. // long option name
  172. return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null;
  173. }
  174. // short option name
  175. return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null;
  176. }
  177. /**
  178. * The token of the cursor, or the last token if the cursor is at the end of the input.
  179. */
  180. private function getRelevantToken(): string
  181. {
  182. return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex];
  183. }
  184. /**
  185. * Whether the cursor is "free" (i.e. at the end of the input preceded by a space).
  186. */
  187. private function isCursorFree(): bool
  188. {
  189. $nrOfTokens = \count($this->tokens);
  190. if ($this->currentIndex > $nrOfTokens) {
  191. throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.');
  192. }
  193. return $this->currentIndex >= $nrOfTokens;
  194. }
  195. public function __toString()
  196. {
  197. $str = '';
  198. foreach ($this->tokens as $i => $token) {
  199. $str .= $token;
  200. if ($this->currentIndex === $i) {
  201. $str .= '|';
  202. }
  203. $str .= ' ';
  204. }
  205. if ($this->currentIndex > $i) {
  206. $str .= '|';
  207. }
  208. return rtrim($str);
  209. }
  210. }