ArgumentParser::parseArgumentArray()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 12
ccs 10
cts 10
cp 1
rs 9.6111
cc 5
nc 5
nop 3
crap 5
1
<?php
2
3
namespace clearice\argparser;
4
5
6
/**
7
 * For parsing arguments in ClearIce
8
 *
9
 * @package clearice\argparser
10
 */
11
class ArgumentParser
12
{
13
    /**
14
     * Description to put on top of the help message.
15
     * @var string
16
     */
17
    private $description;
18
19
    /**
20
     * A little message for the foot of the help message.
21
     * @var string
22
     */
23
    private $footer;
24
25
    /**
26
     * The name of the application.
27
     * @var string
28
     */
29
    private $name;
30
31
    /**
32
     * Commands that the application can execute.
33
     * @var array
34
     */
35
    private $commands = [];
36
37
    /**
38
     * A cache of all the options added.
39
     * The array keys represents a concatenation of the command and either the short or long name of the option. Elements
40
     * in this array will be the same as those in the options property. However, options that have both a short and long
41
     * name would appear twice.
42
     * @var array
43
     */
44
    private $optionsCache = [];
45
46
    /**
47
     * All the possible options for arguments.
48
     * @var array
49
     */
50
    private $options = [];
51
52
    /**
53
     * An instance of the help generator.
54
     * @var HelpMessageGenerator
55
     */
56
    private $helpGenerator;
57
58
    /**
59
     * An instance of the validator.
60
     * @var Validator
61
     */
62
    private $validator;
63
64
    /**
65
     * A reference to a function to be called for exitting the entire application.
66
     * @var callable
67
     */
68
    private $exitFunction;
69
70
    /**
71
     * Flag raised when help has been enabled.
72
     * @var bool
73
     */
74
    private $helpEnabled = false;
75
76
    /**
77
     * ArgumentParser constructor.
78
     *
79
     * @param ValidatorInterface $validator
80
     * @param HelpGeneratorInterface $helpWriter
81
     */
82 16
    public function __construct(HelpGeneratorInterface $helpWriter = null, ValidatorInterface $validator = null)
83
    {
84 16
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
85 16
        $this->validator = $validator ?? new Validator();
86 16
        $this->exitFunction = function ($code) { exit($code); };
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
87 16
    }
88
89
    /**
90
     * Add an option to the option cache for easy access through associative arrays.
91
     * The option cache associates arguments with their options.
92
     *
93
     * @param string $identifier
94
     * @param mixed $option
95
     * @throws OptionExistsException
96
     */
97 15
    private function addToOptionCache(string $identifier, $option) : void
98
    {
99 15
        if (!isset($option[$identifier])) {
100 2
            return;
101
        }
102 15
        $cacheKey = "${option['command']}${option[$identifier]}";
103 15
        if (!isset($this->optionsCache[$cacheKey])) {
104 15
            $this->optionsCache[$cacheKey] = $option;
105
        } else {
106 2
            throw new OptionExistsException(
107 2
                "An argument option with $identifier {$option['command']} {$option[$identifier]} already exists."
108
            );
109
        }
110 15
    }
111
112
    /**
113
     * @param string $command
114
     * @param string $name
115
     * @return mixed
116
     * @throws InvalidArgumentException
117
     */
118 8
    private function retrieveOptionFromCache(string $command, string $name)
119
    {
120 8
        $key = $command . $name;
121 8
        if(isset($this->optionsCache[$key])) {
122 7
            return $this->optionsCache[$key];
123 1
        } else if(isset($this->optionsCache[$name]) && $this->optionsCache[$name]['command'] == "") {
124 1
            return $this->optionsCache[$name];
125
        } else{
126
            throw new InvalidArgumentException("Unknown option '$name'. Please run with `--help` for more information on valid options.");
127
        }
128
    }
129
130
    /**
131
     * Add an option to be parsed.
132
     * Arguments are presented as a structured array with the following possible keys.
133
     *
134
     *  name: The name of the option prefixed with a double dash --
135
     *  short_name: A shorter single character option prefixed with a single dash -
136
     *  type: Required for all options that take values. An option specified without a type is considered to be a
137
     *        boolean flag.
138
     *  repeats: A boolean value that states whether the option can be repeated or not. Repeatable options are returned
139
     *        as arrays.
140
     *  default: A default value for the option.
141
     *  help: A help message for the option
142
     *
143
     * @param array $option
144
     * @throws OptionExistsException
145
     * @throws InvalidArgumentDescriptionException
146
     * @throws UnknownCommandException
147
     */
148 16
    public function addOption(array $option): void
149
    {
150 16
        $this->validator->validateOption($option, $this->commands);
151 15
        $option['repeats'] = $option['repeats'] ?? false;
152
        // Save a copy of the original command definition, so it can be spread out if it's an array.
153 15
        $commands = $option['command'] ?? '';
154 15
        foreach(is_array($commands) ? $commands : [$commands] as $command) {
155 15
            $option['command'] = $command;
156 15
            $this->options[] = $option;
157 15
            $this->addToOptionCache('name', $option);
158 15
            $this->addToOptionCache('short_name', $option);
159
        }
160 15
    }
161
162
    /**
163
     * @param $arguments
164
     * @param $argPointer
165
     * @return mixed
166
     * @throws InvalidValueException
167
     */
168 5
    private function getNextValueOrFail($arguments, &$argPointer, $name)
169
    {
170 5
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
171 3
            $argPointer++;
172 3
            return $arguments[$argPointer];
173
        } else {
174 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
175
        }
176
    }
177
178
    /**
179
     * Parse a long argument that is prefixed with a double dash "--"
180
     *
181
     * @param $arguments
182
     * @param $argPointer
183
     * @throws InvalidValueException
184
     * @throws InvalidArgumentException
185
     */
186 6
    private function parseLongArgument($command, $arguments, &$argPointer, &$output)
187
    {
188 6
        $string = substr($arguments[$argPointer], 2);
189 6
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
190 6
        $option = $this->retrieveOptionFromCache($command, $matches['name']);
191 6
        $value = true;
192
193 6
        if (isset($option['type'])) {
194 4
            if ($matches['equal'] === '=') {
195 1
                $value = $matches['value'];
196
            } else {
197 4
                $value = $this->getNextValueOrFail($arguments, $argPointer, $matches['name']);
198
            }
199
        }
200
201 5
        $this->assignValue($option, $output, $option['name'], $value);
202 5
    }
203
204
    /**
205
     * Parse a short argument that is prefixed with a single dash '-'
206
     *
207
     * @param $command
208
     * @param $arguments
209
     * @param $argPointer
210
     * @throws InvalidValueException
211
     * @throws InvalidArgumentException
212
     */
213 3
    private function parseShortArgument($command, $arguments, &$argPointer, &$output)
214
    {
215 3
        $argument = $arguments[$argPointer];
216 3
        $option = $this->retrieveOptionFromCache($command, substr($argument, 1, 1));
217 3
        $value = true;
218
219 3
        if (isset($option['type'])) {
220 3
            if (substr($argument, 2) != "") {
221 2
                $value = substr($argument, 2);
222
            } else {
223 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
224
            }
225
        }
226
227 2
        $this->assignValue($option, $output, $option['name'], $value);
228 2
    }
229
230 6
    private function assignValue($option, &$output, $key, $value)
231
    {
232 6
        if($option['repeats']) {
233 1
            $output[$key] = isset($output[$key]) ? array_merge($output[$key], [$value]) : [$value];
234
        } else {
235 5
            $output[$key] = $value;
236
        }
237 6
    }
238
239
    /**
240
     * @param $arguments
241
     * @param $argPointer
242
     * @param $output
243
     * @throws InvalidValueException
244
     * @throws InvalidArgumentException
245
     */
246 8
    private function parseArgumentArray($arguments, &$argPointer, &$output)
247
    {
248 8
        $numArguments = count($arguments);
249 8
        $command = $output['__command'] ?? '';
250 8
        for (; $argPointer < $numArguments; $argPointer++) {
251 8
            $arg = $arguments[$argPointer];
252 8
            if (substr($arg, 0, 2) == "--") {
253 6
                $this->parseLongArgument($command, $arguments, $argPointer, $output);
254 3
            } else if ($arg[0] == '-') {
255 3
                $this->parseShortArgument($command, $arguments, $argPointer, $output);
256
            } else {
257 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
258
            }
259
        }
260 6
    }
261
262
    /**
263
     * @param $output
264
     * @throws HelpMessageRequestedException
265
     */
266 6
    private function maybeShowHelp($output)
267
    {
268 6
        if ((isset($output['help']) && $output['help'] && $this->helpEnabled)) {
269 1
            print $this->getHelpMessage($output['__command'] ?? null);
270 1
            throw new HelpMessageRequestedException();
271
        }
272
273 5
        if(isset($output['__command']) && $output['__command'] == 'help' && $this->helpEnabled) {
274
            print $this->getHelpMessage($output['__args'][0] ?? null);
275
        }
276 5
    }
277
278 8
    private function parseCommand($arguments, &$argPointer, &$output)
279
    {
280 8
        if (count($arguments) > 1 && isset($this->commands[$arguments[$argPointer]])) {
281 2
            $output["__command"] = $arguments[$argPointer];
282 2
            $argPointer++;
283
        }
284 8
    }
285
286
    /**
287
     * @param $parsed
288
     */
289 5
    private function fillInDefaults(&$parsed)
290
    {
291 5
        foreach($this->options as $option) {
292 5
            if(!isset($parsed[$option['name']]) && isset($option['default']) && ($option['command'] == ($parsed['__command'] ?? "") || $option['command'] == '')) {
293 1
                $parsed[$option['name']] = $option['default'];
294
            }
295
        }
296 5
    }
297
298
    /**
299
     * A function called to exit the application whenever there's a parsing error or after requested help has been
300
     * displayed/
301
     *
302
     * @param callable $exit
303
     */
304 1
    public function setExitCallback(callable $exit)
305
    {
306 1
        $this->exitFunction = $exit;
307 1
    }
308
309
    /**
310
     * Parses command line arguments and return a structured array of options and their associated values.
311
     *
312
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
313
     * @return array
314
     * @throws InvalidValueException
315
     */
316 8
    public function parse($arguments = null)
317
    {
318
        try{
319 8
            global $argv;
320 8
            $arguments = $arguments ?? $argv;
321 8
            $argPointer = 1;
322 8
            $parsed = [];
323 8
            $this->name = $this->name ?? $arguments[0];
324 8
            $this->parseCommand($arguments, $argPointer, $parsed);
325 8
            $this->parseArgumentArray($arguments, $argPointer, $parsed);
326 6
            $this->maybeShowHelp($parsed);
327 5
            $this->validator->validateArguments($this->options, $parsed);
328 5
            $this->fillInDefaults($parsed);
329 5
            $parsed['__executed'] = $this->name;
330 5
            return $parsed;
331 3
        } catch (HelpMessageRequestedException $exception) {
332 1
            ($this->exitFunction)(0);
333 2
        } catch (InvalidArgumentException $exception) {
334
            print $exception->getMessage() . PHP_EOL;
335
            ($this->exitFunction)(1024);
336
        }
337 1
    }
338
339
    /**
340
     * Enables help messages so they are shown automatically when the appropriate argument (`--help` or `help`) is passed.
341
     * This method also allows you to optionally pass the name of the application, a description header for the help 
342
     * message and a footer.
343
     *
344
     * @param string $name The name of the application binary
345
     * @param string $description A description to be displayed on top of the help message
346
     * @param string $footer A footer message to be displayed after the help message
347
     *
348
     * @throws InvalidArgumentDescriptionException
349
     * @throws OptionExistsException
350
     * @throws UnknownCommandException
351
     */
352 2
    public function enableHelp(string $description = null, string $footer = null, string $name = null) : void
353
    {
354 2
        global $argv;
355 2
        $this->name = $name ?? $argv[0];
356 2
        $this->description = $description;
357 2
        $this->footer = $footer;
358 2
        $this->helpEnabled = true;
359 2
        $this->addOption([
360 2
            'name' => 'help',
361
            'short_name' => 'h', 'help' => "display this help message"
362
        ]);
363 2
        if($this->commands) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->commands of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
364 1
            $this->addCommand(['name' => 'help', 'help' => "display help for any command. Usage: {$this->name} help [command]"]);
365 1
            foreach($this->commands as $command) {
366 1
                $this->addOption([
367 1
                    'name' => 'help',
368 1
                    'help' => 'display this help message',
369 1
                    'command' => $command['name']
370
                ]);
371
            }
372
        }
373 2
    }
374
375 2
    public function getHelpMessage($command = '')
376
    {
377 2
        return $this->helpGenerator->generate(
378 2
            $this->name, $command ?? null,
379 2
            ['options' => $this->options, 'commands' => $this->commands],
380 2
            $this->description, $this->footer
381
        );
382
    }
383
384
    /**
385
     * @param $command
386
     * @throws CommandExistsException
387
     * @throws InvalidArgumentDescriptionException
388
     */
389 7
    public function addCommand($command)
390
    {
391 7
        $this->validator->validateCommand($command, $this->commands);
392 7
        $this->commands[$command['name']] = $command;
393 7
    }
394
}
395