Parser::parse()   A
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 7
nop 1
dl 0
loc 28
rs 9.1111
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Platine Console
5
 *
6
 * Platine Console is a powerful library with support of custom
7
 * style to build command line interface applications
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Console
12
 * Copyright (c) 2017-2020 Jitendra Adhikari
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file Parser.php
35
 *
36
 *  The Input Parser class
37
 *
38
 *  @package    Platine\Console\Input
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   https://www.platine-php.com
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Console\Input;
50
51
use Platine\Console\Exception\InvalidParameterException;
52
use Platine\Console\Exception\RuntimeException;
53
use Platine\Console\Util\Helper;
54
55
/**
56
 * @class Parser
57
 * @package Platine\Console\Input
58
 */
59
abstract class Parser
60
{
61
    /**
62
     * The last seen variadic option name
63
     * @var string|null
64
     */
65
    protected ?string $lastVariadic = null;
66
67
    /**
68
     * The list of options
69
     * @var array<Option>
70
     */
71
    protected array $options = [];
72
73
    /**
74
     * The list of arguments
75
     * @var array<Argument>
76
     */
77
    protected array $arguments = [];
78
79
    /**
80
     * Parsed values indexed by option name
81
     * @var array<string|int, mixed>
82
     */
83
    protected array $values = [];
84
85
    /**
86
     * The version
87
     * @var string
88
     */
89
    protected string $version = '';
90
91
92
    /**
93
     * Parse the command line argument.
94
     * @param array<int, string> $argv
95
     * @return $this
96
     */
97
    public function parse(array $argv): self
98
    {
99
        //The first item is ignored.
100
        array_shift($argv);
101
102
        $arguments = Helper::normalizeArguments($argv);
103
104
        $count = count($arguments);
105
        $literal = false;
106
107
        for ($i = 0; $i < $count; $i++) {
108
            list($arg, $nextArg) = [
109
                $arguments[$i],
110
                isset($arguments[$i + 1]) ? $arguments[$i + 1] : null
111
            ];
112
113
            if ($arg === '--') {
114
                $literal = true;
115
            } elseif ($arg[0] !== '-' || $literal) {
116
                $this->parseArgument($arg);
117
            } else {
118
                $i += (int) $this->parseOptions($arg, $nextArg);
119
            }
120
        }
121
122
        $this->validate();
123
124
        return $this;
125
    }
126
127
    /**
128
     * Return all options.
129
     * @return Option[]
130
     */
131
    public function options(): array
132
    {
133
        return $this->options;
134
    }
135
136
    /**
137
     * Return all arguments.
138
     * @return Argument[]
139
     */
140
    public function arguments(): array
141
    {
142
        return $this->arguments;
143
    }
144
145
    /**
146
     * Get the command arguments i.e which is not an option.
147
     * @return string[]
148
     */
149
    public function args(): array
150
    {
151
        return array_diff_key($this->values, $this->options);
152
    }
153
154
    /**
155
     * Get values indexed by camel case attribute name.
156
     * @param bool $withDefaults
157
     * @return array<string|int, mixed>
158
     */
159
    public function values(bool $withDefaults = true): array
160
    {
161
        $values = $this->values;
162
        $values['version'] = $this->version;
163
164
        if (!$withDefaults) {
165
            unset($values['help'], $values['version'], $values['verbosity']);
166
        }
167
168
        return $values;
169
    }
170
171
    /**
172
     * Handle Unknown option
173
     * @param string $arg is option name
174
     * @param string|null $value is option value
175
     * @return mixed If true it will indicate that value has been eaten.
176
     */
177
    abstract protected function handleUnknown(string $arg, ?string $value = null): mixed;
178
179
    /**
180
     * Emit the event with value.
181
     * @param string $event is option name
182
     * @param mixed $value is option value
183
     * @return mixed
184
     */
185
    abstract protected function emit(string $event, mixed $value = null): mixed;
186
187
    /**
188
     * Parse single argument.
189
     * @param string $arg
190
     * @return mixed
191
     */
192
    protected function parseArgument(string $arg): mixed
193
    {
194
        if ($this->lastVariadic) {
195
            return $this->set($this->lastVariadic, $arg, true);
196
        }
197
198
        /** @var Argument|false $argument */
199
        $argument = reset($this->arguments);
200
        if ($argument === false) {
201
            return $this->set(null, $arg, false);
202
        }
203
204
        $this->setValue($argument, $arg);
205
206
        // Otherwise we will always collect same arguments again!
207
        if (!$argument->isVariadic()) {
208
            array_shift($this->arguments);
209
        }
210
211
        return true;
212
    }
213
214
    /**
215
     * Parse an option, emit its event and set value
216
     * @param string $arg
217
     * @param string|null $nextArg
218
     * @return mixed|bool
219
     */
220
    protected function parseOptions(string $arg, ?string $nextArg = null): mixed
221
    {
222
        $value = null;
223
        if ($nextArg !== null && substr($nextArg, 0, 1) !== '-') {
224
            $value =  $nextArg;
225
        }
226
227
        $option = $this->getOptionForArgument($arg);
228
        if ($option === null) {
229
            return $this->handleUnknown($arg, $value);
230
        }
231
232
        $this->lastVariadic = $option->isVariadic()
233
                ? $option->getAttributeName()
234
                : null;
235
236
        return $this->emit($option->getAttributeName(), $value) === false
237
                ? false
238
                : $this->setValue($option, $value);
239
    }
240
241
    /**
242
     * Get matching option by argument (name) or null.
243
     * @param string $arg
244
     * @return Option|null
245
     */
246
    protected function getOptionForArgument(string $arg): ?Option
247
    {
248
        foreach ($this->options as $option) {
249
            if ($option->is($arg)) {
250
                return $option;
251
            }
252
        }
253
254
        return null;
255
    }
256
257
    /**
258
     * Set a raw value.
259
     * @param mixed $key
260
     * @param mixed $value
261
     * @param bool $isVariadic
262
     * @return bool
263
     */
264
    protected function set(mixed $key, mixed $value, bool $isVariadic = false): bool
265
    {
266
        if ($key === null) {
267
            $this->values[] = $value;
268
        } elseif ($isVariadic) {
269
            $this->values[$key] = array_merge(
270
                isset($this->values[$key]) ? $this->values[$key] : [],
271
                (array) $value
272
            );
273
        } else {
274
            $this->values[$key] = $value;
275
        }
276
277
        return !in_array($value, [true, false, null], true);
278
    }
279
280
    /**
281
     * Sets value of an option.
282
     * @param Parameter $parameter
283
     * @param string|null $value
284
     * @return bool
285
     */
286
    protected function setValue(Parameter $parameter, ?string $value = null): bool
287
    {
288
        $name = $parameter->getAttributeName();
289
        $normalizedValue = Helper::normalizeValue($parameter, $value);
290
291
        return $this->set($name, $normalizedValue, $parameter->isVariadic());
292
    }
293
294
    /**
295
     * Validate if all required arguments/options have proper values.
296
     * @return void
297
     */
298
    protected function validate(): void
299
    {
300
        /** @var Parameter[] $missingItems */
301
        $missingItems = array_filter(
302
            $this->options + $this->arguments,
303
            function (Parameter $item) {
304
                return $item->isRequired() && in_array(
305
                    $this->values[$item->getAttributeName()],
306
                    [null, []]
307
                );
308
            }
309
        );
310
311
        foreach ($missingItems as $item) {
312
            list($name, $label) = [$item->getName(), 'Argument'];
313
            if ($item instanceof Option) {
314
                list($name, $label) = [$item->getLong(), 'Option'];
315
            }
316
317
            throw new RuntimeException(sprintf(
318
                '%s "%s" is required',
319
                $label,
320
                $name
321
            ));
322
        }
323
    }
324
325
    /**
326
     * Register a new argument/option.
327
     * @param Parameter $parameter
328
     * @return void
329
     */
330
    protected function register(Parameter $parameter): void
331
    {
332
        $this->checkDuplicate($parameter);
333
334
        $name = $parameter->getAttributeName();
335
336
        if ($parameter instanceof Option) {
337
            $this->options[$name] = $parameter;
338
        } elseif ($parameter instanceof Argument) {
339
            $this->arguments[$name] = $parameter;
340
        }
341
342
        $this->set($name, $parameter->getDefault());
343
    }
344
345
    /**
346
     * Remove a registered argument/option.
347
     * @param string $name
348
     * @return void
349
     */
350
    protected function unregister(string $name): void
351
    {
352
        unset(
353
            $this->values[$name],
354
            $this->options[$name],
355
            $this->arguments[$name]
356
        );
357
    }
358
359
    /**
360
     * Check if either argument/option with given name is registered.
361
     * @param string $name
362
     * @return bool
363
     */
364
    protected function isRegistered(string $name): bool
365
    {
366
        return array_key_exists($name, $this->values);
367
    }
368
369
    /**
370
     * What if the given name is already registered.
371
     * @param Parameter $parameter
372
     * @return void
373
     */
374
    protected function checkDuplicate(Parameter $parameter): void
375
    {
376
        if ($this->isRegistered($parameter->getAttributeName())) {
377
            throw new InvalidParameterException(sprintf(
378
                'The parameter [%s] is already registered',
379
                $parameter instanceof Option
380
                    ? $parameter->getLong()
381
                    : $parameter->getName()
382
            ));
383
        }
384
    }
385
}
386