CompletionInput.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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 array $tokens;
  30. private int $currentIndex;
  31. private string $completionType;
  32. private ?string $completionName = null;
  33. private string $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. public function bind(InputDefinition $definition): void
  58. {
  59. parent::bind($definition);
  60. $relevantToken = $this->getRelevantToken();
  61. if ('-' === $relevantToken[0]) {
  62. // the current token is an input option: complete either option name or option value
  63. [$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', ''];
  64. $option = $this->getOptionFromToken($optionToken);
  65. if (null === $option && !$this->isCursorFree()) {
  66. $this->completionType = self::TYPE_OPTION_NAME;
  67. $this->completionValue = $relevantToken;
  68. return;
  69. }
  70. if ($option?->acceptValue()) {
  71. $this->completionType = self::TYPE_OPTION_VALUE;
  72. $this->completionName = $option->getName();
  73. $this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : '');
  74. return;
  75. }
  76. }
  77. $previousToken = $this->tokens[$this->currentIndex - 1];
  78. if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) {
  79. // check if previous option accepted a value
  80. $previousOption = $this->getOptionFromToken($previousToken);
  81. if ($previousOption?->acceptValue()) {
  82. $this->completionType = self::TYPE_OPTION_VALUE;
  83. $this->completionName = $previousOption->getName();
  84. $this->completionValue = $relevantToken;
  85. return;
  86. }
  87. }
  88. // complete argument value
  89. $this->completionType = self::TYPE_ARGUMENT_VALUE;
  90. foreach ($this->definition->getArguments() as $argumentName => $argument) {
  91. if (!isset($this->arguments[$argumentName])) {
  92. break;
  93. }
  94. $argumentValue = $this->arguments[$argumentName];
  95. $this->completionName = $argumentName;
  96. if (\is_array($argumentValue)) {
  97. $this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null;
  98. } else {
  99. $this->completionValue = $argumentValue;
  100. }
  101. }
  102. if ($this->currentIndex >= \count($this->tokens)) {
  103. if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) {
  104. $this->completionName = $argumentName;
  105. $this->completionValue = '';
  106. } else {
  107. // we've reached the end
  108. $this->completionType = self::TYPE_NONE;
  109. $this->completionName = null;
  110. $this->completionValue = '';
  111. }
  112. }
  113. }
  114. /**
  115. * Returns the type of completion required.
  116. *
  117. * TYPE_ARGUMENT_VALUE when completing the value of an input argument
  118. * TYPE_OPTION_VALUE when completing the value of an input option
  119. * TYPE_OPTION_NAME when completing the name of an input option
  120. * TYPE_NONE when nothing should be completed
  121. *
  122. * TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component.
  123. *
  124. * @return self::TYPE_*
  125. */
  126. public function getCompletionType(): string
  127. {
  128. return $this->completionType;
  129. }
  130. /**
  131. * The name of the input option or argument when completing a value.
  132. *
  133. * @return string|null returns null when completing an option name
  134. */
  135. public function getCompletionName(): ?string
  136. {
  137. return $this->completionName;
  138. }
  139. /**
  140. * The value already typed by the user (or empty string).
  141. */
  142. public function getCompletionValue(): string
  143. {
  144. return $this->completionValue;
  145. }
  146. public function mustSuggestOptionValuesFor(string $optionName): bool
  147. {
  148. return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName();
  149. }
  150. public function mustSuggestArgumentValuesFor(string $argumentName): bool
  151. {
  152. return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
  153. }
  154. protected function parseToken(string $token, bool $parseOptions): bool
  155. {
  156. try {
  157. return parent::parseToken($token, $parseOptions);
  158. } catch (RuntimeException) {
  159. // suppress errors, completed input is almost never valid
  160. }
  161. return $parseOptions;
  162. }
  163. private function getOptionFromToken(string $optionToken): ?InputOption
  164. {
  165. $optionName = ltrim($optionToken, '-');
  166. if (!$optionName) {
  167. return null;
  168. }
  169. if ('-' === ($optionToken[1] ?? ' ')) {
  170. // long option name
  171. return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null;
  172. }
  173. // short option name
  174. return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null;
  175. }
  176. /**
  177. * The token of the cursor, or the last token if the cursor is at the end of the input.
  178. */
  179. private function getRelevantToken(): string
  180. {
  181. return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex];
  182. }
  183. /**
  184. * Whether the cursor is "free" (i.e. at the end of the input preceded by a space).
  185. */
  186. private function isCursorFree(): bool
  187. {
  188. $nrOfTokens = \count($this->tokens);
  189. if ($this->currentIndex > $nrOfTokens) {
  190. throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.');
  191. }
  192. return $this->currentIndex >= $nrOfTokens;
  193. }
  194. public function __toString()
  195. {
  196. $str = '';
  197. foreach ($this->tokens as $i => $token) {
  198. $str .= $token;
  199. if ($this->currentIndex === $i) {
  200. $str .= '|';
  201. }
  202. $str .= ' ';
  203. }
  204. if ($this->currentIndex > $i) {
  205. $str .= '|';
  206. }
  207. return rtrim($str);
  208. }
  209. }