Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -0,0 +1,180 @@
<?php
namespace Stecman\Component\Symfony\Console\BashCompletion;
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionInterface;
class Completion implements CompletionInterface
{
/**
* The type of input (option/argument) the completion should be run for
*
* @see CompletionInterface::ALL_TYPES
* @var string
*/
protected $type;
/**
* The command name the completion should be run for
*
* @see CompletionInterface::ALL_COMMANDS
* @var string|null
*/
protected $commandName;
/**
* The option/argument name the completion should be run for
*
* @var string
*/
protected $targetName;
/**
* Array of values to return, or a callback to generate completion results with
* The callback can be in any form accepted by call_user_func.
*
* @var callable|array
*/
protected $completion;
/**
* Create a Completion with the command name set to CompletionInterface::ALL_COMMANDS
*
* @deprecated - This will be removed in 1.0.0 as it is redundant and isn't any more concise than what it implements.
*
* @param string $targetName
* @param string $type
* @param array|callable $completion
* @return Completion
*/
public static function makeGlobalHandler($targetName, $type, $completion)
{
return new Completion(CompletionInterface::ALL_COMMANDS, $targetName, $type, $completion);
}
/**
* @param string $commandName
* @param string $targetName
* @param string $type
* @param array|callable $completion
*/
public function __construct($commandName, $targetName, $type, $completion)
{
$this->commandName = $commandName;
$this->targetName = $targetName;
$this->type = $type;
$this->completion = $completion;
}
/**
* Return the stored completion, or the results returned from the completion callback
*
* @return array
*/
public function run()
{
if ($this->isCallable()) {
return call_user_func($this->completion);
}
return $this->completion;
}
/**
* Get type of input (option/argument) the completion should be run for
*
* @see CompletionInterface::ALL_TYPES
* @return string|null
*/
public function getType()
{
return $this->type;
}
/**
* Set type of input (option/argument) the completion should be run for
*
* @see CompletionInterface::ALL_TYPES
* @param string|null $type
*/
public function setType($type)
{
$this->type = $type;
}
/**
* Get the command name the completion should be run for
*
* @see CompletionInterface::ALL_COMMANDS
* @return string|null
*/
public function getCommandName()
{
return $this->commandName;
}
/**
* Set the command name the completion should be run for
*
* @see CompletionInterface::ALL_COMMANDS
* @param string|null $commandName
*/
public function setCommandName($commandName)
{
$this->commandName = $commandName;
}
/**
* Set the option/argument name the completion should be run for
*
* @see setType()
* @return string
*/
public function getTargetName()
{
return $this->targetName;
}
/**
* Get the option/argument name the completion should be run for
*
* @see getType()
* @param string $targetName
*/
public function setTargetName($targetName)
{
$this->targetName = $targetName;
}
/**
* Return the array or callback configured for for the Completion
*
* @return array|callable
*/
public function getCompletion()
{
return $this->completion;
}
/**
* Set the array or callback to return/run when Completion is run
*
* @see run()
* @param array|callable $completion
*/
public function setCompletion($completion)
{
$this->completion = $completion;
}
/**
* Check if the configured completion value is a callback function
*
* @return bool
*/
public function isCallable()
{
return is_callable($this->completion);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
interface CompletionAwareInterface
{
/**
* Return possible values for the named option
*
* @param string $optionName
* @param CompletionContext $context
* @return array
*/
public function completeOptionValues($optionName, CompletionContext $context);
/**
* Return possible values for the named argument
*
* @param string $argumentName
* @param CompletionContext $context
* @return array
*/
public function completeArgumentValues($argumentName, CompletionContext $context);
}

View file

@ -0,0 +1,48 @@
<?php
namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;
interface CompletionInterface
{
// Sugar for indicating that a Completion should run for all command names and for all types
// Intended to avoid meaningless null parameters in the constructors of implementing classes
const ALL_COMMANDS = null;
const ALL_TYPES = null;
const TYPE_OPTION = 'option';
const TYPE_ARGUMENT = 'argument';
/**
* Return the type of input (option/argument) completion should be run for
*
* @see \Symfony\Component\Console\Command\Command::addArgument
* @see \Symfony\Component\Console\Command\Command::addOption
* @return string - one of the CompletionInterface::TYPE_* constants
*/
public function getType();
/**
* Return the name of the command completion should be run for
* If the return value is CompletionInterface::ALL_COMMANDS, the completion will be run for any command name
*
* @see \Symfony\Component\Console\Command\Command::setName
* @return string|null
*/
public function getCommandName();
/**
* Return the option/argument name the completion should be run for
* CompletionInterface::getType determines whether the target name refers to an option or an argument
*
* @return string
*/
public function getTargetName();
/**
* Execute the completion
*
* @return string[] - an array of possible completion values
*/
public function run();
}

View file

@ -0,0 +1,65 @@
<?php
namespace Stecman\Component\Symfony\Console\BashCompletion\Completion;
/**
* Shell Path Completion
*
* Defers completion to the calling shell's built-in path completion functionality.
*/
class ShellPathCompletion implements CompletionInterface
{
/**
* Exit code set up to trigger path completion in the completion hooks
* @see Stecman\Component\Symfony\Console\BashCompletion\HookFactory
*/
const PATH_COMPLETION_EXIT_CODE = 200;
protected $type;
protected $commandName;
protected $targetName;
public function __construct($commandName, $targetName, $type)
{
$this->commandName = $commandName;
$this->targetName = $targetName;
$this->type = $type;
}
/**
* @inheritdoc
*/
public function getType()
{
return $this->type;
}
/**
* @inheritdoc
*/
public function getCommandName()
{
return $this->commandName;
}
/**
* @inheritdoc
*/
public function getTargetName()
{
return $this->targetName;
}
/**
* Exit with a status code configured to defer completion to the shell
*
* @see \Stecman\Component\Symfony\Console\BashCompletion\HookFactory::$hooks
*/
public function run()
{
exit(self::PATH_COMPLETION_EXIT_CODE);
}
}

View file

@ -0,0 +1,149 @@
<?php
namespace Stecman\Component\Symfony\Console\BashCompletion;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CompletionCommand extends SymfonyCommand
{
/**
* @var CompletionHandler
*/
protected $handler;
protected function configure()
{
$this
->setName('_completion')
->setDefinition($this->createDefinition())
->setDescription('BASH completion hook.')
->setHelp(<<<END
To enable BASH completion, run:
<comment>eval `[program] _completion -g`</comment>.
Or for an alias:
<comment>eval `[program] _completion -g -p [alias]`</comment>.
END
);
// Hide this command from listing if supported
// Command::setHidden() was not available before Symfony 3.2.0
if (method_exists($this, 'setHidden')) {
$this->setHidden(true);
}
}
/**
* {@inheritdoc}
*/
public function getNativeDefinition()
{
return $this->createDefinition();
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->handler = new CompletionHandler($this->getApplication());
$handler = $this->handler;
if ($input->getOption('generate-hook')) {
global $argv;
$program = $argv[0];
$factory = new HookFactory();
$alias = $input->getOption('program');
$multiple = (bool)$input->getOption('multiple');
if (!$alias) {
$alias = basename($program);
}
$hook = $factory->generateHook(
$input->getOption('shell-type') ?: $this->getShellType(),
$program,
$alias,
$multiple
);
$output->write($hook, true);
} else {
$handler->setContext(new EnvironmentCompletionContext());
$output->write($this->runCompletion(), true);
}
}
/**
* Run the completion handler and return a filtered list of results
*
* @deprecated - This will be removed in 1.0.0 in favour of CompletionCommand::configureCompletion
*
* @return string[]
*/
protected function runCompletion()
{
$this->configureCompletion($this->handler);
return $this->handler->runCompletion();
}
/**
* Configure the CompletionHandler instance before it is run
*
* @param CompletionHandler $handler
*/
protected function configureCompletion(CompletionHandler $handler)
{
// Override this method to configure custom value completions
}
/**
* Determine the shell type for use with HookFactory
*
* @return string
*/
protected function getShellType()
{
if (!getenv('SHELL')) {
throw new \RuntimeException('Could not read SHELL environment variable. Please specify your shell type using the --shell-type option.');
}
return basename(getenv('SHELL'));
}
protected function createDefinition()
{
return new InputDefinition(array(
new InputOption(
'generate-hook',
'g',
InputOption::VALUE_NONE,
'Generate BASH code that sets up completion for this application.'
),
new InputOption(
'program',
'p',
InputOption::VALUE_REQUIRED,
"Program name that should trigger completion\n<comment>(defaults to the absolute application path)</comment>."
),
new InputOption(
'multiple',
'm',
InputOption::VALUE_NONE,
"Generated hook can be used for multiple applications."
),
new InputOption(
'shell-type',
null,
InputOption::VALUE_OPTIONAL,
'Set the shell type (zsh or bash). Otherwise this is determined automatically.'
),
));
}
}

View file

@ -0,0 +1,256 @@
<?php
namespace Stecman\Component\Symfony\Console\BashCompletion;
/**
* Command line context for completion
*
* Represents the current state of the command line that is being completed
*/
class CompletionContext
{
/**
* The current contents of the command line as a single string
*
* Bash equivalent: COMP_LINE
*
* @var string
*/
protected $commandLine;
/**
* The index of the user's cursor relative to the start of the command line.
*
* If the current cursor position is at the end of the current command,
* the value of this variable is equal to the length of $this->commandLine
*
* Bash equivalent: COMP_POINT
*
* @var int
*/
protected $charIndex = 0;
/**
* An array containing the individual words in the current command line.
*
* This is not set until $this->splitCommand() is called, when it is populated by
* $commandLine exploded by $wordBreaks
*
* Bash equivalent: COMP_WORDS
*
* @var array|null
*/
protected $words = null;
/**
* The index in $this->words containing the word at the current cursor position.
*
* This is not set until $this->splitCommand() is called.
*
* Bash equivalent: COMP_CWORD
*
* @var int|null
*/
protected $wordIndex = null;
/**
* Characters that $this->commandLine should be split on to get a list of individual words
*
* Bash equivalent: COMP_WORDBREAKS
*
* @var string
*/
protected $wordBreaks = "'\"()= \t\n";
/**
* Set the whole contents of the command line as a string
*
* @param string $commandLine
*/
public function setCommandLine($commandLine)
{
$this->commandLine = $commandLine;
$this->reset();
}
/**
* Return the current command line verbatim as a string
*
* @return string
*/
public function getCommandLine()
{
return $this->commandLine;
}
/**
* Return the word from the command line that the cursor is currently in
*
* Most of the time this will be a partial word. If the cursor has a space before it,
* this will return an empty string, indicating a new word.
*
* @return string
*/
public function getCurrentWord()
{
if (isset($this->words[$this->wordIndex])) {
return $this->words[$this->wordIndex];
}
return '';
}
/**
* Return a word by index from the command line
*
* @see $words, $wordBreaks
* @param int $index
* @return string
*/
public function getWordAtIndex($index)
{
if (isset($this->words[$index])) {
return $this->words[$index];
}
return '';
}
/**
* Get the contents of the command line, exploded into words based on the configured word break characters
*
* @see $wordBreaks, setWordBreaks
* @return array
*/
public function getWords()
{
if ($this->words === null) {
$this->splitCommand();
}
return $this->words;
}
/**
* Get the index of the word the cursor is currently in
*
* @see getWords, getCurrentWord
* @return int
*/
public function getWordIndex()
{
if ($this->wordIndex === null) {
$this->splitCommand();
}
return $this->wordIndex;
}
/**
* Get the character index of the user's cursor on the command line
*
* This is in the context of the full command line string, so includes word break characters.
* Note that some shells can only provide an approximation for character index. Under ZSH for
* example, this will always be the character at the start of the current word.
*
* @return int
*/
public function getCharIndex()
{
return $this->charIndex;
}
/**
* Set the cursor position as a character index relative to the start of the command line
*
* @param int $index
*/
public function setCharIndex($index)
{
$this->charIndex = $index;
$this->reset();
}
/**
* Set characters to use as split points when breaking the command line into words
*
* This defaults to a sane value based on BASH's word break characters and shouldn't
* need to be changed unless your completions contain the default word break characters.
*
* @see wordBreaks
* @param string $charList - a single string containing all of the characters to break words on
*/
public function setWordBreaks($charList)
{
$this->wordBreaks = $charList;
$this->reset();
}
/**
* Split the command line into words using the configured word break characters
*
* @return string[]
*/
protected function splitCommand()
{
$this->words = array();
$this->wordIndex = null;
$cursor = 0;
$breaks = preg_quote($this->wordBreaks);
if (!preg_match_all("/([^$breaks]*)([$breaks]*)/", $this->commandLine, $matches)) {
return;
}
// Groups:
// 1: Word
// 2: Break characters
foreach ($matches[0] as $index => $wholeMatch) {
// Determine which word the cursor is in
$cursor += strlen($wholeMatch);
$word = $matches[1][$index];
$breaks = $matches[2][$index];
if ($this->wordIndex === null && $cursor >= $this->charIndex) {
$this->wordIndex = $index;
// Find the user's cursor position relative to the end of this word
// The end of the word is the internal cursor minus any break characters that were captured
$cursorWordOffset = $this->charIndex - ($cursor - strlen($breaks));
if ($cursorWordOffset < 0) {
// Cursor is inside the word - truncate the word at the cursor
// (This emulates normal BASH completion behaviour I've observed, though I'm not entirely sure if it's useful)
$word = substr($word, 0, strlen($word) + $cursorWordOffset);
} elseif ($cursorWordOffset > 0) {
// Cursor is in the break-space after a word
// Push an empty word at the cursor to allow completion of new terms at the cursor, ignoring words ahead
$this->wordIndex++;
$this->words[] = $word;
$this->words[] = '';
continue;
}
}
if ($word !== '') {
$this->words[] = $word;
}
}
if ($this->wordIndex > count($this->words) - 1) {
$this->wordIndex = count($this->words) - 1;
}
}
/**
* Reset the computed words so that $this->splitWords is forced to run again
*/
protected function reset()
{
$this->words = null;
$this->wordIndex = null;
}
}

View file

@ -0,0 +1,472 @@
<?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();
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.');
}
$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);
}
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Stecman\Component\Symfony\Console\BashCompletion;
class EnvironmentCompletionContext extends CompletionContext
{
/**
* Set up completion context from the environment variables set by the parent shell
*/
public function __construct()
{
$this->commandLine = getenv('CMDLINE_CONTENTS');
$this->charIndex = intval(getenv('CMDLINE_CURSOR_INDEX'));
if ($this->commandLine === false) {
$message = 'Failed to configure from environment; Environment var CMDLINE_CONTENTS not set.';
if (getenv('COMP_LINE')) {
$message .= "\n\nYou appear to be attempting completion using an out-dated hook. If you've just updated,"
. " you probably need to reinitialise the completion shell hook by reloading your shell"
. " profile or starting a new shell session. If you are using a hard-coded (rather than generated)"
. " hook, you will need to update that function with the new environment variable names."
. "\n\nSee here for details: https://github.com/stecman/symfony-console-completion/issues/31";
}
throw new \RuntimeException($message);
}
}
/**
* Use the word break characters set by the parent shell.
*
* @throws \RuntimeException
*/
public function useWordBreaksFromEnvironment()
{
$breaks = getenv('CMDLINE_WORDBREAKS');
if (!$breaks) {
throw new \RuntimeException('Failed to read word breaks from environment; Environment var CMDLINE_WORDBREAKS not set');
}
$this->wordBreaks = $breaks;
}
}

View file

@ -0,0 +1,207 @@
<?php
namespace Stecman\Component\Symfony\Console\BashCompletion;
final class HookFactory
{
/**
* Hook scripts
*
* These are shell-specific scripts that pass required information from that shell's
* completion system to the interface of the completion command in this module.
*
* The following placeholders are replaced with their value at runtime:
*
* %%function_name%% - name of the generated shell function run for completion
* %%program_name%% - command name completion will be enabled for
* %%program_path%% - path to program the completion is for/generated by
* %%completion_command%% - command to be run to compute completions
*
* NOTE: Comments are stripped out by HookFactory::stripComments as eval reads
* input as a single line, causing it to break if comments are included.
* While comments work using `... | source /dev/stdin`, existing installations
* are likely using eval as it's been part of the instructions for a while.
*
* @var array
*/
protected static $hooks = array(
// BASH Hook
'bash' => <<<'END'
# BASH completion for %%program_path%%
function %%function_name%% {
# Copy BASH's completion variables to the ones the completion command expects
# These line up exactly as the library was originally designed for BASH
local CMDLINE_CONTENTS="$COMP_LINE"
local CMDLINE_CURSOR_INDEX="$COMP_POINT"
local CMDLINE_WORDBREAKS="$COMP_WORDBREAKS";
export CMDLINE_CONTENTS CMDLINE_CURSOR_INDEX CMDLINE_WORDBREAKS
local RESULT STATUS;
RESULT="$(%%completion_command%% </dev/null)";
STATUS=$?;
local cur mail_check_backup;
mail_check_backup=$MAILCHECK;
MAILCHECK=-1;
_get_comp_words_by_ref -n : cur;
# Check if shell provided path completion is requested
# @see Completion\ShellPathCompletion
if [ $STATUS -eq 200 ]; then
_filedir;
return 0;
# Bail out if PHP didn't exit cleanly
elif [ $STATUS -ne 0 ]; then
echo -e "$RESULT";
return $?;
fi;
COMPREPLY=(`compgen -W "$RESULT" -- $cur`);
__ltrim_colon_completions "$cur";
MAILCHECK=mail_check_backup;
};
if [ "$(type -t _get_comp_words_by_ref)" == "function" ]; then
complete -F %%function_name%% "%%program_name%%";
else
>&2 echo "Completion was not registered for %%program_name%%:";
>&2 echo "The 'bash-completion' package is required but doesn't appear to be installed.";
fi
END
// ZSH Hook
, 'zsh' => <<<'END'
# ZSH completion for %%program_path%%
function %%function_name%% {
local -x CMDLINE_CONTENTS="$words"
local -x CMDLINE_CURSOR_INDEX
(( CMDLINE_CURSOR_INDEX = ${#${(j. .)words[1,CURRENT]}} ))
local RESULT STATUS
RESULT=("${(@f)$( %%completion_command%% )}")
STATUS=$?;
# Check if shell provided path completion is requested
# @see Completion\ShellPathCompletion
if [ $STATUS -eq 200 ]; then
_path_files;
return 0;
# Bail out if PHP didn't exit cleanly
elif [ $STATUS -ne 0 ]; then
echo -e "$RESULT";
return $?;
fi;
compadd -- $RESULT
};
compdef %%function_name%% "%%program_name%%";
END
);
/**
* Return the names of shells that have hooks
*
* @return string[]
*/
public static function getShellTypes()
{
return array_keys(self::$hooks);
}
/**
* Return a completion hook for the specified shell type
*
* @param string $type - a key from self::$hooks
* @param string $programPath
* @param string $programName
* @param bool $multiple
*
* @return string
*/
public function generateHook($type, $programPath, $programName = null, $multiple = false)
{
if (!isset(self::$hooks[$type])) {
throw new \RuntimeException(sprintf(
"Cannot generate hook for unknown shell type '%s'. Available hooks are: %s",
$type,
implode(', ', self::getShellTypes())
));
}
// Use the program path if an alias/name is not given
$programName = $programName ?: $programPath;
if ($multiple) {
$completionCommand = '$1 _completion';
} else {
$completionCommand = $programPath . ' _completion';
}
return str_replace(
array(
'%%function_name%%',
'%%program_name%%',
'%%program_path%%',
'%%completion_command%%',
),
array(
$this->generateFunctionName($programPath, $programName),
$programName,
$programPath,
$completionCommand
),
$this->stripComments(self::$hooks[$type])
);
}
/**
* Generate a function name that is unlikely to conflict with other generated function names in the same shell
*/
protected function generateFunctionName($programPath, $programName)
{
return sprintf(
'_%s_%s_complete',
$this->sanitiseForFunctionName(basename($programName)),
substr(md5($programPath), 0, 16)
);
}
/**
* Make a string safe for use as a shell function name
*
* @param string $name
* @return string
*/
protected function sanitiseForFunctionName($name)
{
$name = str_replace('-', '_', $name);
return preg_replace('/[^A-Za-z0-9_]+/', '', $name);
}
/**
* Strip '#' style comments from a string
*
* BASH's eval doesn't work with comments as it removes line breaks, so comments have to be stripped out
* for this method of sourcing the hook to work. Eval seems to be the most reliable method of getting a
* hook into a shell, so while it would be nice to render comments, this stripping is required for now.
*
* @param string $script
* @return string
*/
protected function stripComments($script)
{
return preg_replace('/(^\s*\#.*$)/m', '', $script);
}
}