application = $application; $this->context = $context; // Set up completions for commands that are built-into Application $this->addHandler( new Completion( 'help', 'command_name', Completion::TYPE_ARGUMENT, $this->getCommandNames() ) ); $this->addHandler( new Completion( 'list', 'namespace', Completion::TYPE_ARGUMENT, $application->getNamespaces() ) ); } public function setContext(CompletionContext $context) { $this->context = $context; } /** * @return CompletionContext */ public function getContext() { return $this->context; } /** * @param CompletionInterface[] $array */ public function addHandlers(array $array) { $this->helpers = array_merge($this->helpers, $array); } /** * @param CompletionInterface $helper */ public function addHandler(CompletionInterface $helper) { $this->helpers[] = $helper; } /** * Do the actual completion, returning an array of strings to provide to the parent shell's completion system * * @throws \RuntimeException * @return string[] */ public function runCompletion() { if (!$this->context) { throw new \RuntimeException('A CompletionContext must be set before requesting completion.'); } $cmdName = $this->getInput()->getFirstArgument(); try { $this->command = $this->application->find($cmdName); } catch (\InvalidArgumentException $e) { // Exception thrown, when multiple or none commands are found. } $process = array( 'completeForOptionValues', 'completeForOptionShortcuts', 'completeForOptionShortcutValues', 'completeForOptions', 'completeForCommandName', 'completeForCommandArguments' ); foreach ($process as $methodName) { $result = $this->{$methodName}(); if (false !== $result) { // Return the result of the first completion mode that matches return $this->filterResults((array) $result); } } return array(); } /** * Get an InputInterface representation of the completion context * * @return ArrayInput */ public function getInput() { // Filter the command line content to suit ArrayInput $words = $this->context->getWords(); array_shift($words); $words = array_filter($words); return new ArrayInput($words); } /** * Attempt to complete the current word as a long-form option (--my-option) * * @return array|false */ protected function completeForOptions() { $word = $this->context->getCurrentWord(); if (substr($word, 0, 2) === '--') { $options = array(); foreach ($this->getAllOptions() as $opt) { $options[] = '--'.$opt->getName(); } return $options; } return false; } /** * Attempt to complete the current word as an option shortcut. * * If the shortcut exists it will be completed, but a list of possible shortcuts is never returned for completion. * * @return array|false */ protected function completeForOptionShortcuts() { $word = $this->context->getCurrentWord(); if (strpos($word, '-') === 0 && strlen($word) == 2) { $definition = $this->command ? $this->command->getNativeDefinition() : $this->application->getDefinition(); if ($definition->hasShortcut(substr($word, 1))) { return array($word); } } return false; } /** * Attempt to complete the current word as the value of an option shortcut * * @return array|false */ protected function completeForOptionShortcutValues() { $wordIndex = $this->context->getWordIndex(); if ($this->command && $wordIndex > 1) { $left = $this->context->getWordAtIndex($wordIndex - 1); // Complete short options if ($left[0] == '-' && strlen($left) == 2) { $shortcut = substr($left, 1); $def = $this->command->getNativeDefinition(); if (!$def->hasShortcut($shortcut)) { return false; } $opt = $def->getOptionForShortcut($shortcut); if ($opt->isValueRequired() || $opt->isValueOptional()) { return $this->completeOption($opt); } } } return false; } /** * Attemp to complete the current word as the value of a long-form option * * @return array|false */ protected function completeForOptionValues() { $wordIndex = $this->context->getWordIndex(); if ($this->command && $wordIndex > 1) { $left = $this->context->getWordAtIndex($wordIndex - 1); if (strpos($left, '--') === 0) { $name = substr($left, 2); $def = $this->command->getNativeDefinition(); if (!$def->hasOption($name)) { return false; } $opt = $def->getOption($name); if ($opt->isValueRequired() || $opt->isValueOptional()) { return $this->completeOption($opt); } } } return false; } /** * Attempt to complete the current word as a command name * * @return array|false */ protected function completeForCommandName() { if (!$this->command || (count($this->context->getWords()) == 2 && $this->context->getWordIndex() == 1)) { return $this->getCommandNames(); } return false; } /** * Attempt to complete the current word as a command argument value * * @see Symfony\Component\Console\Input\InputArgument * @return array|false */ protected function completeForCommandArguments() { if (!$this->command || strpos($this->context->getCurrentWord(), '-') === 0) { return false; } $definition = $this->command->getNativeDefinition(); $argWords = $this->mapArgumentsToWords($definition->getArguments()); $wordIndex = $this->context->getWordIndex(); if (isset($argWords[$wordIndex])) { $name = $argWords[$wordIndex]; } elseif (!empty($argWords) && $definition->getArgument(end($argWords))->isArray()) { $name = end($argWords); } else { return false; } if ($helper = $this->getCompletionHelper($name, Completion::TYPE_ARGUMENT)) { return $helper->run(); } if ($this->command instanceof CompletionAwareInterface) { return $this->command->completeArgumentValues($name, $this->context); } return false; } /** * Find a CompletionInterface that matches the current command, target name, and target type * * @param string $name * @param string $type * @return CompletionInterface|null */ protected function getCompletionHelper($name, $type) { foreach ($this->helpers as $helper) { if ($helper->getType() != $type && $helper->getType() != CompletionInterface::ALL_TYPES) { continue; } if ($helper->getCommandName() == CompletionInterface::ALL_COMMANDS || $helper->getCommandName() == $this->command->getName()) { if ($helper->getTargetName() == $name) { return $helper; } } } return null; } /** * Complete the value for the given option if a value completion is availble * * @param InputOption $option * @return array|false */ protected function completeOption(InputOption $option) { if ($helper = $this->getCompletionHelper($option->getName(), Completion::TYPE_OPTION)) { return $helper->run(); } if ($this->command instanceof CompletionAwareInterface) { return $this->command->completeOptionValues($option->getName(), $this->context); } return false; } /** * Step through the command line to determine which word positions represent which argument values * * The word indexes of argument values are found by eliminating words that are known to not be arguments (options, * option values, and command names). Any word that doesn't match for elimination is assumed to be an argument value, * * @param InputArgument[] $argumentDefinitions * @return array as [argument name => word index on command line] */ protected function mapArgumentsToWords($argumentDefinitions) { $argumentPositions = array(); $argumentNumber = 0; $previousWord = null; $argumentNames = array_keys($argumentDefinitions); // Build a list of option values to filter out $optionsWithArgs = $this->getOptionWordsWithValues(); foreach ($this->context->getWords() as $wordIndex => $word) { // Skip program name, command name, options, and option values if ($wordIndex < 2 || ($word && '-' === $word[0]) || in_array($previousWord, $optionsWithArgs)) { $previousWord = $word; continue; } else { $previousWord = $word; } // If argument n exists, pair that argument's name with the current word if (isset($argumentNames[$argumentNumber])) { $argumentPositions[$wordIndex] = $argumentNames[$argumentNumber]; } $argumentNumber++; } return $argumentPositions; } /** * Build a list of option words/flags that will have a value after them * Options are returned in the format they appear as on the command line. * * @return string[] - eg. ['--myoption', '-m', ... ] */ protected function getOptionWordsWithValues() { $strings = array(); foreach ($this->getAllOptions() as $option) { if ($option->isValueRequired()) { $strings[] = '--' . $option->getName(); if ($option->getShortcut()) { $strings[] = '-' . $option->getShortcut(); } } } return $strings; } /** * Filter out results that don't match the current word on the command line * * @param string[] $array * @return string[] */ protected function filterResults(array $array) { $curWord = $this->context->getCurrentWord(); return array_filter($array, function($val) use ($curWord) { return fnmatch($curWord.'*', $val); }); } /** * Get the combined options of the application and entered command * * @return InputOption[] */ protected function getAllOptions() { if (!$this->command) { return $this->application->getDefinition()->getOptions(); } return array_merge( $this->command->getNativeDefinition()->getOptions(), $this->application->getDefinition()->getOptions() ); } /** * Get command names available for completion * * Filters out hidden commands where supported. * * @return string[] */ protected function getCommandNames() { // Command::Hidden isn't supported before Symfony Console 3.2.0 // We don't complete hidden command names as these are intended to be private if (method_exists('\Symfony\Component\Console\Command\Command', 'isHidden')) { $commands = array(); foreach ($this->application->all() as $name => $command) { if (!$command->isHidden()) { $commands[] = $name; } } return $commands; } else { // Fallback for compatibility with Symfony Console < 3.2.0 // This was the behaviour prior to pull #75 $commands = $this->application->all(); unset($commands['_completion']); return array_keys($commands); } } }