Completed
Pull Request — master (#90)
by
unknown
01:18
created

CompletionCommand::writeForShell()   B

Complexity

Conditions 10
Paths 14

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 54
rs 7.1369
c 0
b 0
f 0
cc 10
nc 14
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
            $this->configureCompletion($handler);
127
            $results = $this->handler->runCompletion();
128
129
            // Escape results for the current shell
130
            $shellType = $input->getOption('shell-type') ?: $this->getShellType();
131
132
            return $this->writeForShell($results, $shellType, $output);
133
        }
134
135
        return 0;
136
    }
137
138
    /**
139
     * Escape each completion result for the specified shell
140
     *
141
     * @param Completion\CompletionResultInterface $result - Completion results that should appear in the shell
142
     * @param string $shellType - Valid shell type from HookFactory
143
     * @param OutputInterface $output
144
     * @return int
145
     */
146
    protected function writeForShell($result, $shellType, $output)
147
    {
148
        $desc = $result->isDescriptive();
149
        $values = $result->getValues();
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
                if ($desc) {
155
                    // BASH does not support autocompletion descriptions, so we just want the actual suggestions
156
                    $values = array_keys($values);
157
                }
158
                foreach ($values as &$value) {
159
                    $context = $this->handler->getContext();
160
                    $wordStart = substr($context->getRawCurrentWord(), 0, 1);
161
162
                    if ($wordStart == "'") {
163
                        // If the current word is single-quoted, escape any single quotes in the result
164
                        $value = str_replace("'", "\\'", $value);
165
                    } else if ($wordStart == '"') {
166
                        // If the current word is double-quoted, escape any double quotes in the result
167
                        $value = str_replace('"', '\\"', $value);
168
                    } else {
169
                        // Otherwise assume the string is unquoted and word breaks should be escaped
170
                        $value = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $value);
171
                    }
172
173
                    // Escape output to prevent special characters being lost when passing results to compgen
174
                    $value = escapeshellarg($value);
175
                }
176
                $output->write($values, true);
0 ignored issues
show
Documentation introduced by
$values is of type array<integer,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...
177
178
                return 0;
179
180
            case 'zsh':
181
                if ($desc) {
182
                    $out = array();
183
                    foreach ($values as $cmd => $description) {
184
                        $out[] = sprintf("'%s:%s'", $cmd, $description);
185
                    }
186
187
                    $output->write(sprintf("(%s)", implode(" ", $out)), true);
188
                    return 100;
189
                }
190
191
            // No transformation by default
192
            default:
193
                if ($desc) {
194
                    $values = array_keys($values);
195
                }
196
                $output->write($values, true);
0 ignored issues
show
Documentation introduced by
$values is of type array<integer,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...
197
                return 0;
198
        }
199
    }
200
201
    /**
202
     * Configure the CompletionHandler instance before it is run
203
     *
204
     * @param CompletionHandler $handler
205
     * @return void
206
     */
207
    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...
208
    {
209
        // Override this method to configure custom value completions
210
    }
211
212
    /**
213
     * Determine the shell type for use with HookFactory
214
     *
215
     * @return string
216
     */
217
    protected function getShellType()
218
    {
219
        if (!getenv('SHELL')) {
220
            throw new \RuntimeException('Could not read SHELL environment variable. Please specify your shell type using the --shell-type option.');
221
        }
222
223
        return basename(getenv('SHELL'));
224
    }
225
226
    protected function createDefinition()
227
    {
228
        return new InputDefinition(array(
229
            new InputOption(
230
                'generate-hook',
231
                'g',
232
                InputOption::VALUE_NONE,
233
                'Generate BASH code that sets up completion for this application.'
234
            ),
235
            new InputOption(
236
                'program',
237
                'p',
238
                InputOption::VALUE_REQUIRED,
239
                "Program name that should trigger completion\n<comment>(defaults to the absolute application path)</comment>."
240
            ),
241
            new InputOption(
242
                'multiple',
243
                'm',
244
                InputOption::VALUE_NONE,
245
                "Generated hook can be used for multiple applications."
246
            ),
247
            new InputOption(
248
                'shell-type',
249
                null,
250
                InputOption::VALUE_OPTIONAL,
251
                'Set the shell type (zsh or bash). Otherwise this is determined automatically.'
252
            ),
253
        ));
254
    }
255
}
256