3 namespace Stecman\Component\Symfony\Console\BashCompletion;
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;
13 class CompletionHandler
16 * Application to complete for
17 * @var \Symfony\Component\Console\Application
19 protected $application;
27 * @var CompletionContext
32 * Array of completion helpers.
33 * @var CompletionInterface[]
35 protected $helpers = array();
37 public function __construct(Application $application, CompletionContext $context = null)
39 $this->application = $application;
40 $this->context = $context;
46 Completion::TYPE_ARGUMENT,
47 array_keys($application->all())
55 Completion::TYPE_ARGUMENT,
56 $application->getNamespaces()
61 public function setContext(CompletionContext $context)
63 $this->context = $context;
67 * @return CompletionContext
69 public function getContext()
71 return $this->context;
75 * @param CompletionInterface[] $array
77 public function addHandlers(array $array)
79 $this->helpers = array_merge($this->helpers, $array);
83 * @param CompletionInterface $helper
85 public function addHandler(CompletionInterface $helper)
87 $this->helpers[] = $helper;
91 * Do the actual completion, returning an array of strings to provide to the parent shell's completion system
93 * @throws \RuntimeException
96 public function runCompletion()
98 if (!$this->context) {
99 throw new \RuntimeException('A CompletionContext must be set before requesting completion.');
102 $cmdName = $this->getInput()->getFirstArgument();
105 $this->command = $this->application->find($cmdName);
106 } catch (\InvalidArgumentException $e) {
107 // Exception thrown, when multiple or none commands are found.
111 'completeForOptionValues',
112 'completeForOptionShortcuts',
113 'completeForOptionShortcutValues',
114 'completeForOptions',
115 'completeForCommandName',
116 'completeForCommandArguments'
119 foreach ($process as $methodName) {
120 $result = $this->{$methodName}();
122 if (false !== $result) {
123 // Return the result of the first completion mode that matches
124 return $this->filterResults((array) $result);
132 * Get an InputInterface representation of the completion context
136 public function getInput()
138 // Filter the command line content to suit ArrayInput
139 $words = $this->context->getWords();
141 $words = array_filter($words);
143 return new ArrayInput($words);
147 * Attempt to complete the current word as a long-form option (--my-option)
149 * @return array|false
151 protected function completeForOptions()
153 $word = $this->context->getCurrentWord();
155 if (substr($word, 0, 2) === '--') {
158 foreach ($this->getAllOptions() as $opt) {
159 $options[] = '--'.$opt->getName();
169 * Attempt to complete the current word as an option shortcut.
171 * If the shortcut exists it will be completed, but a list of possible shortcuts is never returned for completion.
173 * @return array|false
175 protected function completeForOptionShortcuts()
177 $word = $this->context->getCurrentWord();
179 if (strpos($word, '-') === 0 && strlen($word) == 2) {
180 $definition = $this->command ? $this->command->getNativeDefinition() : $this->application->getDefinition();
182 if ($definition->hasShortcut(substr($word, 1))) {
191 * Attempt to complete the current word as the value of an option shortcut
193 * @return array|false
195 protected function completeForOptionShortcutValues()
197 $wordIndex = $this->context->getWordIndex();
199 if ($this->command && $wordIndex > 1) {
200 $left = $this->context->getWordAtIndex($wordIndex - 1);
202 // Complete short options
203 if ($left[0] == '-' && strlen($left) == 2) {
204 $shortcut = substr($left, 1);
205 $def = $this->command->getNativeDefinition();
207 if (!$def->hasShortcut($shortcut)) {
211 $opt = $def->getOptionForShortcut($shortcut);
212 if ($opt->isValueRequired() || $opt->isValueOptional()) {
213 return $this->completeOption($opt);
222 * Attemp to complete the current word as the value of a long-form option
224 * @return array|false
226 protected function completeForOptionValues()
228 $wordIndex = $this->context->getWordIndex();
230 if ($this->command && $wordIndex > 1) {
231 $left = $this->context->getWordAtIndex($wordIndex - 1);
233 if (strpos($left, '--') === 0) {
234 $name = substr($left, 2);
235 $def = $this->command->getNativeDefinition();
237 if (!$def->hasOption($name)) {
241 $opt = $def->getOption($name);
242 if ($opt->isValueRequired() || $opt->isValueOptional()) {
243 return $this->completeOption($opt);
252 * Attempt to complete the current word as a command name
254 * @return array|false
256 protected function completeForCommandName()
258 if (!$this->command || (count($this->context->getWords()) == 2 && $this->context->getWordIndex() == 1)) {
259 $commands = $this->application->all();
260 $names = array_keys($commands);
262 if ($key = array_search('_completion', $names)) {
273 * Attempt to complete the current word as a command argument value
275 * @see Symfony\Component\Console\Input\InputArgument
276 * @return array|false
278 protected function completeForCommandArguments()
280 if (!$this->command || strpos($this->context->getCurrentWord(), '-') === 0) {
284 $definition = $this->command->getNativeDefinition();
285 $argWords = $this->mapArgumentsToWords($definition->getArguments());
286 $wordIndex = $this->context->getWordIndex();
288 if (isset($argWords[$wordIndex])) {
289 $name = $argWords[$wordIndex];
290 } elseif (!empty($argWords) && $definition->getArgument(end($argWords))->isArray()) {
291 $name = end($argWords);
296 if ($helper = $this->getCompletionHelper($name, Completion::TYPE_ARGUMENT)) {
297 return $helper->run();
300 if ($this->command instanceof CompletionAwareInterface) {
301 return $this->command->completeArgumentValues($name, $this->context);
308 * Find a CompletionInterface that matches the current command, target name, and target type
310 * @param string $name
311 * @param string $type
312 * @return CompletionInterface|null
314 protected function getCompletionHelper($name, $type)
316 foreach ($this->helpers as $helper) {
317 if ($helper->getType() != $type && $helper->getType() != CompletionInterface::ALL_TYPES) {
321 if ($helper->getCommandName() == CompletionInterface::ALL_COMMANDS || $helper->getCommandName() == $this->command->getName()) {
322 if ($helper->getTargetName() == $name) {
332 * Complete the value for the given option if a value completion is availble
334 * @param InputOption $option
335 * @return array|false
337 protected function completeOption(InputOption $option)
339 if ($helper = $this->getCompletionHelper($option->getName(), Completion::TYPE_OPTION)) {
340 return $helper->run();
343 if ($this->command instanceof CompletionAwareInterface) {
344 return $this->command->completeOptionValues($option->getName(), $this->context);
351 * Step through the command line to determine which word positions represent which argument values
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,
356 * @param InputArgument[] $argumentDefinitions
357 * @return array as [argument name => word index on command line]
359 protected function mapArgumentsToWords($argumentDefinitions)
361 $argumentPositions = array();
363 $previousWord = null;
364 $argumentNames = array_keys($argumentDefinitions);
366 // Build a list of option values to filter out
367 $optionsWithArgs = $this->getOptionWordsWithValues();
369 foreach ($this->context->getWords() as $wordIndex => $word) {
370 // Skip program name, command name, options, and option values
372 || ($word && '-' === $word[0])
373 || in_array($previousWord, $optionsWithArgs)) {
374 $previousWord = $word;
377 $previousWord = $word;
380 // If argument n exists, pair that argument's name with the current word
381 if (isset($argumentNames[$argumentNumber])) {
382 $argumentPositions[$wordIndex] = $argumentNames[$argumentNumber];
388 return $argumentPositions;
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.
395 * @return string[] - eg. ['--myoption', '-m', ... ]
397 protected function getOptionWordsWithValues()
401 foreach ($this->getAllOptions() as $option) {
402 if ($option->isValueRequired()) {
403 $strings[] = '--' . $option->getName();
405 if ($option->getShortcut()) {
406 $strings[] = '-' . $option->getShortcut();
415 * Filter out results that don't match the current word on the command line
417 * @param string[] $array
420 protected function filterResults(array $array)
422 $curWord = $this->context->getCurrentWord();
424 return array_filter($array, function($val) use ($curWord) {
425 return fnmatch($curWord.'*', $val);
430 * Get the combined options of the application and entered command
432 * @return InputOption[]
434 protected function getAllOptions()
436 if (!$this->command) {
437 return $this->application->getDefinition()->getOptions();
441 $this->command->getNativeDefinition()->getOptions(),
442 $this->application->getDefinition()->getOptions()