Files
popsu-d9/old.vendor/stecman/symfony-console-completion/src/CompletionHandler.php
2022-04-27 11:30:43 +02:00

518 lines
15 KiB
PHP

<?php
namespace Stecman\Component\Symfony\Console\BashCompletion;
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class CompletionHandler
{
/**
* Application to complete for
* @var \Symfony\Component\Console\Application
*/
protected $application;
/**
* @var Command
*/
protected $command;
/**
* @var CompletionContext
*/
protected $context;
/**
* Array of completion helpers.
* @var CompletionInterface[]
*/
protected $helpers = array();
/**
* Index the command name was detected at
* @var int
*/
private $commandWordIndex;
public function __construct(Application $application, CompletionContext $context = null)
{
$this->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.');
}
// Set the command to query options and arugments from
$this->command = $this->detectCommand();
$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
*
* @deprecated Incorrectly uses the ArrayInput API and is no longer needed.
* This will be removed in the next major version.
*
* @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 || $this->context->getWordIndex() == $this->commandWordIndex) {
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 == 0
|| $wordIndex === $this->commandWordIndex
|| ($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);
}
}
/**
* Find the current command name in the command-line
*
* Note this only cares about flag-type options. Options with values cannot
* appear before a command name in Symfony Console application.
*
* @return Command|null
*/
private function detectCommand()
{
// Always skip the first word (program name)
$skipNext = true;
foreach ($this->context->getWords() as $index => $word) {
// Skip word if flagged
if ($skipNext) {
$skipNext = false;
continue;
}
// Skip empty words and words that look like options
if (strlen($word) == 0 || $word[0] === '-') {
continue;
}
// Return the first unambiguous match to argument-like words
try {
$cmd = $this->application->find($word);
$this->commandWordIndex = $index;
return $cmd;
} catch (\InvalidArgumentException $e) {
// Exception thrown, when multiple or no commands are found.
}
}
// No command found
return null;
}
}