Passed
Push — develop ( add880...26f5dd )
by nguereza
02:36
created

Parser::options()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
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   http://www.iacademy.cf
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
    /**
63
     * The last seen variadic option name
64
     * @var string|null
65
     */
66
    protected ?string $lastVariadic = null;
67
68
    /**
69
     * The list of options
70
     * @var array<Option>
71
     */
72
    protected array $options = [];
73
74
    /**
75
     * The list of arguments
76
     * @var array<Argument>
77
     */
78
    protected array $arguments = [];
79
80
    /**
81
     * Parsed values indexed by option name
82
     * @var array<string|int, mixed>
83
     */
84
    protected array $values = [];
85
86
    /**
87
     * The version
88
     * @var string
89
     */
90
    protected string $version = '';
91
92
93
    /**
94
     * Parse the command line argument.
95
     * @param array<int, string> $argv
96
     * @return $this
97
     */
98
    public function parse(array $argv): self
99
    {
100
        //The first item is ignored.
101
        array_shift($argv);
102
103
        $arguments = Helper::normalizeArguments($argv);
104
105
        $count = count($arguments);
106
        $literal = false;
107
108
        for ($i = 0; $i < $count; $i++) {
109
            list($arg, $nextArg) = [
110
                $arguments[$i],
111
                isset($arguments[$i + 1]) ? $arguments[$i + 1] : null
112
            ];
113
114
            if ($arg === '--') {
115
                $literal = true;
116
            } elseif ($arg[0] !== '-' || $literal) {
117
                $this->parseArgument($arg);
118
            } else {
119
                $i += (int) $this->parseOptions($arg, $nextArg);
120
            }
121
        }
122
123
        $this->validate();
124
125
        return $this;
126
    }
127
128
    /**
129
     * Return all options.
130
     * @return array<Option>
131
     */
132
    public function options(): array
133
    {
134
        return $this->options;
135
    }
136
137
    /**
138
     * Return all arguments.
139
     * @return array<Argument>
140
     */
141
    public function arguments(): array
142
    {
143
        return $this->arguments;
144
    }
145
146
    /**
147
     * Get the command arguments i.e which is not an option.
148
     * @return array<string>
149
     */
150
    public function args(): array
151
    {
152
        return array_diff_key($this->values, $this->options);
153
    }
154
155
    /**
156
     * Get values indexed by camel case attribute name.
157
     * @param bool $withDefaults
158
     * @return array<string|int, mixed>
159
     */
160
    public function values(bool $withDefaults = true): array
161
    {
162
        $values = $this->values;
163
        $values['version'] = $this->version;
164
165
        if (!$withDefaults) {
166
            unset($values['help'], $values['version'], $values['verbosity']);
167
        }
168
169
        return $values;
170
    }
171
172
    /**
173
     * Handle Unknown option
174
     * @param string $arg is option name
175
     * @param string|null $value is option value
176
     * @return mixed If true it will indicate that value has been eaten.
177
     */
178
    abstract protected function handleUnknown(string $arg, $value = null);
179
180
    /**
181
     * Emit the event with value.
182
     * @param string $event is option name
183
     * @param mixed $value is option value
184
     * @return mixed
185
     */
186
    abstract protected function emit(string $event, $value = null);
187
188
    /**
189
     * Parse single argument.
190
     * @param string $arg
191
     * @return mixed
192
     */
193
    protected function parseArgument(string $arg)
194
    {
195
        if ($this->lastVariadic) {
196
            return $this->set($this->lastVariadic, $arg, true);
197
        }
198
199
        /** @var Argument|false $argument */
200
        $argument = reset($this->arguments);
201
        if ($argument === false) {
202
            return $this->set(null, $arg, false);
203
        }
204
205
        $this->setValue($argument, $arg);
206
207
        // Otherwise we will always collect same arguments again!
208
        if (!$argument->isVariadic()) {
209
            array_shift($this->arguments);
210
        }
211
    }
212
213
    /**
214
     * Parse an option, emit its event and set value
215
     * @param string $arg
216
     * @param string|null $nextArg
217
     * @return bool
218
     */
219
    protected function parseOptions(string $arg, ?string $nextArg = null): bool
220
    {
221
        $value = null;
222
        if ($nextArg !== null && substr($nextArg, 0, 1) !== '-') {
223
            $value =  $nextArg;
224
        }
225
226
        $option = $this->getOptionForArgument($arg);
227
        if ($option === null) {
228
            return $this->handleUnknown($arg, $value);
229
        }
230
231
        $this->lastVariadic = $option->isVariadic()
232
                ? $option->getAttributeName()
233
                : null;
234
235
        return $this->emit($option->getAttributeName(), $value) === false
236
                ? false
237
                : $this->setValue($option, $value);
238
    }
239
240
    /**
241
     * Get matching option by argument (name) or null.
242
     * @param string $arg
243
     * @return Option|null
244
     */
245
    protected function getOptionForArgument(string $arg): ?Option
246
    {
247
        foreach ($this->options as $option) {
248
            if ($option->is($arg)) {
249
                return $option;
250
            }
251
        }
252
253
        return null;
254
    }
255
256
    /**
257
     * Set a raw value.
258
     * @param mixed $key
259
     * @param mixed $value
260
     * @param bool $isVariadic
261
     * @return bool
262
     */
263
    protected function set($key, $value, bool $isVariadic = false): bool
264
    {
265
        if ($key === null) {
266
            $this->values[] = $value;
267
        } elseif ($isVariadic) {
268
            $this->values[$key] = array_merge($this->values[$key], (array) $value);
269
        } else {
270
            $this->values[$key] = $value;
271
        }
272
273
        return !in_array($value, [true, false, null], true);
274
    }
275
276
    /**
277
     * Sets value of an option.
278
     * @param Parameter $parameter
279
     * @param string|null $value
280
     * @return bool
281
     */
282
    protected function setValue(Parameter $parameter, ?string $value = null): bool
283
    {
284
        $name = $parameter->getAttributeName();
285
        $normalizedValue = Helper::normalizeValue($parameter, $value);
286
287
        return $this->set($name, $normalizedValue, $parameter->isVariadic());
288
    }
289
290
    /**
291
     * Validate if all required arguments/options have proper values.
292
     * @return void
293
     */
294
    protected function validate(): void
295
    {
296
        /** @var array<Parameter> $missingItems */
297
        $missingItems = array_filter(
298
            $this->options + $this->arguments,
299
            function ($item) {
300
            /** @var Parameter $item */
301
                return $item->isRequired() && in_array(
302
                    $this->values[$item->getAttributeName()],
303
                    [null, []]
304
                );
305
            }
306
        );
307
308
        foreach ($missingItems as $item) {
309
            list($name, $label) = [$item->getName(), 'Argument'];
310
            if ($item instanceof Option) {
311
                list($name, $label) = [$item->getLong(), 'Option'];
312
            }
313
314
            throw new RuntimeException(sprintf(
315
                '%s "%s" is required',
316
                $label,
317
                $name
318
            ));
319
        }
320
    }
321
322
    /**
323
     * Register a new argument/option.
324
     * @param Parameter $parameter
325
     * @return void
326
     */
327
    protected function register(Parameter $parameter): void
328
    {
329
        $this->checkDuplicate($parameter);
330
331
        $name = $parameter->getAttributeName();
332
333
        if ($parameter instanceof Option) {
334
            $this->options[$name] = $parameter;
335
        } elseif ($parameter instanceof Argument) {
336
            $this->arguments[$name] = $parameter;
337
        }
338
339
        $this->set($name, $parameter->getDefault());
340
    }
341
342
    /**
343
     * Remove a registered argument/option.
344
     * @param string $name
345
     * @return void
346
     */
347
    protected function unregister(string $name): void
348
    {
349
        unset(
350
            $this->values[$name],
351
            $this->options[$name],
352
            $this->arguments[$name]
353
        );
354
    }
355
356
    /**
357
     * Check if either argument/option with given name is registered.
358
     * @param string $name
359
     * @return bool
360
     */
361
    protected function isRegistered(string $name): bool
362
    {
363
        return array_key_exists($name, $this->values);
364
    }
365
366
    /**
367
     * What if the given name is already registered.
368
     * @param Parameter $parameter
369
     * @return void
370
     */
371
    protected function checkDuplicate(Parameter $parameter): void
372
    {
373
        if ($this->isRegistered($parameter->getAttributeName())) {
374
            throw new InvalidParameterException(sprintf(
375
                'The parameter [%s] is already registered',
376
                $parameter instanceof Option
377
                    ? $parameter->getLong()
378
                    : $parameter->getName()
379
            ));
380
        }
381
    }
382
}
383