1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Stecman\Component\Symfony\Console\BashCompletion; |
4
|
|
|
|
5
|
|
|
use Symfony\Component\Console\Command\Command as SymfonyCommand; |
6
|
|
|
use Symfony\Component\Console\Input\InputDefinition; |
7
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
8
|
|
|
use Symfony\Component\Console\Input\InputOption; |
9
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
10
|
|
|
|
11
|
|
|
class CompletionCommand extends SymfonyCommand |
12
|
|
|
{ |
13
|
|
|
/** |
14
|
|
|
* @var CompletionHandler |
15
|
|
|
*/ |
16
|
|
|
protected $handler; |
17
|
|
|
|
18
|
|
|
protected function configure() |
19
|
|
|
{ |
20
|
|
|
$this |
21
|
|
|
->setName('_completion') |
22
|
|
|
->setDefinition($this->createDefinition()) |
23
|
|
|
->setDescription('BASH completion hook.') |
24
|
|
|
->setHelp(<<<END |
25
|
|
|
To enable BASH completion, run: |
26
|
|
|
|
27
|
|
|
<comment>eval `[program] _completion -g`</comment>. |
28
|
|
|
|
29
|
|
|
Or for an alias: |
30
|
|
|
|
31
|
|
|
<comment>eval `[program] _completion -g -p [alias]`</comment>. |
32
|
|
|
|
33
|
|
|
END |
34
|
|
|
); |
35
|
|
|
|
36
|
|
|
// Hide this command from listing if supported |
37
|
|
|
// Command::setHidden() was not available before Symfony 3.2.0 |
38
|
|
|
if (method_exists($this, 'setHidden')) { |
39
|
|
|
$this->setHidden(true); |
40
|
|
|
} |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* {@inheritdoc} |
45
|
|
|
*/ |
46
|
|
|
public function getNativeDefinition() |
47
|
|
|
{ |
48
|
|
|
return $this->createDefinition(); |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Ignore user-defined global options |
53
|
|
|
* |
54
|
|
|
* Any global options defined by user-code are meaningless to this command. |
55
|
|
|
* Options outside of the core defaults are ignored to avoid name and shortcut conflicts. |
56
|
|
|
*/ |
57
|
|
|
public function mergeApplicationDefinition($mergeArgs = true) |
58
|
|
|
{ |
59
|
|
|
// Get current application options |
60
|
|
|
$appDefinition = $this->getApplication()->getDefinition(); |
61
|
|
|
$originalOptions = $appDefinition->getOptions(); |
62
|
|
|
|
63
|
|
|
// Temporarily replace application options with a filtered list |
64
|
|
|
$appDefinition->setOptions( |
65
|
|
|
$this->filterApplicationOptions($originalOptions) |
66
|
|
|
); |
67
|
|
|
|
68
|
|
|
parent::mergeApplicationDefinition($mergeArgs); |
69
|
|
|
|
70
|
|
|
// Restore original application options |
71
|
|
|
$appDefinition->setOptions($originalOptions); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Reduce the passed list of options to the core defaults (if they exist) |
76
|
|
|
* |
77
|
|
|
* @param InputOption[] $appOptions |
78
|
|
|
* @return InputOption[] |
79
|
|
|
*/ |
80
|
|
|
protected function filterApplicationOptions(array $appOptions) |
81
|
|
|
{ |
82
|
|
|
return array_filter($appOptions, function(InputOption $option) { |
83
|
|
|
static $coreOptions = array( |
84
|
|
|
'help' => true, |
85
|
|
|
'quiet' => true, |
86
|
|
|
'verbose' => true, |
87
|
|
|
'version' => true, |
88
|
|
|
'ansi' => true, |
89
|
|
|
'no-ansi' => true, |
90
|
|
|
'no-interaction' => true, |
91
|
|
|
); |
92
|
|
|
|
93
|
|
|
return isset($coreOptions[$option->getName()]); |
94
|
|
|
}); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
98
|
|
|
{ |
99
|
|
|
$this->handler = new CompletionHandler($this->getApplication()); |
100
|
|
|
$handler = $this->handler; |
101
|
|
|
|
102
|
|
|
if ($input->getOption('generate-hook')) { |
103
|
|
|
global $argv; |
104
|
|
|
$program = $argv[0]; |
105
|
|
|
|
106
|
|
|
$factory = new HookFactory(); |
107
|
|
|
$alias = $input->getOption('program'); |
108
|
|
|
$multiple = (bool)$input->getOption('multiple'); |
109
|
|
|
|
110
|
|
|
if (!$alias) { |
111
|
|
|
$alias = basename($program); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
$hook = $factory->generateHook( |
115
|
|
|
$input->getOption('shell-type') ?: $this->getShellType(), |
116
|
|
|
$program, |
117
|
|
|
$alias, |
118
|
|
|
$multiple |
119
|
|
|
); |
120
|
|
|
|
121
|
|
|
$output->write($hook, true); |
122
|
|
|
} else { |
123
|
|
|
$handler->setContext(new EnvironmentCompletionContext()); |
124
|
|
|
|
125
|
|
|
// Get completion results |
126
|
|
|
$results = $this->runCompletion(); |
|
|
|
|
127
|
|
|
|
128
|
|
|
// Escape results for the current shell |
129
|
|
|
$shellType = $input->getOption('shell-type') ?: $this->getShellType(); |
130
|
|
|
|
131
|
|
|
foreach ($results as &$result) { |
132
|
|
|
$result = $this->escapeForShell($result, $shellType); |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
$output->write($results, true); |
|
|
|
|
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Escape each completion result for the specified shell |
141
|
|
|
* |
142
|
|
|
* @param string $result - Completion results that should appear in the shell |
143
|
|
|
* @param string $shellType - Valid shell type from HookFactory |
144
|
|
|
* @return string |
145
|
|
|
*/ |
146
|
|
|
protected function escapeForShell($result, $shellType) |
147
|
|
|
{ |
148
|
|
|
switch ($shellType) { |
149
|
|
|
// BASH requires special escaping for multi-word and special character results |
150
|
|
|
// This emulates registering completion with`-o filenames`, without side-effects like dir name slashes |
151
|
|
|
case 'bash': |
152
|
|
|
$context = $this->handler->getContext(); |
153
|
|
|
$wordStart = substr($context->getRawCurrentWord(), 0, 1); |
154
|
|
|
|
155
|
|
|
if ($wordStart == "'") { |
156
|
|
|
// If the current word is single-quoted, escape any single quotes in the result |
157
|
|
|
$result = str_replace("'", "\\'", $result); |
158
|
|
|
} else if ($wordStart == '"') { |
159
|
|
|
// If the current word is double-quoted, escape any double quotes in the result |
160
|
|
|
$result = str_replace('"', '\\"', $result); |
161
|
|
|
} else { |
162
|
|
|
// Otherwise assume the string is unquoted and word breaks should be escaped |
163
|
|
|
$result = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $result); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
// Escape output to prevent special characters being lost when passing results to compgen |
167
|
|
|
return escapeshellarg($result); |
168
|
|
|
|
169
|
|
|
// No transformation by default |
170
|
|
|
default: |
171
|
|
|
return $result; |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Run the completion handler and return a filtered list of results |
177
|
|
|
* |
178
|
|
|
* @deprecated - This will be removed in 1.0.0 in favour of CompletionCommand::configureCompletion |
179
|
|
|
* |
180
|
|
|
* @return string[] |
181
|
|
|
*/ |
182
|
|
|
protected function runCompletion() |
183
|
|
|
{ |
184
|
|
|
$this->configureCompletion($this->handler); |
185
|
|
|
return $this->handler->runCompletion(); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Configure the CompletionHandler instance before it is run |
190
|
|
|
* |
191
|
|
|
* @param CompletionHandler $handler |
192
|
|
|
*/ |
193
|
|
|
protected function configureCompletion(CompletionHandler $handler) |
|
|
|
|
194
|
|
|
{ |
195
|
|
|
// Override this method to configure custom value completions |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* Determine the shell type for use with HookFactory |
200
|
|
|
* |
201
|
|
|
* @return string |
202
|
|
|
*/ |
203
|
|
|
protected function getShellType() |
204
|
|
|
{ |
205
|
|
|
if (!getenv('SHELL')) { |
206
|
|
|
throw new \RuntimeException('Could not read SHELL environment variable. Please specify your shell type using the --shell-type option.'); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
return basename(getenv('SHELL')); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
protected function createDefinition() |
213
|
|
|
{ |
214
|
|
|
return new InputDefinition(array( |
215
|
|
|
new InputOption( |
216
|
|
|
'generate-hook', |
217
|
|
|
'g', |
218
|
|
|
InputOption::VALUE_NONE, |
219
|
|
|
'Generate BASH code that sets up completion for this application.' |
220
|
|
|
), |
221
|
|
|
new InputOption( |
222
|
|
|
'program', |
223
|
|
|
'p', |
224
|
|
|
InputOption::VALUE_REQUIRED, |
225
|
|
|
"Program name that should trigger completion\n<comment>(defaults to the absolute application path)</comment>." |
226
|
|
|
), |
227
|
|
|
new InputOption( |
228
|
|
|
'multiple', |
229
|
|
|
'm', |
230
|
|
|
InputOption::VALUE_NONE, |
231
|
|
|
"Generated hook can be used for multiple applications." |
232
|
|
|
), |
233
|
|
|
new InputOption( |
234
|
|
|
'shell-type', |
235
|
|
|
null, |
236
|
|
|
InputOption::VALUE_OPTIONAL, |
237
|
|
|
'Set the shell type (zsh or bash). Otherwise this is determined automatically.' |
238
|
|
|
), |
239
|
|
|
)); |
240
|
|
|
} |
241
|
|
|
} |
242
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.