Обновление клиента (apps, 3rdparty, install)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
@@ -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);
|
||||
}
|
||||
+48
@@ -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();
|
||||
}
|
||||
+65
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?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
|
||||
$this->setHidden(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNativeDefinition(): InputDefinition
|
||||
{
|
||||
return $this->createDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore user-defined global options
|
||||
*
|
||||
* Any global options defined by user-code are meaningless to this command.
|
||||
* Options outside of the core defaults are ignored to avoid name and shortcut conflicts.
|
||||
*/
|
||||
public function mergeApplicationDefinition(bool $mergeArgs = true): void
|
||||
{
|
||||
// Get current application options
|
||||
$appDefinition = $this->getApplication()->getDefinition();
|
||||
$originalOptions = $appDefinition->getOptions();
|
||||
|
||||
// Temporarily replace application options with a filtered list
|
||||
$appDefinition->setOptions(
|
||||
$this->filterApplicationOptions($originalOptions)
|
||||
);
|
||||
|
||||
parent::mergeApplicationDefinition($mergeArgs);
|
||||
|
||||
// Restore original application options
|
||||
$appDefinition->setOptions($originalOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the passed list of options to the core defaults (if they exist)
|
||||
*
|
||||
* @param InputOption[] $appOptions
|
||||
* @return InputOption[]
|
||||
*/
|
||||
protected function filterApplicationOptions(array $appOptions)
|
||||
{
|
||||
return array_filter($appOptions, function(InputOption $option) {
|
||||
static $coreOptions = array(
|
||||
'help' => true,
|
||||
'quiet' => true,
|
||||
'verbose' => true,
|
||||
'version' => true,
|
||||
'ansi' => true,
|
||||
'no-ansi' => true,
|
||||
'no-interaction' => true,
|
||||
);
|
||||
|
||||
return isset($coreOptions[$option->getName()]);
|
||||
});
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$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());
|
||||
|
||||
// Get completion results
|
||||
$results = $this->runCompletion();
|
||||
|
||||
// Escape results for the current shell
|
||||
$shellType = $input->getOption('shell-type') ?: $this->getShellType();
|
||||
|
||||
foreach ($results as &$result) {
|
||||
$result = $this->escapeForShell($result, $shellType);
|
||||
}
|
||||
|
||||
$output->write($results, true);
|
||||
}
|
||||
|
||||
return SymfonyCommand::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape each completion result for the specified shell
|
||||
*
|
||||
* @param string $result - Completion results that should appear in the shell
|
||||
* @param string $shellType - Valid shell type from HookFactory
|
||||
* @return string
|
||||
*/
|
||||
protected function escapeForShell($result, $shellType)
|
||||
{
|
||||
switch ($shellType) {
|
||||
// BASH requires special escaping for multi-word and special character results
|
||||
// This emulates registering completion with`-o filenames`, without side-effects like dir name slashes
|
||||
case 'bash':
|
||||
$context = $this->handler->getContext();
|
||||
$wordStart = substr($context->getRawCurrentWord(), 0, 1);
|
||||
|
||||
if ($wordStart == "'") {
|
||||
// If the current word is single-quoted, escape any single quotes in the result
|
||||
$result = str_replace("'", "\\'", $result);
|
||||
} else if ($wordStart == '"') {
|
||||
// If the current word is double-quoted, escape any double quotes in the result
|
||||
$result = str_replace('"', '\\"', $result);
|
||||
} else {
|
||||
// Otherwise assume the string is unquoted and word breaks should be escaped
|
||||
$result = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $result);
|
||||
}
|
||||
|
||||
// Escape output to prevent special characters being lost when passing results to compgen
|
||||
return escapeshellarg($result);
|
||||
|
||||
// No transformation by default
|
||||
default:
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.'
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
<?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 of 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 string[]|null
|
||||
*/
|
||||
protected $words = null;
|
||||
|
||||
/**
|
||||
* Words from the currently command-line before quotes and escaping is processed
|
||||
*
|
||||
* This is indexed the same as $this->words, but in their raw input terms are in their input form, including
|
||||
* quotes and escaping.
|
||||
*
|
||||
* @var string[]|null
|
||||
*/
|
||||
protected $rawWords = 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 the unprocessed string for the word under the cursor
|
||||
*
|
||||
* This preserves any quotes and escaping that are present in the input command line.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRawCurrentWord()
|
||||
{
|
||||
if (isset($this->rawWords[$this->wordIndex])) {
|
||||
return $this->rawWords[$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 unprocessed/literal words from the command line
|
||||
*
|
||||
* This is indexed the same as getWords(), but preserves any quoting and escaping from the command line
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getRawWords()
|
||||
{
|
||||
if ($this->rawWords === null) {
|
||||
$this->splitCommand();
|
||||
}
|
||||
|
||||
return $this->rawWords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @deprecated This is becoming an internal setting that doesn't make sense to expose publicly.
|
||||
*
|
||||
* @see wordBreaks
|
||||
* @param string $charList - a single string containing all of the characters to break words on
|
||||
*/
|
||||
public function setWordBreaks($charList)
|
||||
{
|
||||
// Drop quotes from break characters - strings are handled separately to word breaks now
|
||||
$this->wordBreaks = str_replace(array('"', '\''), '', $charList);;
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the command line into words using the configured word break characters
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function splitCommand()
|
||||
{
|
||||
$tokens = $this->tokenizeString($this->commandLine);
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if ($token['type'] != 'break') {
|
||||
$this->words[] = $this->getTokenValue($token);
|
||||
$this->rawWords[] = $token['value'];
|
||||
}
|
||||
|
||||
// Determine which word index the cursor is inside once we reach it's offset
|
||||
if ($this->wordIndex === null && $this->charIndex <= $token['offsetEnd']) {
|
||||
$this->wordIndex = count($this->words) - 1;
|
||||
|
||||
if ($token['type'] == 'break') {
|
||||
// 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[] = '';
|
||||
$this->rawWords[] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->charIndex < $token['offsetEnd']) {
|
||||
// Cursor is inside the current word - truncate the word at the cursor to complete on
|
||||
// This emulates BASH completion's behaviour with COMP_CWORD
|
||||
|
||||
// Create a copy of the token with its value truncated
|
||||
$truncatedToken = $token;
|
||||
$relativeOffset = $this->charIndex - $token['offset'];
|
||||
$truncatedToken['value'] = substr($token['value'], 0, $relativeOffset);
|
||||
|
||||
// Replace the current word with the truncated value
|
||||
$this->words[$this->wordIndex] = $this->getTokenValue($truncatedToken);
|
||||
$this->rawWords[$this->wordIndex] = $truncatedToken['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor position is past the end of the command line string - consider it a new word
|
||||
if ($this->wordIndex === null) {
|
||||
$this->wordIndex = count($this->words);
|
||||
$this->words[] = '';
|
||||
$this->rawWords[] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a token's value with escaping and quotes removed
|
||||
*
|
||||
* @see self::tokenizeString()
|
||||
* @param array $token
|
||||
* @return string
|
||||
*/
|
||||
protected function getTokenValue($token)
|
||||
{
|
||||
$value = $token['value'];
|
||||
|
||||
// Remove outer quote characters (or first quote if unclosed)
|
||||
if ($token['type'] == 'quoted') {
|
||||
$value = preg_replace('/^(?:[\'"])(.*?)(?:[\'"])?$/', '$1', $value);
|
||||
}
|
||||
|
||||
// Remove escape characters
|
||||
$value = preg_replace('/\\\\(.)/', '$1', $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Break a string into words, quoted strings and non-words (breaks)
|
||||
*
|
||||
* Returns an array of unmodified segments of $string with offset and type information.
|
||||
*
|
||||
* @param string $string
|
||||
* @return array as [ [type => string, value => string, offset => int], ... ]
|
||||
*/
|
||||
protected function tokenizeString($string)
|
||||
{
|
||||
// Map capture groups to returned token type
|
||||
$typeMap = array(
|
||||
'double_quote_string' => 'quoted',
|
||||
'single_quote_string' => 'quoted',
|
||||
'word' => 'word',
|
||||
'break' => 'break',
|
||||
);
|
||||
|
||||
// Escape every word break character including whitespace
|
||||
// preg_quote won't work here as it doesn't understand the ignore whitespace flag ("x")
|
||||
$breaks = preg_replace('/(.)/', '\\\$1', $this->wordBreaks);
|
||||
|
||||
$pattern = <<<"REGEX"
|
||||
/(?:
|
||||
(?P<double_quote_string>
|
||||
"(\\\\.|[^\"\\\\])*(?:"|$)
|
||||
) |
|
||||
(?P<single_quote_string>
|
||||
'(\\\\.|[^'\\\\])*(?:'|$)
|
||||
) |
|
||||
(?P<word>
|
||||
(?:\\\\.|[^$breaks])+
|
||||
) |
|
||||
(?P<break>
|
||||
[$breaks]+
|
||||
)
|
||||
)/x
|
||||
REGEX;
|
||||
|
||||
$tokens = array();
|
||||
|
||||
if (!preg_match_all($pattern, $string, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
foreach ($matches as $set) {
|
||||
foreach ($set as $groupName => $match) {
|
||||
|
||||
// Ignore integer indices preg_match outputs (duplicates of named groups)
|
||||
if (is_integer($groupName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if the offset indicates this group didn't match
|
||||
if ($match[1] === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tokens[] = array(
|
||||
'type' => $typeMap[$groupName],
|
||||
'value' => $match[0],
|
||||
'offset' => $match[1],
|
||||
'offsetEnd' => $match[1] + strlen($match[0])
|
||||
);
|
||||
|
||||
// Move to the next set (only one group should match per set)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the computed words so that $this->splitWords is forced to run again
|
||||
*/
|
||||
protected function reset()
|
||||
{
|
||||
$this->words = null;
|
||||
$this->wordIndex = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
<?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()
|
||||
{
|
||||
$commands = array();
|
||||
|
||||
foreach ($this->application->all() as $name => $command) {
|
||||
if (!$command->isHidden()) {
|
||||
$commands[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
return $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;
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?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;
|
||||
|
||||
# Force splitting by newline instead of default delimiters
|
||||
local IFS=$'\n';
|
||||
|
||||
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
|
||||
# Turn file/dir completion on temporarily and give control back to BASH
|
||||
compopt -o default;
|
||||
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';
|
||||
}
|
||||
|
||||
// Pass shell type during completion so output can be encoded if the shell requires it
|
||||
$completionCommand .= " --shell-type $type";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user