CompletionInput.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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;
  33. private $completionValue = '';
  34. /**
  35. * Converts a terminal string into tokens.
  36. *
  37. * This is required for shell completions without COMP_WORDS support.
  38. * @param string $inputStr
  39. * @param int $currentIndex
  40. */
  41. public static function fromString($inputStr, $currentIndex)
  42. {
  43. preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?<!\\\\)\1(?=$|\s)/', $inputStr, $tokens);
  44. return self::fromTokens($tokens[0], $currentIndex);
  45. }
  46. /**
  47. * Create an input based on an COMP_WORDS token list.
  48. *
  49. * @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv)
  50. * @param $currentIndex the index of the cursor (e.g. COMP_CWORD)
  51. * @param int $currentIndex
  52. */
  53. public static function fromTokens($tokens, $currentIndex)
  54. {
  55. $input = new self($tokens);
  56. $input->tokens = $tokens;
  57. $input->currentIndex = $currentIndex;
  58. return $input;
  59. }
  60. /**
  61. * @param \Symfony\Component\Console\Input\InputDefinition $definition
  62. */
  63. public function bind($definition)
  64. {
  65. parent::bind($definition);
  66. $relevantToken = $this->getRelevantToken();
  67. if ('-' === $relevantToken[0]) {
  68. // the current token is an input option: complete either option name or option value
  69. [$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', ''];
  70. $option = $this->getOptionFromToken($optionToken);
  71. if (null === $option && !$this->isCursorFree()) {
  72. $this->completionType = self::TYPE_OPTION_NAME;
  73. $this->completionValue = $relevantToken;
  74. return;
  75. }
  76. if (($option2 = $option) ? $option2->acceptValue() : null) {
  77. $this->completionType = self::TYPE_OPTION_VALUE;
  78. $this->completionName = $option->getName();
  79. $this->completionValue = $optionValue ?: (strncmp($optionToken, '--', strlen('--')) !== 0 ? substr($optionToken, 2) : '');
  80. return;
  81. }
  82. }
  83. $previousToken = $this->tokens[$this->currentIndex - 1];
  84. if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) {
  85. // check if previous option accepted a value
  86. $previousOption = $this->getOptionFromToken($previousToken);
  87. if (($previousOption2 = $previousOption) ? $previousOption2->acceptValue() : null) {
  88. $this->completionType = self::TYPE_OPTION_VALUE;
  89. $this->completionName = $previousOption->getName();
  90. $this->completionValue = $relevantToken;
  91. return;
  92. }
  93. }
  94. // complete argument value
  95. $this->completionType = self::TYPE_ARGUMENT_VALUE;
  96. foreach ($this->definition->getArguments() as $argumentName => $argument) {
  97. if (!isset($this->arguments[$argumentName])) {
  98. break;
  99. }
  100. $argumentValue = $this->arguments[$argumentName];
  101. $this->completionName = $argumentName;
  102. if (\is_array($argumentValue)) {
  103. end($argumentValue);
  104. $this->completionValue = $argumentValue ? $argumentValue[key($argumentValue)] : null;
  105. } else {
  106. $this->completionValue = $argumentValue;
  107. }
  108. }
  109. if ($this->currentIndex >= \count($this->tokens)) {
  110. if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) {
  111. $this->completionName = $argumentName;
  112. $this->completionValue = '';
  113. } else {
  114. // we've reached the end
  115. $this->completionType = self::TYPE_NONE;
  116. $this->completionName = null;
  117. $this->completionValue = '';
  118. }
  119. }
  120. }
  121. /**
  122. * Returns the type of completion required.
  123. *
  124. * TYPE_ARGUMENT_VALUE when completing the value of an input argument
  125. * TYPE_OPTION_VALUE when completing the value of an input option
  126. * TYPE_OPTION_NAME when completing the name of an input option
  127. * TYPE_NONE when nothing should be completed
  128. *
  129. * @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component
  130. */
  131. public function getCompletionType()
  132. {
  133. return $this->completionType;
  134. }
  135. /**
  136. * The name of the input option or argument when completing a value.
  137. *
  138. * @return string|null returns null when completing an option name
  139. */
  140. public function getCompletionName()
  141. {
  142. return $this->completionName;
  143. }
  144. /**
  145. * The value already typed by the user (or empty string).
  146. */
  147. public function getCompletionValue()
  148. {
  149. return $this->completionValue;
  150. }
  151. /**
  152. * @param string $optionName
  153. */
  154. public function mustSuggestOptionValuesFor($optionName)
  155. {
  156. return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName();
  157. }
  158. /**
  159. * @param string $argumentName
  160. */
  161. public function mustSuggestArgumentValuesFor($argumentName)
  162. {
  163. return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
  164. }
  165. /**
  166. * @param string $token
  167. * @param bool $parseOptions
  168. */
  169. protected function parseToken($token, $parseOptions)
  170. {
  171. try {
  172. return parent::parseToken($token, $parseOptions);
  173. } catch (RuntimeException $exception) {
  174. // suppress errors, completed input is almost never valid
  175. }
  176. return $parseOptions;
  177. }
  178. /**
  179. * @param string $optionToken
  180. */
  181. private function getOptionFromToken($optionToken)
  182. {
  183. $optionName = ltrim($optionToken, '-');
  184. if (!$optionName) {
  185. return null;
  186. }
  187. if ('-' === ($optionToken[1] ?? ' ')) {
  188. // long option name
  189. return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null;
  190. }
  191. // short option name
  192. return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null;
  193. }
  194. /**
  195. * The token of the cursor, or the last token if the cursor is at the end of the input.
  196. */
  197. private function getRelevantToken()
  198. {
  199. return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex];
  200. }
  201. /**
  202. * Whether the cursor is "free" (i.e. at the end of the input preceded by a space).
  203. */
  204. private function isCursorFree()
  205. {
  206. $nrOfTokens = \count($this->tokens);
  207. if ($this->currentIndex > $nrOfTokens) {
  208. throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.');
  209. }
  210. return $this->currentIndex >= $nrOfTokens;
  211. }
  212. public function __toString()
  213. {
  214. $str = '';
  215. foreach ($this->tokens as $i => $token) {
  216. $str .= $token;
  217. if ($this->currentIndex === $i) {
  218. $str .= '|';
  219. }
  220. $str .= ' ';
  221. }
  222. if ($this->currentIndex > $i) {
  223. $str .= '|';
  224. }
  225. return rtrim($str);
  226. }
  227. }