CompletionCommand   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 0
Metric Value
wmc 20
lcom 2
cbo 9
dl 0
loc 233
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 24 2
A getNativeDefinition() 0 4 1
A mergeApplicationDefinition() 0 16 1
A filterApplicationOptions() 0 16 1
B execute() 0 43 6
A escapeForShell() 0 28 4
A runCompletion() 0 5 1
A configureCompletion() 0 4 1
A getShellType() 0 8 2
A createDefinition() 0 29 1
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();
0 ignored issues
show
Deprecated Code introduced by
The method Stecman\Component\Symfon...ommand::runCompletion() has been deprecated with message: - This will be removed in 1.0.0 in favour of CompletionCommand::configureCompletion

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.

Loading history...
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);
0 ignored issues
show
Documentation introduced by
$results is of type array<integer,string>, but the function expects a string|object<Symfony\Co...onsole\Output\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
136
        }
137
138
        return 0;
139
    }
140
141
    /**
142
     * Escape each completion result for the specified shell
143
     *
144
     * @param string $result - Completion results that should appear in the shell
145
     * @param string $shellType - Valid shell type from HookFactory
146
     * @return string
147
     */
148
    protected function escapeForShell($result, $shellType)
149
    {
150
        switch ($shellType) {
151
            // BASH requires special escaping for multi-word and special character results
152
            // This emulates registering completion with`-o filenames`, without side-effects like dir name slashes
153
            case 'bash':
154
                $context = $this->handler->getContext();
155
                $wordStart = substr($context->getRawCurrentWord(), 0, 1);
156
157
                if ($wordStart == "'") {
158
                    // If the current word is single-quoted, escape any single quotes in the result
159
                    $result = str_replace("'", "\\'", $result);
160
                } else if ($wordStart == '"') {
161
                    // If the current word is double-quoted, escape any double quotes in the result
162
                    $result = str_replace('"', '\\"', $result);
163
                } else {
164
                    // Otherwise assume the string is unquoted and word breaks should be escaped
165
                    $result = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $result);
166
                }
167
168
                // Escape output to prevent special characters being lost when passing results to compgen
169
                return escapeshellarg($result);
170
171
            // No transformation by default
172
            default:
173
                return $result;
174
        }
175
    }
176
177
    /**
178
     * Run the completion handler and return a filtered list of results
179
     *
180
     * @deprecated - This will be removed in 1.0.0 in favour of CompletionCommand::configureCompletion
181
     *
182
     * @return string[]
183
     */
184
    protected function runCompletion()
185
    {
186
        $this->configureCompletion($this->handler);
187
        return $this->handler->runCompletion();
188
    }
189
190
    /**
191
     * Configure the CompletionHandler instance before it is run
192
     *
193
     * @param CompletionHandler $handler
194
     */
195
    protected function configureCompletion(CompletionHandler $handler)
0 ignored issues
show
Unused Code introduced by
The parameter $handler is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
196
    {
197
        // Override this method to configure custom value completions
198
    }
199
200
    /**
201
     * Determine the shell type for use with HookFactory
202
     *
203
     * @return string
204
     */
205
    protected function getShellType()
206
    {
207
        if (!getenv('SHELL')) {
208
            throw new \RuntimeException('Could not read SHELL environment variable. Please specify your shell type using the --shell-type option.');
209
        }
210
211
        return basename(getenv('SHELL'));
212
    }
213
214
    protected function createDefinition()
215
    {
216
        return new InputDefinition(array(
217
            new InputOption(
218
                'generate-hook',
219
                'g',
220
                InputOption::VALUE_NONE,
221
                'Generate BASH code that sets up completion for this application.'
222
            ),
223
            new InputOption(
224
                'program',
225
                'p',
226
                InputOption::VALUE_REQUIRED,
227
                "Program name that should trigger completion\n<comment>(defaults to the absolute application path)</comment>."
228
            ),
229
            new InputOption(
230
                'multiple',
231
                'm',
232
                InputOption::VALUE_NONE,
233
                "Generated hook can be used for multiple applications."
234
            ),
235
            new InputOption(
236
                'shell-type',
237
                null,
238
                InputOption::VALUE_OPTIONAL,
239
                'Set the shell type (zsh or bash). Otherwise this is determined automatically.'
240
            ),
241
        ));
242
    }
243
}
244