Parser   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Importance

Changes 9
Bugs 4 Features 2
Metric Value
eloc 76
c 9
b 4
f 2
dl 0
loc 311
rs 9.44
wmc 37

15 Methods

Rating   Name   Duplication   Size   Complexity  
A args() 0 3 1
A optionFor() 0 9 3
A allArguments() 0 3 1
A values() 0 10 2
A registered() 0 3 1
A set() 0 11 3
A parse() 0 25 5
A allOptions() 0 3 1
A register() 0 12 2
A validate() 0 16 4
A setValue() 0 6 1
A ifAlreadyRegistered() 0 6 3
A parseArgs() 0 15 4
A parseOptions() 0 11 5
A __get() 0 3 1
1
<?php
2
3
/*
4
 * This file is part of the PHP-CLI package.
5
 *
6
 * (c) Jitendra Adhikari <[email protected]>
7
 *     <https://github.com/adhocore>
8
 *
9
 * Licensed under MIT license.
10
 */
11
12
namespace Ahc\Cli\Input;
13
14
use Ahc\Cli\Exception\InvalidParameterException;
15
use Ahc\Cli\Exception\RuntimeException;
16
use Ahc\Cli\Helper\Normalizer;
17
18
/**
19
 * Argv parser for the cli.
20
 *
21
 * @author  Jitendra Adhikari <[email protected]>
22
 * @license MIT
23
 *
24
 * @link    https://github.com/adhocore/cli
25
 */
26
abstract class Parser
27
{
28
    /** @var string|null The last seen variadic option name */
29
    protected $_lastVariadic;
30
31
    /** @var Normalizer */
32
    protected $_normalizer;
33
34
    /** @var Option[] Registered options */
35
    private $_options = [];
36
37
    /** @var Argument[] Registered arguments */
38
    private $_arguments = [];
39
40
    /** @var array Parsed values indexed by option name */
41
    private $_values = [];
42
43
    /**
44
     * Parse the argv input.
45
     *
46
     * @param array $argv The first item is ignored.
47
     *
48
     * @throws \RuntimeException When argument is missing or invalid.
49
     *
50
     * @return self
51
     */
52
    public function parse(array $argv): self
53
    {
54
        $this->_normalizer = new Normalizer;
55
56
        \array_shift($argv);
57
58
        $argv    = $this->_normalizer->normalizeArgs($argv);
59
        $count   = \count($argv);
60
        $literal = false;
61
62
        for ($i = 0; $i < $count; $i++) {
63
            list($arg, $nextArg) = [$argv[$i], $argv[$i + 1] ?? null];
64
65
            if ($arg === '--') {
66
                $literal = true;
67
            } elseif ($arg[0] !== '-' || $literal) {
68
                $this->parseArgs($arg);
69
            } else {
70
                $i += (int) $this->parseOptions($arg, $nextArg);
71
            }
72
        }
73
74
        $this->validate();
75
76
        return $this;
77
    }
78
79
    /**
80
     * Parse single arg.
81
     *
82
     * @param string $arg
83
     *
84
     * @return mixed
85
     */
86
    protected function parseArgs(string $arg)
87
    {
88
        if ($this->_lastVariadic) {
89
            return $this->set($this->_lastVariadic, $arg, true);
90
        }
91
92
        if (!$argument = \reset($this->_arguments)) {
93
            return $this->set(null, $arg);
94
        }
95
96
        $this->setValue($argument, $arg);
97
98
        // Otherwise we will always collect same arguments again!
99
        if (!$argument->variadic()) {
100
            \array_shift($this->_arguments);
101
        }
102
    }
103
104
    /**
105
     * Parse an option, emit its event and set value.
106
     *
107
     * @param string      $arg
108
     * @param string|null $nextArg
109
     *
110
     * @return bool Whether to eat next arg.
111
     */
112
    protected function parseOptions(string $arg, string $nextArg = null): bool
113
    {
114
        $value = \substr($nextArg, 0, 1) === '-' ? null : $nextArg;
115
116
        if (null === $option  = $this->optionFor($arg)) {
117
            return $this->handleUnknown($arg, $value);
118
        }
119
120
        $this->_lastVariadic = $option->variadic() ? $option->attributeName() : null;
121
122
        return false === $this->emit($option->attributeName(), $value) ? false : $this->setValue($option, $value);
123
    }
124
125
    /**
126
     * Get matching option by arg (name) or null.
127
     *
128
     * @param string $arg
129
     *
130
     * @return Option|null
131
     */
132
    protected function optionFor(string $arg)
133
    {
134
        foreach ($this->_options as $option) {
135
            if ($option->is($arg)) {
136
                return $option;
137
            }
138
        }
139
140
        return null;
141
    }
142
143
    /**
144
     * Handle Unknown option.
145
     *
146
     * @param string      $arg   Option name
147
     * @param string|null $value Value
148
     *
149
     * @throws \RuntimeException When given arg is not registered and allow unkown flag is not set.
150
     *
151
     * @return mixed If true it will indicate that value has been eaten.
152
     */
153
    abstract protected function handleUnknown(string $arg, string $value = null);
154
155
    /**
156
     * Emit the event with value.
157
     *
158
     * @param string $event Event name (is option name technically)
159
     * @param mixed  $value Value (is option value technically)
160
     *
161
     * @return mixed
162
     */
163
    abstract protected function emit(string $event, $value = null);
164
165
    /**
166
     * Sets value of an option.
167
     *
168
     * @param Parameter   $parameter
169
     * @param string|null $value
170
     *
171
     * @return bool Indicating whether it has eaten adjoining arg to its right.
172
     */
173
    protected function setValue(Parameter $parameter, string $value = null): bool
174
    {
175
        $name  = $parameter->attributeName();
176
        $value = $this->_normalizer->normalizeValue($parameter, $value);
177
178
        return $this->set($name, $value, $parameter->variadic());
179
    }
180
181
    /**
182
     * Set a raw value.
183
     *
184
     * @param mixed $key
185
     * @param mixed $value
186
     * @param bool  $variadic
187
     *
188
     * @return bool
189
     */
190
    protected function set($key, $value, bool $variadic = false): bool
191
    {
192
        if (null === $key) {
193
            $this->_values[] = $value;
194
        } elseif ($variadic) {
195
            $this->_values[$key] = \array_merge($this->_values[$key], (array) $value);
196
        } else {
197
            $this->_values[$key] = $value;
198
        }
199
200
        return !\in_array($value, [true, false, null], true);
201
    }
202
203
    /**
204
     * Validate if all required arguments/options have proper values.
205
     *
206
     * @throw RuntimeException If value missing for required ones.
207
     */
208
    protected function validate()
209
    {
210
        /** @var Parameter[] $missingItems */
211
        $missingItems = \array_filter($this->_options + $this->_arguments, function ($item) {
212
            /* @var Parameter $item */
213
            return $item->required() && \in_array($this->_values[$item->attributeName()], [null, []]);
214
        });
215
216
        foreach ($missingItems as $item) {
217
            list($name, $label) = [$item->name(), 'Argument'];
218
            if ($item instanceof Option) {
219
                list($name, $label) = [$item->long(), 'Option'];
220
            }
221
222
            throw new RuntimeException(
223
                \sprintf('%s "%s" is required', $label, $name)
224
            );
225
        }
226
    }
227
228
    /**
229
     * Register a new argument/option.
230
     *
231
     * @param Parameter $param
232
     *
233
     * @return void
234
     */
235
    protected function register(Parameter $param)
236
    {
237
        $this->ifAlreadyRegistered($param);
238
239
        $name = $param->attributeName();
240
        if ($param instanceof Option) {
241
            $this->_options[$name] = $param;
242
        } else {
243
            $this->_arguments[$name] = $param;
244
        }
245
246
        $this->set($name, $param->default());
247
    }
248
249
    /**
250
     * What if the given name is already registered.
251
     *
252
     * @param Parameter $param
253
     *
254
     * @throws \InvalidArgumentException If given param name is already registered.
255
     */
256
    protected function ifAlreadyRegistered(Parameter $param)
257
    {
258
        if ($this->registered($param->attributeName())) {
259
            throw new InvalidParameterException(\sprintf(
260
                'The parameter "%s" is already registered',
261
                $param instanceof Option ? $param->long() : $param->name()
262
            ));
263
        }
264
    }
265
266
    /**
267
     * Check if either argument/option with given name is registered.
268
     *
269
     * @param string $attribute
270
     *
271
     * @return bool
272
     */
273
    public function registered($attribute): bool
274
    {
275
        return \array_key_exists($attribute, $this->_values);
276
    }
277
278
    /**
279
     * Get all options.
280
     *
281
     * @return Option[]
282
     */
283
    public function allOptions(): array
284
    {
285
        return $this->_options;
286
    }
287
288
    /**
289
     * Get all arguments.
290
     *
291
     * @return Argument[]
292
     */
293
    public function allArguments(): array
294
    {
295
        return $this->_arguments;
296
    }
297
298
    /**
299
     * Magic getter for specific value by its key.
300
     *
301
     * @param string $key
302
     *
303
     * @return mixed
304
     */
305
    public function __get(string $key)
306
    {
307
        return $this->_values[$key] ?? null;
308
    }
309
310
    /**
311
     * Get the command arguments i.e which is not an option.
312
     *
313
     * @return array
314
     */
315
    public function args(): array
316
    {
317
        return \array_diff_key($this->_values, $this->_options);
318
    }
319
320
    /**
321
     * Get values indexed by camelized attribute name.
322
     *
323
     * @param bool $withDefaults
324
     *
325
     * @return array
326
     */
327
    public function values(bool $withDefaults = true): array
328
    {
329
        $values            = $this->_values;
330
        $values['version'] = $this->_version ?? null;
0 ignored issues
show
Bug Best Practice introduced by
The property _version does not exist on Ahc\Cli\Input\Parser. Since you implemented __get, consider adding a @property annotation.
Loading history...
331
332
        if (!$withDefaults) {
333
            unset($values['help'], $values['version'], $values['verbosity']);
334
        }
335
336
        return $values;
337
    }
338
}
339