Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/3rdparty.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoas Schilling <coding@schilljs.com>2016-09-21 01:03:15 +0300
committerJoas Schilling <coding@schilljs.com>2016-09-21 01:03:15 +0300
commitb6ef8a42ae8c7d4d69c6acdf919072a7989648d7 (patch)
treec250924af2303df2e4d99e0b46a1117d40a0d108 /stecman
parentf5555fef8e80d8380efb44dc8b7622a1de573c15 (diff)
Add stecman/symfony-console-completion
Diffstat (limited to 'stecman')
-rw-r--r--stecman/symfony-console-completion/LICENCE21
-rw-r--r--stecman/symfony-console-completion/src/Completion.php180
-rw-r--r--stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php27
-rw-r--r--stecman/symfony-console-completion/src/Completion/CompletionInterface.php48
-rw-r--r--stecman/symfony-console-completion/src/Completion/ShellPathCompletion.php65
-rw-r--r--stecman/symfony-console-completion/src/CompletionCommand.php144
-rw-r--r--stecman/symfony-console-completion/src/CompletionContext.php256
-rw-r--r--stecman/symfony-console-completion/src/CompletionHandler.php445
-rw-r--r--stecman/symfony-console-completion/src/EnvironmentCompletionContext.php46
-rw-r--r--stecman/symfony-console-completion/src/HookFactory.php207
10 files changed, 1439 insertions, 0 deletions
diff --git a/stecman/symfony-console-completion/LICENCE b/stecman/symfony-console-completion/LICENCE
new file mode 100644
index 00000000..8f8e82c0
--- /dev/null
+++ b/stecman/symfony-console-completion/LICENCE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Stephen Holdaway
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. \ No newline at end of file
diff --git a/stecman/symfony-console-completion/src/Completion.php b/stecman/symfony-console-completion/src/Completion.php
new file mode 100644
index 00000000..f5adb45b
--- /dev/null
+++ b/stecman/symfony-console-completion/src/Completion.php
@@ -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);
+ }
+}
diff --git a/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php b/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php
new file mode 100644
index 00000000..20963cb8
--- /dev/null
+++ b/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php
@@ -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);
+}
diff --git a/stecman/symfony-console-completion/src/Completion/CompletionInterface.php b/stecman/symfony-console-completion/src/Completion/CompletionInterface.php
new file mode 100644
index 00000000..4f1ca05a
--- /dev/null
+++ b/stecman/symfony-console-completion/src/Completion/CompletionInterface.php
@@ -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();
+}
diff --git a/stecman/symfony-console-completion/src/Completion/ShellPathCompletion.php b/stecman/symfony-console-completion/src/Completion/ShellPathCompletion.php
new file mode 100644
index 00000000..c8f92296
--- /dev/null
+++ b/stecman/symfony-console-completion/src/Completion/ShellPathCompletion.php
@@ -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);
+ }
+}
diff --git a/stecman/symfony-console-completion/src/CompletionCommand.php b/stecman/symfony-console-completion/src/CompletionCommand.php
new file mode 100644
index 00000000..b51666a1
--- /dev/null
+++ b/stecman/symfony-console-completion/src/CompletionCommand.php
@@ -0,0 +1,144 @@
+<?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
+ );
+ }
+
+ /**
+ * {@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');
+
+ // When completing for multiple apps having absolute path in the alias doesn't make sense.
+ if (!$alias && $multiple) {
+ $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.'
+ ),
+ ));
+ }
+}
diff --git a/stecman/symfony-console-completion/src/CompletionContext.php b/stecman/symfony-console-completion/src/CompletionContext.php
new file mode 100644
index 00000000..60292534
--- /dev/null
+++ b/stecman/symfony-console-completion/src/CompletionContext.php
@@ -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;
+ }
+}
diff --git a/stecman/symfony-console-completion/src/CompletionHandler.php b/stecman/symfony-console-completion/src/CompletionHandler.php
new file mode 100644
index 00000000..531fae33
--- /dev/null
+++ b/stecman/symfony-console-completion/src/CompletionHandler.php
@@ -0,0 +1,445 @@
+<?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;
+
+ $this->addHandler(
+ new Completion(
+ 'help',
+ 'command_name',
+ Completion::TYPE_ARGUMENT,
+ array_keys($application->all())
+ )
+ );
+
+ $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)) {
+ $commands = $this->application->all();
+ $names = array_keys($commands);
+
+ if ($key = array_search('_completion', $names)) {
+ unset($names[$key]);
+ }
+
+ return $names;
+ }
+
+ 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()
+ );
+ }
+}
diff --git a/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php b/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php
new file mode 100644
index 00000000..04027ce1
--- /dev/null
+++ b/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php
@@ -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;
+ }
+}
diff --git a/stecman/symfony-console-completion/src/HookFactory.php b/stecman/symfony-console-completion/src/HookFactory.php
new file mode 100644
index 00000000..19601e8b
--- /dev/null
+++ b/stecman/symfony-console-completion/src/HookFactory.php
@@ -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);
+ }
+}