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