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