Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
108 / 108
100.00% covered (success)
100.00%
19 / 19
CRAP
100.00% covered (success)
100.00%
1 / 1
Console
100.00% covered (success)
100.00%
108 / 108
100.00% covered (success)
100.00%
19 / 19
57
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setDefaultCommands
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArguments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArgument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addCommand
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 addCommands
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getCommand
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getCommands
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 removeCommand
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 removeCommands
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasCommand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 exec
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 reset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 prepare
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
12
 commandToArgs
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
14
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework CLI Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\CLI;
11
12use Framework\CLI\Commands\About;
13use Framework\CLI\Commands\Help;
14use Framework\CLI\Commands\Index;
15use Framework\Language\Language;
16use JetBrains\PhpStorm\Pure;
17
18/**
19 * Class Console.
20 *
21 * @package cli
22 */
23class Console
24{
25    /**
26     * List of commands.
27     *
28     * @var array<string,Command> The command name as key and the object as value
29     */
30    protected array $commands = [];
31    /**
32     * The current command name.
33     */
34    protected string $command = '';
35    /**
36     * Input options.
37     *
38     * @var array<string,bool|string> The option value as string or TRUE if it
39     * was passed without a value
40     */
41    protected array $options = [];
42    /**
43     * Input arguments.
44     *
45     * @var array<int,string>
46     */
47    protected array $arguments = [];
48    /**
49     * The Language instance.
50     */
51    protected Language $language;
52
53    /**
54     * Console constructor.
55     *
56     * @param Language|null $language
57     */
58    public function __construct(Language $language = null)
59    {
60        if ($language === null) {
61            $language = new Language('en');
62        }
63        $this->language = $language->addDirectory(__DIR__ . '/Languages');
64        global $argv;
65        $this->prepare($argv ?? []);
66        $this->setDefaultCommands();
67    }
68
69    protected function setDefaultCommands() : static
70    {
71        if ($this->getCommand('index') === null) {
72            $this->addCommand(new Index($this));
73        }
74        if ($this->getCommand('help') === null) {
75            $this->addCommand(new Help($this));
76        }
77        if ($this->getCommand('about') === null) {
78            $this->addCommand(new About($this));
79        }
80        return $this;
81    }
82
83    /**
84     * Get all CLI options.
85     *
86     * @return array<string,bool|string>
87     */
88    #[Pure]
89    public function getOptions() : array
90    {
91        return $this->options;
92    }
93
94    /**
95     * Get a specific option or null.
96     *
97     * @param string $option
98     *
99     * @return bool|string|null The option value as string, TRUE if it
100     * was passed without a value or NULL if the option was not set
101     */
102    #[Pure]
103    public function getOption(string $option) : bool | string | null
104    {
105        return $this->options[$option] ?? null;
106    }
107
108    /**
109     * Get all arguments.
110     *
111     * @return array<int,string>
112     */
113    #[Pure]
114    public function getArguments() : array
115    {
116        return $this->arguments;
117    }
118
119    /**
120     * Get a specific argument or null.
121     *
122     * @param int $position Argument position, starting from zero
123     *
124     * @return string|null The argument value or null if it was not set
125     */
126    #[Pure]
127    public function getArgument(int $position) : ?string
128    {
129        return $this->arguments[$position] ?? null;
130    }
131
132    /**
133     * Get the Language instance.
134     *
135     * @return Language
136     */
137    #[Pure]
138    public function getLanguage() : Language
139    {
140        return $this->language;
141    }
142
143    /**
144     * Add a command to the console.
145     *
146     * @param class-string<Command>|Command $command A Command instance or the class FQN
147     *
148     * @return static
149     */
150    public function addCommand(Command | string $command) : static
151    {
152        if (\is_string($command)) {
153            $command = new $command();
154        }
155        $command->setConsole($this);
156        $this->commands[$command->getName()] = $command;
157        return $this;
158    }
159
160    /**
161     * Add many commands to the console.
162     *
163     * @param array<class-string<Command>|Command> $commands A list of Command
164     * instances or the classes FQN
165     *
166     * @return static
167     */
168    public function addCommands(array $commands) : static
169    {
170        foreach ($commands as $command) {
171            $this->addCommand($command);
172        }
173        return $this;
174    }
175
176    /**
177     * Get an active command.
178     *
179     * @param string $name Command name
180     *
181     * @return Command|null The Command on success or null if not found
182     */
183    public function getCommand(string $name) : ?Command
184    {
185        if (isset($this->commands[$name]) && $this->commands[$name]->isActive()) {
186            return $this->commands[$name];
187        }
188        return null;
189    }
190
191    /**
192     * Get a list of active commands.
193     *
194     * @return array<string,Command>
195     */
196    public function getCommands() : array
197    {
198        $commands = $this->commands;
199        foreach ($commands as $name => $command) {
200            if ( ! $command->isActive()) {
201                unset($commands[$name]);
202            }
203        }
204        \ksort($commands);
205        return $commands;
206    }
207
208    /**
209     * Remove a command.
210     *
211     * @param string $name Command name
212     *
213     * @return static
214     */
215    public function removeCommand(string $name) : static
216    {
217        unset($this->commands[$name]);
218        return $this;
219    }
220
221    /**
222     * Remove commands.
223     *
224     * @param array<string> $names Command names
225     *
226     * @return static
227     */
228    public function removeCommands(array $names) : static
229    {
230        foreach ($names as $name) {
231            $this->removeCommand($name);
232        }
233        return $this;
234    }
235
236    /**
237     * Tells if it has a command.
238     *
239     * @param string $name Command name
240     *
241     * @return bool
242     */
243    public function hasCommand(string $name) : bool
244    {
245        return $this->getCommand($name) !== null;
246    }
247
248    /**
249     * Run the Console.
250     */
251    public function run() : void
252    {
253        if ($this->command === '') {
254            $this->command = 'index';
255        }
256        $command = $this->getCommand($this->command);
257        if ($command === null) {
258            CLI::error(CLI::style(
259                $this->getLanguage()->render('cli', 'commandNotFound', [$this->command]),
260                CLI::FG_BRIGHT_RED
261            ), \defined('TESTING') ? null : 1);
262            return;
263        }
264        $command->run();
265    }
266
267    public function exec(string $command) : void
268    {
269        $argumentValues = static::commandToArgs($command);
270        \array_unshift($argumentValues, 'removed');
271        $this->prepare($argumentValues);
272        $this->run();
273    }
274
275    protected function reset() : void
276    {
277        $this->command = '';
278        $this->options = [];
279        $this->arguments = [];
280    }
281
282    /**
283     * Prepare information of the command line.
284     *
285     * [options] [arguments] [options]
286     * [options] -- [arguments]
287     * [command]
288     * [command] [options] [arguments] [options]
289     * [command] [options] -- [arguments]
290     * Short option: -l, -la === l = true, a = true
291     * Long option: --list, --all=vertical === list = true, all = vertical
292     * Only Long Options receive values:
293     * --foo=bar or --f=bar - "foo" and "f" are bar
294     * -foo=bar or -f=bar - all characters are true (f, o, =, b, a, r)
295     * After -- all values are arguments, also if is prefixed with -
296     * Without --, arguments and options can be mixed: -ls foo -x abc --a=e.
297     *
298     * @param array<int,string> $argumentValues
299     */
300    protected function prepare(array $argumentValues) : void
301    {
302        $this->reset();
303        unset($argumentValues[0]);
304        if (isset($argumentValues[1]) && $argumentValues[1][0] !== '-') {
305            $this->command = $argumentValues[1];
306            unset($argumentValues[1]);
307        }
308        $endOptions = false;
309        foreach ($argumentValues as $value) {
310            if ($endOptions === false && $value === '--') {
311                $endOptions = true;
312                continue;
313            }
314            if ($endOptions === false && $value[0] === '-') {
315                if (isset($value[1]) && $value[1] === '-') {
316                    $option = \substr($value, 2);
317                    if (\str_contains($option, '=')) {
318                        [$option, $value] = \explode('=', $option, 2);
319                        $this->options[$option] = $value;
320                        continue;
321                    }
322                    $this->options[$option] = true;
323                    continue;
324                }
325                foreach (\str_split(\substr($value, 1)) as $item) {
326                    $this->options[$item] = true;
327                }
328                continue;
329            }
330            //$endOptions = true;
331            $this->arguments[] = $value;
332        }
333    }
334
335    /**
336     * @param string $command
337     *
338     * @see https://someguyjeremy.com/2017/07/adventures-in-parsing-strings-to-argv-in-php.html
339     *
340     * @return array<int,string>
341     */
342    #[Pure]
343    public static function commandToArgs(string $command) : array
344    {
345        $charCount = \strlen($command);
346        $argv = [];
347        $arg = '';
348        $inDQuote = false;
349        $inSQuote = false;
350        for ($i = 0; $i < $charCount; $i++) {
351            $char = $command[$i];
352            if ($char === ' ' && ! $inDQuote && ! $inSQuote) {
353                if ($arg !== '') {
354                    $argv[] = $arg;
355                }
356                $arg = '';
357                continue;
358            }
359            if ($inSQuote && $char === "'") {
360                $inSQuote = false;
361                continue;
362            }
363            if ($inDQuote && $char === '"') {
364                $inDQuote = false;
365                continue;
366            }
367            if ($char === '"' && ! $inSQuote) {
368                $inDQuote = true;
369                continue;
370            }
371            if ($char === "'" && ! $inDQuote) {
372                $inSQuote = true;
373                continue;
374            }
375            $arg .= $char;
376        }
377        $argv[] = $arg;
378        return $argv;
379    }
380}