| 148 |
lars |
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 |
|
|
|
12 |
namespace Symfony\Component\Console\Completion;
|
|
|
13 |
|
|
|
14 |
use Symfony\Component\Console\Exception\RuntimeException;
|
|
|
15 |
use Symfony\Component\Console\Input\ArgvInput;
|
|
|
16 |
use Symfony\Component\Console\Input\InputDefinition;
|
|
|
17 |
use 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 |
*/
|
|
|
27 |
final 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 $tokens;
|
|
|
35 |
private $currentIndex;
|
|
|
36 |
private $completionType;
|
|
|
37 |
private $completionName = null;
|
|
|
38 |
private $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 $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 |
* @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component
|
|
|
145 |
*/
|
|
|
146 |
public function getCompletionType(): string
|
|
|
147 |
{
|
|
|
148 |
return $this->completionType;
|
|
|
149 |
}
|
|
|
150 |
|
|
|
151 |
/**
|
|
|
152 |
* The name of the input option or argument when completing a value.
|
|
|
153 |
*
|
|
|
154 |
* @return string|null returns null when completing an option name
|
|
|
155 |
*/
|
|
|
156 |
public function getCompletionName(): ?string
|
|
|
157 |
{
|
|
|
158 |
return $this->completionName;
|
|
|
159 |
}
|
|
|
160 |
|
|
|
161 |
/**
|
|
|
162 |
* The value already typed by the user (or empty string).
|
|
|
163 |
*/
|
|
|
164 |
public function getCompletionValue(): string
|
|
|
165 |
{
|
|
|
166 |
return $this->completionValue;
|
|
|
167 |
}
|
|
|
168 |
|
|
|
169 |
public function mustSuggestOptionValuesFor(string $optionName): bool
|
|
|
170 |
{
|
|
|
171 |
return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName();
|
|
|
172 |
}
|
|
|
173 |
|
|
|
174 |
public function mustSuggestArgumentValuesFor(string $argumentName): bool
|
|
|
175 |
{
|
|
|
176 |
return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
|
|
|
177 |
}
|
|
|
178 |
|
|
|
179 |
protected function parseToken(string $token, bool $parseOptions): bool
|
|
|
180 |
{
|
|
|
181 |
try {
|
|
|
182 |
return parent::parseToken($token, $parseOptions);
|
|
|
183 |
} catch (RuntimeException) {
|
|
|
184 |
// suppress errors, completed input is almost never valid
|
|
|
185 |
}
|
|
|
186 |
|
|
|
187 |
return $parseOptions;
|
|
|
188 |
}
|
|
|
189 |
|
|
|
190 |
private function getOptionFromToken(string $optionToken): ?InputOption
|
|
|
191 |
{
|
|
|
192 |
$optionName = ltrim($optionToken, '-');
|
|
|
193 |
if (!$optionName) {
|
|
|
194 |
return null;
|
|
|
195 |
}
|
|
|
196 |
|
|
|
197 |
if ('-' === ($optionToken[1] ?? ' ')) {
|
|
|
198 |
// long option name
|
|
|
199 |
return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null;
|
|
|
200 |
}
|
|
|
201 |
|
|
|
202 |
// short option name
|
|
|
203 |
return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null;
|
|
|
204 |
}
|
|
|
205 |
|
|
|
206 |
/**
|
|
|
207 |
* The token of the cursor, or the last token if the cursor is at the end of the input.
|
|
|
208 |
*/
|
|
|
209 |
private function getRelevantToken(): string
|
|
|
210 |
{
|
|
|
211 |
return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex];
|
|
|
212 |
}
|
|
|
213 |
|
|
|
214 |
/**
|
|
|
215 |
* Whether the cursor is "free" (i.e. at the end of the input preceded by a space).
|
|
|
216 |
*/
|
|
|
217 |
private function isCursorFree(): bool
|
|
|
218 |
{
|
|
|
219 |
$nrOfTokens = \count($this->tokens);
|
|
|
220 |
if ($this->currentIndex > $nrOfTokens) {
|
|
|
221 |
throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.');
|
|
|
222 |
}
|
|
|
223 |
|
|
|
224 |
return $this->currentIndex >= $nrOfTokens;
|
|
|
225 |
}
|
|
|
226 |
|
|
|
227 |
public function __toString()
|
|
|
228 |
{
|
|
|
229 |
$str = '';
|
|
|
230 |
foreach ($this->tokens as $i => $token) {
|
|
|
231 |
$str .= $token;
|
|
|
232 |
|
|
|
233 |
if ($this->currentIndex === $i) {
|
|
|
234 |
$str .= '|';
|
|
|
235 |
}
|
|
|
236 |
|
|
|
237 |
$str .= ' ';
|
|
|
238 |
}
|
|
|
239 |
|
|
|
240 |
if ($this->currentIndex > $i) {
|
|
|
241 |
$str .= '|';
|
|
|
242 |
}
|
|
|
243 |
|
|
|
244 |
return rtrim($str);
|
|
|
245 |
}
|
|
|
246 |
}
|