Passed
Pull Request — master (#9)
by ANTHONIUS
03:11
created

Shell::autocompleter()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
c 0
b 0
f 0
rs 8.439
cc 6
eloc 14
nc 5
nop 1
1
<?php
2
3
/*
4
 * This file is part of the dotfiles project.
5
 *
6
 *     (c) Anthonius Munthi <[email protected]>
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 Dotfiles\Core\Console;
13
14
use Dotfiles\Core\DI\Parameters;
15
use Symfony\Component\Console\Exception\RuntimeException;
16
use Symfony\Component\Console\Input\StringInput;
17
use Symfony\Component\Console\Output\ConsoleOutput;
18
use Symfony\Component\Process\PhpExecutableFinder;
19
use Symfony\Component\Process\ProcessBuilder;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Process\ProcessBuilder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
21
/**
22
 * A Shell wraps an Application to add shell capabilities to it.
23
 *
24
 * Support for history and completion only works with a PHP compiled
25
 * with readline support (either --with-readline or --with-libedit)
26
 *
27
 * This class copied directly from symfony console 2.8 component ;-)
28
 *
29
 * @author Fabien Potencier <[email protected]>
30
 * @author Martin Hasoň <[email protected]>
31
 * @author Anthonius Munthi <[email protected]>
32
 * @codeCoverageIgnore
33
 */
34
class Shell
35
{
36
    private $application;
37
38
    private $hasReadline;
39
40
    private $history;
41
42
    private $isRunning = false;
43
44
    private $output;
45
46
    /**
47
     * @var Parameters
48
     */
49
    private $parameters;
50
51
    private $processIsolation = false;
52
53
    /**
54
     * If there is no readline support for the current PHP executable
55
     * a \RuntimeException exception is thrown.
56
     */
57
    public function __construct(Application $application, Parameters $parameters)
58
    {
59
        $this->hasReadline = function_exists('readline');
60
        $this->application = $application;
61
        $this->history = getenv('HOME').'/.history_'.$application->getName();
62
        $this->output = new ConsoleOutput();
63
        $this->parameters = $parameters;
64
    }
65
66
    public function getProcessIsolation()
67
    {
68
        return $this->processIsolation;
69
    }
70
71
    /**
72
     * @throws \Exception
73
     */
74
    public function run()
75
    {
76
        if (!$this->isRunning) {
77
            $this->isRunning = true;
78
            $this->doRun();
79
        } else {
80
            $input = new StringInput('list');
81
            $this->getApplication()->run($input);
82
        }
83
    }
84
85
    public function setProcessIsolation($processIsolation)
86
    {
87
        $this->processIsolation = (bool) $processIsolation;
88
89
        if ($this->processIsolation && !class_exists('Symfony\\Component\\Process\\Process')) {
90
            throw new RuntimeException('Unable to isolate processes as the Symfony Process Component is not installed.');
91
        }
92
    }
93
94
    protected function getApplication()
95
    {
96
        return $this->application;
97
    }
98
99
    /**
100
     * Returns the shell header.
101
     *
102
     * @return string The header string
103
     */
104
    protected function getHeader()
105
    {
106
        return <<<EOF
107
108
Welcome to the <info>{$this->application->getName()}</info> (<comment>{$this->application->getVersion()}</comment>).
109
110
At the prompt, type <comment>help</comment> for some help,
111
or <comment>list</comment> to get a list of available commands.
112
113
To exit the shell, type <comment>^D</comment>.
114
115
EOF;
116
    }
117
118
    protected function getOutput()
119
    {
120
        return $this->output;
121
    }
122
123
    /**
124
     * Renders a prompt.
125
     *
126
     * @return string The prompt
127
     */
128
    protected function getPrompt()
129
    {
130
        $prompt = $this->application->getName().'>> ';
131
132
        return $this->output->getFormatter()->format($prompt);
133
    }
134
135
    /**
136
     * Tries to return autocompletion for the current entered text.
137
     *
138
     * @param string $text The last segment of the entered text
139
     *
140
     * @return bool|array A list of guessed strings or true
141
     */
142
    private function autocompleter($text)
0 ignored issues
show
Unused Code introduced by
The parameter $text is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

142
    private function autocompleter(/** @scrutinizer ignore-unused */ $text)

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

Loading history...
143
    {
144
        $info = readline_info();
145
        $text = substr($info['line_buffer'], 0, $info['end']);
146
147
        if ($info['point'] !== $info['end']) {
148
            return true;
149
        }
150
151
        // task name?
152
        if (false === strpos($text, ' ') || !$text) {
153
            return array_keys($this->application->all());
154
        }
155
156
        // options and arguments?
157
        try {
158
            $command = $this->application->find(substr($text, 0, strpos($text, ' ')));
159
        } catch (\Exception $e) {
160
            return true;
161
        }
162
163
        $list = array('--help');
164
        foreach ($command->getDefinition()->getOptions() as $option) {
165
            $list[] = '--'.$option->getName();
166
        }
167
168
        return $list;
169
    }
170
171
    /**
172
     * Runs the shell.
173
     */
174
    private function doRun()
175
    {
176
        $this->application->setAutoExit(false);
177
        $this->application->setCatchExceptions(true);
178
179
        if ($this->hasReadline) {
180
            readline_read_history($this->history);
181
            readline_completion_function(array($this, 'autocompleter'));
182
        }
183
184
        $this->output->writeln($this->getHeader());
185
        $php = null;
186
        if ($this->processIsolation) {
187
            $finder = new PhpExecutableFinder();
188
            $php = $finder->find();
189
            $this->output->writeln(
190
                <<<'EOF'
191
<info>Running with process isolation, you should consider this:</info>
192
  * each command is executed as separate process,
193
  * commands don't support interactivity, all params must be passed explicitly,
194
  * commands output is not colorized.
195
196
EOF
197
            );
198
        }
199
200
        while (true) {
201
            $command = $this->readline();
202
203
            if (false === $command) {
204
                $this->output->writeln("\n");
205
206
                break;
207
            }
208
209
            if ($this->hasReadline) {
210
                readline_add_history($command);
211
                readline_write_history($this->history);
212
            }
213
214
            if ($this->processIsolation) {
215
                $pb = new ProcessBuilder();
216
217
                $process = $pb
218
                    ->add($php)
219
                    ->add($_SERVER['argv'][0])
220
                    ->add($command)
221
                    ->inheritEnvironmentVariables(true)
222
                    ->getProcess()
223
                ;
224
225
                $output = $this->output;
226
                $process->run(function ($type, $data) use ($output) {
227
                    $output->writeln($data);
228
                });
229
230
                $ret = $process->getExitCode();
231
            } else {
232
                $ret = $this->application->run(new StringInput($command), $this->output);
233
            }
234
235
            if (0 !== $ret) {
236
                $this->output->writeln(sprintf('<error>The command terminated with an error status (%s)</error>', $ret));
237
            }
238
        }
239
    }
240
241
    /**
242
     * Reads a single line from standard input.
243
     *
244
     * @return string The single line from standard input
245
     */
246
    private function readline()
247
    {
248
        if ($this->hasReadline) {
249
            $line = readline($this->getPrompt());
250
        } else {
251
            $this->output->write($this->getPrompt());
252
            $line = fgets(STDIN, 1024);
253
            $line = (false === $line || '' === $line) ? false : rtrim($line);
254
        }
255
256
        return $line;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $line could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
257
    }
258
}
259