8000 [Console] Bash completion integration · symfony/symfony@962574e · GitHub
[go: up one dir, main page]

Skip to content

Commit 962574e

Browse files
committed
[Console] Bash completion integration
1 parent e43725f commit 962574e

File tree

14 files changed

+756
-14
lines changed

14 files changed

+756
-14
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand;
3939
use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand;
4040
use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber;
41+
use Symfony\Component\Console\Command\CompleteCommand;
4142
use Symfony\Component\Console\EventListener\ErrorListener;
4243
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;
4344
use Symfony\Component\Messenger\Command\DebugCommand;
@@ -66,6 +67,12 @@
6667
->set('console.command.about', AboutCommand::class)
6768
->tag('console.command')
6869

70+
->set('console.command._complete', CompleteCommand::class)
71+
->args([
72+
service('logger')->nullOnInvalid(),
73+
])
74+
->tag('console.command')
75+
6976
->set('console.command.assets_install', AssetsInstallCommand::class)
7077
->args([
7178
service('filesystem'),

src/Symfony/Component/Console/Application.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212
namespace Symfony\Component\Console;
1313

1414
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Command\CompleteCommand;
16+
use Symfony\Component\Console\Command\DumpCompletionCommand;
1517
use Symfony\Component\Console\Command\HelpCommand;
1618
use Symfony\Component\Console\Command\LazyCommand;
1719
use Symfony\Component\Console\Command\ListCommand;
1820
use Symfony\Component\Console\Command\SignalableCommandInterface;
1921
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
22+
use Symfony\Component\Console\Completion\Completion;
23+
use Symfony\Component\Console\Completion\CompletionInput;
24+
use Symfony\Component\Console\Completion\CompletionInterface;
2025
use Symfony\Component\Console\Event\ConsoleCommandEvent;
2126
use Symfony\Component\Console\Event\ConsoleErrorEvent;
2227
use Symfony\Component\Console\Event\ConsoleSignalEvent;
@@ -64,7 +69,7 @@
6469
*
6570
* @author Fabien Potencier <fabien@symfony.com>
6671
*/
67-
class Application implements ResetInterface
72+
class Application implements ResetInterface, CompletionInterface
6873
{
6974
private $commands = [];
7075
private $wantHelps = false;
@@ -350,6 +355,29 @@ public function getDefinition()
350355
return $this->definition;
351356
}
352357

F438
358+
/**
359+
* {@inheritdoc}
360+
*/
361+
public function complete(CompletionInput $input, Completion $completion): void
362+
{
363+
if (
364+
CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType()
365+
&& 'command' === $input->getCompletionName()
366+
) {
367+
$completion->suggestValues(array_filter(array_map(function (Command $command) {
368+
return $command->getName();
369+
}, $this->all())));
370+
371+
return;
372+
}
373+
374+
if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) {
375+
$completion->suggestOptions($this->getDefinition()->getOptions());
376+
377+
return;
378+
}
379+
}
380+
353381
/**
354382
* Gets the help message.
355383
*
@@ -1052,7 +1080,7 @@ protected function getDefaultInputDefinition()
10521080
*/
10531081
protected function getDefaultCommands()
10541082
{
1055-
return [new HelpCommand(), new ListCommand()];
1083+
return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
10561084
}
10571085

10581086
/**

src/Symfony/Component/Console/Command/Command.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
use Symfony\Component\Console\Application;
1515
use Symfony\Component\Console\Attribute\AsCommand;
16+
use Symfony\Component\Console\Completion\Completion;
17+
use Symfony\Component\Console\Completion\CompletionInput;
18+
use Symfony\Component\Console\Completion\CompletionInterface;
1619
use Symfony\Component\Console\Exception\ExceptionInterface;
1720
use Symfony\Component\Console\Exception\InvalidArgumentException;
1821
use Symfony\Component\Console\Exception\LogicException;
@@ -28,7 +31,7 @@
2831
*
2932
* @author Fabien Potencier <fabien@symfony.com>
3033
*/
31-
class Command
34+
class Command implements CompletionInterface
3235
{
3336
// see https://tldp.org/LDP/abs/html/exitcodes.html
3437
public const SUCCESS = 0;
@@ -234,6 +237,15 @@ protected function initialize(InputInterface $input, OutputInterface $output)
234237
{
235238
}
236239

240+
public function complete(CompletionInput $input, Completion $completion): void
241+
{
242+
if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) {
243+
$completion->suggestOptions($this->getDefinition()->getOptions());
244+
245+
return;
246+
}
247+
}
248+
237249
/**
238250
* Runs the command.
239251
*
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 Psr\Log\LoggerInterface;
15+
use Symfony\Component\Console\Completion\Completion;
16+
use Symfony\Component\Console\Completion\CompletionInput;
17+
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
18+
use Symfony\Component\Console\Exception\CommandNotFoundException;
19+
use Symfony\Component\Console\Exception\ExceptionInterface;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Input\InputOption;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
24+
/**
25+
* Responsible for providing the values to the shell completion.
26+
*
27+
* @author Wouter de Jong <wouter@wouterj.nl>
28+
*/
29+
class CompleteCommand extends Command
30+
{
31+
protected static $defaultName = '|_complete';
32+
protected static $defaultDescription = 'Internal command to provide shell completion suggestions';
33+
34+
private static $completionOutputs = [
35+
'bash' => BashCompletionOutput::class,
36+
];
37+
private $logger;
38+
39+
public function __construct(LoggerInterface $logger = null)
40+
{
41+
$this->logger = $logger;
42+
43+
parent::__construct();
44+
}
45+
46+
protected function configure(): void
47+
{
48+
$this
49+
->setName(self::$defaultName)
50+
->setDescription(self::$defaultDescription)
51+
->setHidden(true)
52+
->addOption('shell', 's', InputOption::VALUE_REQUIRED)
53+
->addOption('current', 'c', InputOption::VALUE_REQUIRED)
54+
->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY)
55+
;
56+
}
57+
58+
protected function execute(InputInterface $input, OutputInterface $output): int
59+
{
60+
try {
61+
$shell = $input->getOption('shell');
62+
if (!$shell) {
63+
throw new \RuntimeException('The "--shell" option must be set.');
64+
}
65+
66+
if (!$completionOutput = self::$completionOutputs[$shell] ?? false) {
67+
throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode(&# 10000 39;", "', array_keys(self::$completionOutputs))));
68+
}
69+
70+
$completionInput = $this->createCompletionInput($input);
71+
$completion = new Completion();
72+
73+
$command = $this->findCommand($completionInput, $output);
74+
if (null === $command) {
75+
$this->getApplication()->complete($completionInput, $completion);
76+
} elseif (
77+
CompletionInput::TYPE_ARGUMENT_VALUE === $completionInput->getCompletionType()
78+
&& 'command' === $completionInput->getCompletionName()
79+
&& $command->getName() !== $completionInput->getCompletionValue()
80+
) {
81+
$completion->suggestValue($command->getName());
82+
} else {
83+
$command->mergeApplicationDefinition();
84+
$completionInput->bind($command->getDefinition());
85+
86+
$command->complete($completionInput, $completion);
87+
}
88+
89+
$completionOutput = new $completionOutput();
90+
$completionOutput->write($completion, $output);
91+
} catch (\Throwable $e) {
92+
if ($this->logger) {
93+
$this->logger->warning('Error occurred while running shell completion for "{input}".', ['input' => (string) ($completionInput ?? $input), 'exception' => $e]);
94+
}
95+
96+
if ($output->isVerbose()) {
97+
throw $e;
98+
}
99+
100+
return self::FAILURE;
101+
}
102+
103< D96B /td>+
return self::SUCCESS;
104+
}
105+
106+
private function createCompletionInput(InputInterface $input): CompletionInput
107+
{
108+
$currentIndex = $input->getOption('current');
109+
$completionInput = CompletionInput::fromTokens(array_map(
110+
function (string $i): string { return trim($i, "'"); },
111+
$input->getOption('input')
112+
), $currentIndex);
113+
114+
try {
115+
$completionInput->bind($this->getApplication()->getDefinition());
116+
} catch (ExceptionInterface $e) {
117+
}
118+
119+
return $completionInput;
120+
}
121+
122+
private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command
123+
{
124+
try {
125+
$inputName = $completionInput->getFirstArgument();
126+
if (null === $inputName) {
127+
return null;
128+
}
129+
130+
return $this->getApplication()->find($inputName);
131+
} catch (CommandNotFoundException $e) {
132+
}
133+
134+
return null;
135+
}
136+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\InputArgument;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
19+
20+
/**
21+
* Dumps the completion script for the current shell.
22+
*
23+
* @author Wouter de Jong <wouter@wouterj.nl>
24+
*/
25+
class DumpCompletionCommand extends Command
26+
{
27+
protected function configure()
28+
{
29+
$this
30+
->setName('completion')
31+
->setHidden(true)
32+
->addArgument('shell', InputArgument::OPTIONAL, 'The shell type, the value of the "$SHELL" env var will be used if this is not given')
33+
;
34+
}
35+
36+
protected function execute(InputInterface $input, OutputInterface $output): int
37+
{
38+
$shell = $input->getArgument('shell') ?? $this->guessShell();
39+
$completionFile = __DIR__.'/../Resources/completion.'.$shell;
40+
if (!file_exists($completionFile)) {
41+
($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output)
42+
->writeln(sprintf('<error>Detected shell "%s", which is not supported by Symfony shell completion.</>', $shell));
43+
44+
return self::INVALID;
45+
}
46+
47+
$commandName = basename($_SERVER['argv'][0]);
48+
49+
$output->write(str_replace('{{ COMMAND_NAME }}', $commandName, file_get_contents($completionFile)));
50+
51+
return self::SUCCESS;
52+
}
53+
54+
private function guessShell(): string
55+
{
56+
return basename($_SERVER['SHELL']);
57+
}
58+
}

src/Symfony/Component/Console/Command/LazyCommand.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Symfony\Component\Console\Command;
1313

1414
use Symfony\Component\Console\Application;
15+
use Symfony\Component\Console\Completion\Completion;
16+
use Symfony\Component\Console\Completion\CompletionInput;
1517
use Symfony\Component\Console\Helper\HelperSet;
1618
use Symfony\Component\Console\Input\InputDefinition;
1719
use Symfony\Component\Console\Input\InputInterface;
@@ -69,6 +71,11 @@ public function run(InputInterface $input, OutputInterface $output): int
6971
return $this->getCommand()->run($input, $output);
7072
}
7173

74+
public function complete(CompletionInput $input, Completion $completion): void
75+
{
76+
$this->getCommand()->complete($input, $completion);
77+
}
78+
7279
/**
7380
* @return $this
7481
*/

0 commit comments

Comments
 (0)
0