Skip to content

Commit 2bdf798

Browse files
committed
[Console] WIP shell autocompletion
Only works partially on zsh at the moment (or zsh completion-compatible shells). 1. Copy/symlink src/Symfony/Component/Console/Resources/_console to /usr/share/zsh/vendor-completions/_console 2. Reload your shell 3. Enjoy autocompletion Inspiration has been taken from the _composer autocompletion in zsh: https://sources.debian.org/src/zsh/5.8-5/Completion/Unix/Command/_composer/ TODO: - caching & lazy commands (symfony#39851) - bash autocompletion support - fix major issues with command option completion in zsh ("bin/console assets:install -<TAB>") - better error handling
1 parent 2941951 commit 2bdf798

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed

src/Symfony/Component/Console/Application.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Console;
1313

14+
use Symfony\Component\Console\Command\AutocompleteCommand;
1415
use Symfony\Component\Console\Command\Command;
1516
use Symfony\Component\Console\Command\HelpCommand;
1617
use Symfony\Component\Console\Command\ListCommand;
@@ -1035,6 +1036,7 @@ protected function getDefaultInputDefinition()
10351036
new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
10361037
new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null),
10371038
new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
1039+
new InputOption('--example', '', InputOption::VALUE_REQUIRED, 'Some value example'),
10381040
]);
10391041
}
10401042

@@ -1045,7 +1047,7 @@ protected function getDefaultInputDefinition()
10451047
*/
10461048
protected function getDefaultCommands()
10471049
{
1048-
return [new HelpCommand(), new ListCommand()];
1050+
return [new HelpCommand(), new ListCommand(), new AutocompleteCommand()];
10491051
}
10501052

10511053
/**
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Command;
13+
14+
use Symfony\Component\Console\Input\InputDefinition;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Input\InputOption;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Process\Process;
19+
20+
/**
21+
* @author Wouter de Jong <[email protected]>
22+
*/
23+
class AutocompleteCommand extends Command
24+
{
25+
public function __construct()
26+
{
27+
parent::__construct('_autocomplete');
28+
}
29+
30+
protected function configure()
31+
{
32+
$this
33+
->addOption('global-args', '', InputOption::VALUE_NONE)
34+
->addOption('command', '', InputOption::VALUE_OPTIONAL, '', false)
35+
;
36+
}
37+
38+
public function execute(InputInterface $input, OutputInterface $output)
39+
{
40+
try {
41+
$p = Process::fromShellCommandline('echo $SHELL');
42+
$shell = basename(rtrim($p->mustRun()->getOutput()));
43+
if ('zsh' !== $shell) {
44+
throw new \RuntimeException(sprintf('Unsupported shell: "%s"', $shell));
45+
}
46+
47+
$output->writeln($this->autocompleteZsh($input));
48+
} catch (\Throwable $e) {
49+
$output->writeln($e->getMessage());
50+
}
51+
52+
return 0;
53+
}
54+
55+
private function autocompleteZsh(InputInterface $input): array
56+
{
57+
if ($input->getOption('global-args')) {
58+
return $this->autocompleteZshInputDefinition($this->getApplication()->getDefinition());
59+
}
60+
61+
if (false !== ($cmd = $input->getOption('command'))) {
62+
if (!$cmd) {
63+
$zshDescribe = [];
64+
foreach ($this->getApplication()->all() as $command) {
65+
$zshDescribe[] = sprintf('%s:%s', str_replace(':', '\:', $command->getName()), $command->getDescription());
66+
}
67+
68+
return $zshDescribe;
69+
}
70+
71+
return $this->autocompleteZshInputDefinition($this->getApplication()->find($cmd)->getDefinition());
72+
}
73+
74+
return [];
75+
}
76+
77+
private function autocompleteZshInputDefinition(InputDefinition $inputDefinition): array
78+
{
79+
$zshArguments = [];
80+
foreach ($inputDefinition->getOptions() as $inputOption) {
81+
$name = $inputOption->getName();
82+
if ('verbose' === $name) {
83+
$zshArguments[] = '(-q --quiet --verbose)*-v[increase output verbosity]';
84+
$zshArguments[] = '(-q --quiet --verbose)--verbose[increase output verbosity]';
85+
continue;
86+
} elseif ('quiet' === $name) {
87+
$zshArguments[] = '(-q -v --quiet --verbose)-q[reduce output verbosity]';
88+
$zshArguments[] = '(-q -v --quiet --verbose)--quiet[reduce output verbosity]';
89+
continue;
90+
}
91+
92+
$alternative = $inputOption->isNegatable() ? '-no-'.$name : $inputOption->getShortcut();
93+
$description = $inputOption->getDescription();
94+
if ($alternative) {
95+
$zshArguments[] = sprintf('(--%s -%s)--%1$s[%s]', $name, $alternative, $description);
96+
$zshArguments[] = sprintf('(--%s -%s)-%2$s[%s]', $name, $alternative, $description);
97+
} else {
98+
$zshArguments[] = sprintf('--%1s[%s]', $name, $description);
99+
}
100+
}
101+
102+
return $zshArguments;
103+
}
104+
}
105+
106+
/*
107+
[
108+
'(-h --help)-h[display help information]',
109+
'(-h --help)--help[display help information]',
110+
111+
'(: * -)-V[display version information]',
112+
'(: * -)--version[display version information]',
113+
114+
'(-q -v --quiet --verbose)-q[reduce output verbosity]',
115+
'(-q -v --quiet --verbose)--quiet}[reduce output verbosity]',
116+
'(-q --quiet --verbose)*-v[increase output verbosity]',
117+
'(-q --quiet --verbose)--verbose[increase output verbosity]',
118+
119+
'(--no-ansi)--ansi[force ANSI (color) output]',
120+
'(--ansi)--no-ansi[disable ANSI (color) output]',
121+
122+
'(-n --no-interaction)-n[run non-interactively]',
123+
'(-n --no-interaction)--no-interaction[run non-interactively]',
124+
]
125+
*/
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#compdef _console console
2+
3+
# autocomplete all available commands for this application
4+
__symfony_commands() {
5+
local _commands=(${(@f)"$(${words[1]} _autocomplete --command)"})
6+
_describe 'commands' _commands
7+
}
8+
9+
# autocomplete options/arguments for a specific command
10+
# TODO: this totally doesn't work
11+
__symfony_command() {
12+
local _suboptions=(${(@f)"$(${words[1]} _autocomplete --command=${words[2]})"})
13+
_arguments -s -S : $_suboptions
14+
}
15+
16+
# the main autocomplete function
17+
_console() {
18+
local line
19+
_global_options=(${(@f)"$(${words[1]} _autocomplete --global-args)"})
20+
_arguments -s -S : $_global_options \
21+
'1: :__symfony_commands' \
22+
'*: :__symfony_command'
23+
}

0 commit comments

Comments
 (0)