diff --git a/contrib/completion/bash/yii b/contrib/completion/bash/yii new file mode 100644 index 0000000..084325a --- /dev/null +++ b/contrib/completion/bash/yii @@ -0,0 +1,58 @@ +# This file implements bash completion for the ./yii command file. +# It completes the commands available by the ./yii command. +# See also: +# - https://debian-administration.org/article/317/An_introduction_to_bash_completion_part_2 on how this works. +# - https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html +# - http://www.yiiframework.com/doc-2.0/guide-tutorial-console.html#bash-completion +# +# Usage: +# Temporarily you can source this file in you bash by typing: source yii +# For permanent availability, copy or link this file to /etc/bash_completion.d/ +# + +_yii() +{ + local cur opts yii command + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + yii="${COMP_WORDS[0]}" + + # exit if ./yii does not exist + test -f $yii || return 0 + + # lookup for command + for word in ${COMP_WORDS[@]:1}; do + if [[ $word != -* ]]; then + command=$word + break + fi + done + + [[ $cur == $command ]] && state="command" + [[ $cur != $command ]] && state="option" + [[ $cur = *=* ]] && state="value" + + case $state in + command) + # complete command/route if not given + # fetch available commands from ./yii help/list command + opts=$($yii help/list 2> /dev/null) + ;; + option) + # fetch available options from ./yii help/list-action-options command + opts=$($yii help/list-action-options $command 2> /dev/null | grep -o '^--[a-zA-Z0-9]*') + ;; + value) + # TODO allow normal file completion after an option, e.g. --migrationPath=... + ;; + esac + + # generate completion suggestions + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + +} + +# register completion for the ./yii command +# you may adjust this line if your command file is named differently +complete -F _yii ./yii yii diff --git a/contrib/completion/zsh/_yii b/contrib/completion/zsh/_yii new file mode 100644 index 0000000..e85ebc3 --- /dev/null +++ b/contrib/completion/zsh/_yii @@ -0,0 +1,38 @@ +#compdef yii + +_yii() { + local state command lastArgument commands options executive + lastArgument=${words[${#words[@]}]} + executive=$words[1] + + # lookup for command + for word in ${words[@]:1}; do + if [[ $word != -* ]]; then + command=$word + break + fi + done + + + [[ $lastArgument == $command ]] && state="command" + [[ $lastArgument != $command ]] && state="option" + + case $state in + command) + commands=("${(@f)$(${executive} help/list 2>/dev/null)}") + _describe 'command' commands + ;; + option) + options=("${(@f)$(${executive} help/usage ${command} 2>/dev/null)}") + _message -r "$options" + + suboptions=("${(@f)$(${executive} help/list-action-options ${command} 2>/dev/null)}") + _describe -V -o -t suboption 'action options' suboptions + ;; + *) + esac + +} + +compdef _yii yii + diff --git a/docs/guide/tutorial-console.md b/docs/guide/tutorial-console.md index 0ab694e..db572ea 100644 --- a/docs/guide/tutorial-console.md +++ b/docs/guide/tutorial-console.md @@ -107,6 +107,50 @@ You can see an example of this in the advanced project template. > ``` +Console command completion +--------------- + +Auto-completion of command arguments is a useful thing when working with the shell. +Since version 2.0.11, the `./yii` command provides auto completion for the bash out of the box. + +### Bash completion + +Make sure bash completion is installed. For most of installations it is available by default. + +Place the completion script in `/etc/bash_completion.d/`: + + curl -L https://raw.githubusercontent.com/yiisoft/yii2/blob/master/contrib/completion/bash/yii > /etc/bash_completion.d/yii + +Check the [Bash Manual](https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html) for +other ways of including completion script to your environment. + +### ZSH completion + +Put the completion script in directory for completions, using e.g. `~/.zsh/completion/` + +``` +mkdir -p ~/.zsh/completion +curl -L https://raw.githubusercontent.com/yiisoft/yii2/blob/master/contrib/completion/zsh/_yii > ~/.zsh/completion/_yii +``` + +Include the directory in the `$fpath`, e.g. by adding it to `~/.zshrc` + +``` +fpath=(~/.zsh/completion $fpath) +``` + +Make sure `compinit` is loaded or do it by adding in `~/.zshrc` + +``` +autoload -Uz compinit && compinit -i +``` + +Then reload your shell + +``` +exec $SHELL -l +``` + Creating your own console commands ---------------------------------- diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index f46d889..12cf914 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -28,6 +28,7 @@ Yii Framework 2 Change Log - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) +- Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) - Enh #6809: Added `yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 27fbcd6..6e8f6fb 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -69,6 +69,110 @@ class HelpController extends Controller } /** + * List all available controllers and actions in machine readable format. + * This is used for shell completion. + * @since 2.0.11 + */ + public function actionList() + { + $commands = $this->getCommandDescriptions(); + foreach ($commands as $command => $description) { + $result = Yii::$app->createController($command); + if ($result === false || !($result[0] instanceof Controller)) { + continue; + } + /** @var $controller Controller */ + list($controller, $actionID) = $result; + $actions = $this->getActions($controller); + if (!empty($actions)) { + $prefix = $controller->getUniqueId(); + $this->stdout("$prefix\n"); + foreach ($actions as $action) { + $this->stdout("$prefix/$action\n"); + } + } + } + } + + /** + * List all available options for the $action in machine readable format. + * This is used for shell completion. + * + * @param string $action route to action + * @since 2.0.11 + */ + public function actionListActionOptions($action) + { + $result = Yii::$app->createController($action); + + if ($result === false || !($result[0] instanceof Controller)) { + return; + } + + /** @var Controller $controller */ + list($controller, $actionID) = $result; + $action = $controller->createAction($actionID); + if ($action === null) { + return; + } + + $arguments = $controller->getActionArgsHelp($action); + foreach ($arguments as $argument => $help) { + $description = str_replace("\n", '', addcslashes($help['comment'], ':')) ?: $argument; + $this->stdout($argument . ':' . $description . "\n"); + } + + $this->stdout("\n"); + $options = $controller->getActionOptionsHelp($action); + foreach ($options as $argument => $help) { + $description = str_replace("\n", '', addcslashes($help['comment'], ':')) ?: $argument; + $this->stdout('--' . $argument . ':' . $description . "\n"); + } + } + + /** + * Displays usage information for $action + * + * @param string $action route to action + * @since 2.0.11 + */ + public function actionUsage($action) + { + $result = Yii::$app->createController($action); + + if ($result === false || !($result[0] instanceof Controller)) { + return; + } + + /** @var Controller $controller */ + list($controller, $actionID) = $result; + $action = $controller->createAction($actionID); + if ($action === null) { + return; + } + + $scriptName = $this->getScriptName(); + if ($action->id === $controller->defaultAction) { + $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW)); + } else { + $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW)); + } + + $args = $controller->getActionArgsHelp($action); + foreach ($args as $name => $arg) { + if ($arg['required']) { + $this->stdout(' <' . $name . '>', Console::FG_CYAN); + } else { + $this->stdout(' [' . $name . ']', Console::FG_CYAN); + } + } + + $this->stdout("\n"); + + return; + } + + /** * Returns all available command names. * @return array all available command names */ diff --git a/tests/framework/console/controllers/HelpControllerTest.php b/tests/framework/console/controllers/HelpControllerTest.php index 14d25e1..e508b16 100644 --- a/tests/framework/console/controllers/HelpControllerTest.php +++ b/tests/framework/console/controllers/HelpControllerTest.php @@ -45,6 +45,79 @@ class HelpControllerTest extends TestCase return $controller->flushStdOutBuffer(); } + public function testActionList() + { + $this->mockApplication([ + 'enableCoreCommands' => false, + 'controllerMap' => [ + 'migrate' => 'yii\console\controllers\MigrateController', + 'cache' => 'yii\console\controllers\CacheController', + ], + ]); + $result = Console::stripAnsiFormat($this->runControllerAction('list')); + $this->assertEquals(<<mockApplication([ + 'enableCoreCommands' => false, + 'controllerMap' => [ + 'migrate' => 'yii\console\controllers\MigrateController', + 'cache' => 'yii\console\controllers\CacheController', + ], + ]); + $result = Console::stripAnsiFormat($this->runControllerAction('list-action-options', ['action' => 'help/list-action-options'])); + $this->assertEquals(<<mockApplication([ + 'enableCoreCommands' => false, + 'controllerMap' => [ + 'migrate' => 'yii\console\controllers\MigrateController', + 'cache' => 'yii\console\controllers\CacheController', + ], + ]); + $result = Console::stripAnsiFormat($this->runControllerAction('usage', ['action' => 'help/list-action-options'])); + $this->assertEquals(<< + +STRING + , $result); + } + public function testActionIndex() { $result = Console::stripAnsiFormat($this->runControllerAction('index')); @@ -56,7 +129,7 @@ class HelpControllerTest extends TestCase public function testActionIndexWithHelpCommand() { - $result = Console::stripAnsiFormat($this->runControllerAction('index', ['command' => 'help'])); + $result = Console::stripAnsiFormat($this->runControllerAction('index', ['command' => 'help/index'])); $this->assertContains('Displays available commands or the detailed information', $result); $this->assertContains('bootstrap.php help [command] [...options...]', $result); $this->assertContains('--appconfig: string', $result); @@ -88,4 +161,4 @@ class HelpControllerTest extends TestCase class BufferedHelpController extends HelpController { use StdOutBufferControllerTrait; -} \ No newline at end of file +}