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
+}