Pull merge.
[yaffs-website] / vendor / stecman / symfony-console-completion / src / CompletionHandler.php
1 <?php
2
3 namespace Stecman\Component\Symfony\Console\BashCompletion;
4
5 use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
6 use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionInterface;
7 use Symfony\Component\Console\Application;
8 use Symfony\Component\Console\Command\Command;
9 use Symfony\Component\Console\Input\ArrayInput;
10 use Symfony\Component\Console\Input\InputArgument;
11 use Symfony\Component\Console\Input\InputOption;
12
13 class CompletionHandler
14 {
15     /**
16      * Application to complete for
17      * @var \Symfony\Component\Console\Application
18      */
19     protected $application;
20
21     /**
22      * @var Command
23      */
24     protected $command;
25
26     /**
27      * @var CompletionContext
28      */
29     protected $context;
30
31     /**
32      * Array of completion helpers.
33      * @var CompletionInterface[]
34      */
35     protected $helpers = array();
36
37     public function __construct(Application $application, CompletionContext $context = null)
38     {
39         $this->application = $application;
40         $this->context = $context;
41
42         // Set up completions for commands that are built-into Application
43         $this->addHandler(
44             new Completion(
45                 'help',
46                 'command_name',
47                 Completion::TYPE_ARGUMENT,
48                 $this->getCommandNames()
49             )
50         );
51
52         $this->addHandler(
53             new Completion(
54                 'list',
55                 'namespace',
56                 Completion::TYPE_ARGUMENT,
57                 $application->getNamespaces()
58             )
59         );
60     }
61
62     public function setContext(CompletionContext $context)
63     {
64         $this->context = $context;
65     }
66
67     /**
68      * @return CompletionContext
69      */
70     public function getContext()
71     {
72         return $this->context;
73     }
74
75     /**
76      * @param CompletionInterface[] $array
77      */
78     public function addHandlers(array $array)
79     {
80         $this->helpers = array_merge($this->helpers, $array);
81     }
82
83     /**
84      * @param CompletionInterface $helper
85      */
86     public function addHandler(CompletionInterface $helper)
87     {
88         $this->helpers[] = $helper;
89     }
90
91     /**
92      * Do the actual completion, returning an array of strings to provide to the parent shell's completion system
93      *
94      * @throws \RuntimeException
95      * @return string[]
96      */
97     public function runCompletion()
98     {
99         if (!$this->context) {
100             throw new \RuntimeException('A CompletionContext must be set before requesting completion.');
101         }
102
103         $cmdName = $this->getInput()->getFirstArgument();
104
105         try {
106             $this->command = $this->application->find($cmdName);
107         } catch (\InvalidArgumentException $e) {
108             // Exception thrown, when multiple or none commands are found.
109         }
110
111         $process = array(
112             'completeForOptionValues',
113             'completeForOptionShortcuts',
114             'completeForOptionShortcutValues',
115             'completeForOptions',
116             'completeForCommandName',
117             'completeForCommandArguments'
118         );
119
120         foreach ($process as $methodName) {
121             $result = $this->{$methodName}();
122
123             if (false !== $result) {
124                 // Return the result of the first completion mode that matches
125                 return $this->filterResults((array) $result);
126             }
127         }
128
129         return array();
130     }
131
132     /**
133      * Get an InputInterface representation of the completion context
134      *
135      * @return ArrayInput
136      */
137     public function getInput()
138     {
139         // Filter the command line content to suit ArrayInput
140         $words = $this->context->getWords();
141         array_shift($words);
142         $words = array_filter($words);
143
144         return new ArrayInput($words);
145     }
146
147     /**
148      * Attempt to complete the current word as a long-form option (--my-option)
149      *
150      * @return array|false
151      */
152     protected function completeForOptions()
153     {
154         $word = $this->context->getCurrentWord();
155
156         if (substr($word, 0, 2) === '--') {
157             $options = array();
158
159             foreach ($this->getAllOptions() as $opt) {
160                 $options[] = '--'.$opt->getName();
161             }
162
163             return $options;
164         }
165
166         return false;
167     }
168
169     /**
170      * Attempt to complete the current word as an option shortcut.
171      *
172      * If the shortcut exists it will be completed, but a list of possible shortcuts is never returned for completion.
173      *
174      * @return array|false
175      */
176     protected function completeForOptionShortcuts()
177     {
178         $word = $this->context->getCurrentWord();
179
180         if (strpos($word, '-') === 0 && strlen($word) == 2) {
181             $definition = $this->command ? $this->command->getNativeDefinition() : $this->application->getDefinition();
182
183             if ($definition->hasShortcut(substr($word, 1))) {
184                 return array($word);
185             }
186         }
187
188         return false;
189     }
190
191     /**
192      * Attempt to complete the current word as the value of an option shortcut
193      *
194      * @return array|false
195      */
196     protected function completeForOptionShortcutValues()
197     {
198         $wordIndex = $this->context->getWordIndex();
199
200         if ($this->command && $wordIndex > 1) {
201             $left = $this->context->getWordAtIndex($wordIndex - 1);
202
203             // Complete short options
204             if ($left[0] == '-' && strlen($left) == 2) {
205                 $shortcut = substr($left, 1);
206                 $def = $this->command->getNativeDefinition();
207
208                 if (!$def->hasShortcut($shortcut)) {
209                     return false;
210                 }
211
212                 $opt = $def->getOptionForShortcut($shortcut);
213                 if ($opt->isValueRequired() || $opt->isValueOptional()) {
214                     return $this->completeOption($opt);
215                 }
216             }
217         }
218
219         return false;
220     }
221
222     /**
223      * Attemp to complete the current word as the value of a long-form option
224      *
225      * @return array|false
226      */
227     protected function completeForOptionValues()
228     {
229         $wordIndex = $this->context->getWordIndex();
230
231         if ($this->command && $wordIndex > 1) {
232             $left = $this->context->getWordAtIndex($wordIndex - 1);
233
234             if (strpos($left, '--') === 0) {
235                 $name = substr($left, 2);
236                 $def = $this->command->getNativeDefinition();
237
238                 if (!$def->hasOption($name)) {
239                     return false;
240                 }
241
242                 $opt = $def->getOption($name);
243                 if ($opt->isValueRequired() || $opt->isValueOptional()) {
244                     return $this->completeOption($opt);
245                 }
246             }
247         }
248
249         return false;
250     }
251
252     /**
253      * Attempt to complete the current word as a command name
254      *
255      * @return array|false
256      */
257     protected function completeForCommandName()
258     {
259         if (!$this->command || (count($this->context->getWords()) == 2 && $this->context->getWordIndex() == 1)) {
260             return $this->getCommandNames();
261         }
262
263         return false;
264     }
265
266     /**
267      * Attempt to complete the current word as a command argument value
268      *
269      * @see Symfony\Component\Console\Input\InputArgument
270      * @return array|false
271      */
272     protected function completeForCommandArguments()
273     {
274         if (!$this->command || strpos($this->context->getCurrentWord(), '-') === 0) {
275             return false;
276         }
277
278         $definition = $this->command->getNativeDefinition();
279         $argWords = $this->mapArgumentsToWords($definition->getArguments());
280         $wordIndex = $this->context->getWordIndex();
281
282         if (isset($argWords[$wordIndex])) {
283             $name = $argWords[$wordIndex];
284         } elseif (!empty($argWords) && $definition->getArgument(end($argWords))->isArray()) {
285             $name = end($argWords);
286         } else {
287             return false;
288         }
289
290         if ($helper = $this->getCompletionHelper($name, Completion::TYPE_ARGUMENT)) {
291             return $helper->run();
292         }
293
294         if ($this->command instanceof CompletionAwareInterface) {
295             return $this->command->completeArgumentValues($name, $this->context);
296         }
297
298         return false;
299     }
300
301     /**
302      * Find a CompletionInterface that matches the current command, target name, and target type
303      *
304      * @param string $name
305      * @param string $type
306      * @return CompletionInterface|null
307      */
308     protected function getCompletionHelper($name, $type)
309     {
310         foreach ($this->helpers as $helper) {
311             if ($helper->getType() != $type && $helper->getType() != CompletionInterface::ALL_TYPES) {
312                 continue;
313             }
314
315             if ($helper->getCommandName() == CompletionInterface::ALL_COMMANDS || $helper->getCommandName() == $this->command->getName()) {
316                 if ($helper->getTargetName() == $name) {
317                     return $helper;
318                 }
319             }
320         }
321
322         return null;
323     }
324
325     /**
326      * Complete the value for the given option if a value completion is availble
327      *
328      * @param InputOption $option
329      * @return array|false
330      */
331     protected function completeOption(InputOption $option)
332     {
333         if ($helper = $this->getCompletionHelper($option->getName(), Completion::TYPE_OPTION)) {
334             return $helper->run();
335         }
336
337         if ($this->command instanceof CompletionAwareInterface) {
338             return $this->command->completeOptionValues($option->getName(), $this->context);
339         }
340
341         return false;
342     }
343
344     /**
345      * Step through the command line to determine which word positions represent which argument values
346      *
347      * The word indexes of argument values are found by eliminating words that are known to not be arguments (options,
348      * option values, and command names). Any word that doesn't match for elimination is assumed to be an argument value,
349      *
350      * @param InputArgument[] $argumentDefinitions
351      * @return array as [argument name => word index on command line]
352      */
353     protected function mapArgumentsToWords($argumentDefinitions)
354     {
355         $argumentPositions = array();
356         $argumentNumber = 0;
357         $previousWord = null;
358         $argumentNames = array_keys($argumentDefinitions);
359
360         // Build a list of option values to filter out
361         $optionsWithArgs = $this->getOptionWordsWithValues();
362
363         foreach ($this->context->getWords() as $wordIndex => $word) {
364             // Skip program name, command name, options, and option values
365             if ($wordIndex < 2
366                 || ($word && '-' === $word[0])
367                 || in_array($previousWord, $optionsWithArgs)) {
368                 $previousWord = $word;
369                 continue;
370             } else {
371                 $previousWord = $word;
372             }
373
374             // If argument n exists, pair that argument's name with the current word
375             if (isset($argumentNames[$argumentNumber])) {
376                 $argumentPositions[$wordIndex] = $argumentNames[$argumentNumber];
377             }
378
379             $argumentNumber++;
380         }
381
382         return $argumentPositions;
383     }
384
385     /**
386      * Build a list of option words/flags that will have a value after them
387      * Options are returned in the format they appear as on the command line.
388      *
389      * @return string[] - eg. ['--myoption', '-m', ... ]
390      */
391     protected function getOptionWordsWithValues()
392     {
393         $strings = array();
394
395         foreach ($this->getAllOptions() as $option) {
396             if ($option->isValueRequired()) {
397                 $strings[] = '--' . $option->getName();
398
399                 if ($option->getShortcut()) {
400                     $strings[] = '-' . $option->getShortcut();
401                 }
402             }
403         }
404
405         return $strings;
406     }
407
408     /**
409      * Filter out results that don't match the current word on the command line
410      *
411      * @param string[] $array
412      * @return string[]
413      */
414     protected function filterResults(array $array)
415     {
416         $curWord = $this->context->getCurrentWord();
417
418         return array_filter($array, function($val) use ($curWord) {
419             return fnmatch($curWord.'*', $val);
420         });
421     }
422
423     /**
424      * Get the combined options of the application and entered command
425      *
426      * @return InputOption[]
427      */
428     protected function getAllOptions()
429     {
430         if (!$this->command) {
431             return $this->application->getDefinition()->getOptions();
432         }
433
434         return array_merge(
435             $this->command->getNativeDefinition()->getOptions(),
436             $this->application->getDefinition()->getOptions()
437         );
438     }
439
440     /**
441      * Get command names available for completion
442      *
443      * Filters out hidden commands where supported.
444      *
445      * @return string[]
446      */
447     protected function getCommandNames()
448     {
449         // Command::Hidden isn't supported before Symfony Console 3.2.0
450         // We don't complete hidden command names as these are intended to be private
451         if (method_exists('\Symfony\Component\Console\Command\Command', 'isHidden')) {
452             $commands = array();
453
454             foreach ($this->application->all() as $name => $command) {
455                 if (!$command->isHidden()) {
456                     $commands[] = $name;
457                 }
458             }
459
460             return $commands;
461
462         } else {
463
464             // Fallback for compatibility with Symfony Console < 3.2.0
465             // This was the behaviour prior to pull #75
466             $commands = $this->application->all();
467             unset($commands['_completion']);
468
469             return array_keys($commands);
470         }
471     }
472 }