Issues (2963)

app/Console/Commands/BashCompletionCommand.php (1 issue)

1
<?php
2
3
namespace App\Console\Commands;
4
5
use App\Models\Device;
6
use Illuminate\Console\Command;
7
use Illuminate\Support\Str;
8
use Symfony\Component\Console\Input\InputDefinition;
9
use Symfony\Component\Console\Input\InputOption;
10
use Symfony\Component\Console\Input\StringInput;
11
12
class BashCompletionCommand extends Command
13
{
14
    protected $hidden = true;  // don't show this command in the list
15
16
    /**
17
     * The name and signature of the console command.
18
     *
19
     * @var string
20
     */
21
    protected $signature = 'list:bash-completion {this_command?} {current?} {previous?}';
22
23
    /**
24
     * The console command description.
25
     *
26
     * @var string
27
     */
28
    protected $description = 'Generates a bash completion response';
29
30
    /**
31
     * Execute the console command.
32
     *
33
     * @return mixed
34
     */
35
    public function handle()
36
    {
37
        $completions = collect();
38
        $line = getenv('COMP_LINE');
39
        $current = getenv('COMP_CURRENT');
40
        $previous = getenv('COMP_PREVIOUS');
41
        $words = explode(' ', $line);
42
43
        $command_name = isset($words[1]) ? $words[1] : $current; // handle : silliness
44
45
        if (count($words) < 3) {
46
            $completions = $this->completeCommand($command_name);
47
        } else {
48
            $commands = $this->getApplication()->all();
49
            if (isset($commands[$command_name])) {
50
                $command = $commands[$command_name];
51
                $command_def = $command->getDefinition();
52
                $input = new StringInput(implode(' ', array_slice($words, 2)));
53
                try {
54
                    $input->bind($command_def);
55
                } catch (\RuntimeException $e) {
56
                    // ignore?
57
                }
58
59
                // check if the command can complete arguments
60
                if (method_exists($command, 'completeArgument')) {
61
                    foreach ($input->getArguments() as $name => $value) {
62
                        if ($current == $value) {
63
                            $values = $command->completeArgument($name, $value, $previous);
64
                            if (! empty($values)) {
65
                                echo implode(PHP_EOL, $values);
66
67
                                return 0;
68
                            }
69
                            break;
70
                        }
71
                    }
72
                }
73
74
                if ($option = $this->optionExpectsValue($current, $previous, $command_def)) {
75
                    $completions = $this->completeOptionValue($option, $current);
76
                } else {
77
                    $completions = collect();
78
                    if (! Str::startsWith($previous, '-')) {
79
                        $completions = $this->completeArguments($command_name, $current, end($words));
80
                    }
81
                    $completions = $completions->merge($this->completeOption($command_def, $current, $this->getPreviousOptions($words)));
82
                }
83
            }
84
        }
85
86
        \Log::debug('Bash completion values', get_defined_vars());
87
88
        echo $completions->implode(PHP_EOL);
89
90
        return 0;
91
    }
92
93
    /**
94
     * @param  string  $current
95
     * @param  string  $previous
96
     * @param  InputDefinition  $command_def
97
     * @return false|InputOption
98
     */
99
    private function optionExpectsValue($current, $previous, $command_def)
100
    {
101
        // handle long option =
102
        if (Str::startsWith($current, '--') && Str::contains($current, '=')) {
103
            [$previous, $current] = explode('=', $current);
104
        }
105
106
        if (Str::startsWith($previous, '-')) {
107
            $name = ltrim($previous, '-');
108
            if ($command_def->hasOption($name) && $command_def->getOption($name)->acceptValue()) {
109
                return $command_def->getOption($name);
110
            }
111
112
            if ($command_def->hasShortcut($name) && $command_def->getOptionForShortcut($name)->acceptValue()) {
113
                return $command_def->getOptionForShortcut($name);
114
            }
115
        }
116
117
        return false;
118
    }
119
120
    private function parseOption(InputOption $def)
121
    {
122
        $opts = [];
123
124
        if ($shortcut = $def->getShortcut()) {
125
            $opts[] = '-' . $shortcut;
126
        }
127
128
        if ($name = $def->getName()) {
129
            $opts[] = '--' . $name;
130
        }
131
132
        return $opts;
133
    }
134
135
    /**
136
     * Complete a command
137
     *
138
     * @param  string  $partial
139
     * @return \Illuminate\Support\Collection
140
     */
141
    private function completeCommand($partial)
142
    {
143
        $all_commands = collect(\Artisan::all())->keys()->filter(function ($cmd) {
144
            return $cmd != 'list:bash-completion';
145
        });
146
147
        $completions = $all_commands->filter(function ($cmd) use ($partial) {
148
            return empty($partial) || Str::startsWith($cmd, $partial);
149
        });
150
151
        // handle : silliness
152
        if (Str::contains($partial, ':')) {
153
            $completions = $completions->map(function ($cmd) {
154
                return substr($cmd, strpos($cmd, ':') + 1);
155
            });
156
        }
157
158
        return $completions;
159
    }
160
161
    /**
162
     * Complete options for the given command
163
     *
164
     * @param  InputDefinition  $command
165
     * @param  string  $partial
166
     * @param  array  $prev_options  Previous words in the command
167
     * @return \Illuminate\Support\Collection
168
     */
169
    private function completeOption($command, $partial, $prev_options)
170
    {
171
        // default options
172
        $options = collect([
173
            '-h',
174
            '--help',
175
            '-V',
176
            '--version',
177
            '--ansi',
178
            '--no-ansi',
179
            '-n',
180
            '--no-interaction',
181
            '--env',
182
            '-v',
183
            '-vv',
184
            '-vvv',
185
        ]);
186
187
        if ($command) {
0 ignored issues
show
$command is of type Symfony\Component\Console\Input\InputDefinition, thus it always evaluated to true.
Loading history...
188
            $options = collect($command->getOptions())
189
                ->flatMap(function ($option) use ($prev_options) {
190
                    $option_flags = $this->parseOption($option);
191
                    // don't return previously specified options
192
                    if (array_intersect($option_flags, $prev_options)) {
193
                        return [];
194
                    }
195
196
                    return $option_flags;
197
                })->merge($options);
198
        }
199
200
        return $options->filter(function ($option) use ($partial) {
201
            return empty($partial) || Str::startsWith($option, $partial);
202
        });
203
    }
204
205
    private function getPreviousOptions($words)
206
    {
207
        return array_reduce($words, function ($result, $word) {
208
            if (Str::startsWith($word, '-')) {
209
                $split = explode('=', $word, 2); // users may use equals for values
210
                $result[] = reset($split);
211
            }
212
213
            return $result;
214
        }, []);
215
    }
216
217
    /**
218
     * Complete options with values (if a list is enumerate in the description)
219
     *
220
     * @param  InputOption  $option
221
     * @param  string  $partial
222
     * @return \Illuminate\Support\Collection
223
     */
224
    private function completeOptionValue($option, $partial)
225
    {
226
        if ($option && preg_match('/\[(.+)\]/', $option->getDescription(), $values)) {
227
            return collect(explode(',', $values[1]))
228
                ->map(function ($value) {
229
                    return trim($value);
230
                })
231
                ->filter(function ($value) use ($partial) {
232
                    return empty($partial) || Str::startsWith($value, $partial);
233
                });
234
        }
235
236
        return collect();
237
    }
238
239
    /**
240
     * Complete commands with arguments
241
     *
242
     * @param  string  $command  Name of the current command
243
     * @param  string  $partial
244
     * @param  string  $current_word
245
     * @return \Illuminate\Support\Collection
246
     */
247
    private function completeArguments($command, $partial, $current_word)
248
    {
249
        switch ($command) {
250
            case 'device:remove':
251
                // fall through
252
            case 'device:rename':
253
                $device_query = Device::select('hostname')->limit(5)->orderBy('hostname');
254
                if ($partial) {
255
                    $device_query->where('hostname', 'like', $partial . '%');
256
                }
257
258
                return $device_query->pluck('hostname');
259
            case 'help':
260
                return $this->completeCommand($current_word);
261
            default:
262
                return collect();
263
        }
264
    }
265
}
266