Completed
Push — dev ( 71b6ae...27ea21 )
by James Ekow Abaka
01:35
created

ArgumentParser::retrieveOptionFromCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.1481

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 2
cts 3
cp 0.6667
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 2
crap 2.1481
1
<?php
2
3
namespace clearice\argparser;
4
use clearice\utils\ProgramControl;
5
6
7
/**
8
 * Class ArgumentParser
9
 *
10
 * @package clearice\argparser
11
 */
12
class ArgumentParser
13
{
14
    private $description;
15
16
    private $footer;
17
18
    private $name;
19
20
    private $commands = [];
21
22
    /**
23
     * @var array
24
     */
25
    private $optionsCache = [];
26
27
    /**
28
     * All the possible options for arguments.
29
     * @var array
30
     */
31
    private $options = [];
32
33
    /**
34
     * An instance of the help generator.
35
     * @var HelpMessageGenerator
36
     */
37
    private $helpGenerator;
38
39
    private $programControl;
40
41
    private $helpEnabled = false;
42
43 14
    public function __construct($helpWriter = null, $programControl = null)
44
    {
45 14
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
46 14
        $this->programControl = $programControl ?? new ProgramControl();
47 14
    }
48
49
    /**
50
     * Add a value to the available possible options for later parsing.
51
     *
52
     * @param string $key
53
     * @param array $value
54
     * @throws OptionExistsException
55
     */
56 13
    private function addToOptionCache(string $key, array $value) : void
57
    {
58 13
        if (!isset($value[$key])) {
59 2
            return;
60
        }
61
        $cacheKey = "${value['command']}${value[$key]}";
62
        if (!isset($this->optionsCache[$cacheKey])) {
63 13
            $this->optionsCache[$cacheKey] = $value;
64
        } else {
65 2
            throw new OptionExistsException(
66 2
                "An argument option with $key {$value['command']} {$value[$key]} already exists."
67
            );
68
        }
69 13
    }
70
71
    /**
72
     * @param string $key
73
     * @param string $name
74
     * @return mixed
75
     * @throws InvalidArgumentException
76
     */
77
    private function retrieveOptionFromCache(string $key, string $name)
78
    {
79 7
        if(!isset($this->optionsCache[$key])) {
80
            throw new InvalidArgumentException("Unknown option '$name'. Please run with `--help` for more information on valid options.");
81
        }
82 7
        return $this->optionsCache[$key];
83
    }
84
85
    /**
86
     * @param array $option
87
     * @throws InvalidArgumentDescriptionException
88
     * @throws UnknownCommandException
89
     */
90
    private function validateOption($option) : void
91
    {
92 14
        if (!isset($option['name'])) {
93 1
            throw new InvalidArgumentDescriptionException("Argument must have a name");
94
        }
95 13
        if (isset($option['command']) && !isset($this->commands[$option['command']])) {
96 1
            throw new UnknownCommandException("The command {$option['command']} is unknown");
97
        }
98 13
    }
99
100
    /**
101
     * Add an option to be parsed.
102
     * Arguments are presented as a structured array with the following possible keys.
103
     *
104
     *  name: The name of the option prefixed with a double dash --
105
     *  short_name: A shorter single character option prefixed with a single dash -
106
     *  type: Required for all options that take values. An option specified without a type is considered to be a
107
     *        boolean flag.
108
     *  repeats: A boolean value that states whether the option can be repeated or not. Repeatable options are returned
109
     *        as arrays.
110
     *  default: A default value for the option.
111
     *  help: A help message for the option
112
     *
113
     * @param array $option
114
     * @throws OptionExistsException
115
     * @throws InvalidArgumentDescriptionException
116
     * @throws UnknownCommandException
117
     */
118
    public function addOption(array $option): void
119
    {
120 14
        $this->validateOption($option);
121 13
        $option['command'] = $option['command'] ?? '';
122 13
        $option['repeats'] = $option['repeats'] ?? false;
123 13
        $this->options[] = $option;
124 13
        $this->addToOptionCache('name', $option);
125 13
        $this->addToOptionCache('short_name', $option);
126 13
    }
127
128
    /**
129
     * @param $arguments
130
     * @param $argPointer
131
     * @return mixed
132
     * @throws InvalidValueException
133
     */
134
    private function getNextValueOrFail($arguments, &$argPointer, $name)
135
    {
136 5
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
137 3
            $argPointer++;
138 3
            return $arguments[$argPointer];
139
        } else {
140 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
141
        }
142
    }
143
144
    /**
145
     * Parse a long argument that is prefixed with a double dash "--"
146
     *
147
     * @param $arguments
148
     * @param $argPointer
149
     * @throws InvalidValueException
150
     * @throws InvalidArgumentException
151
     */
152
    private function parseLongArgument($command, $arguments, &$argPointer, &$output)
153
    {
154 5
        $string = substr($arguments[$argPointer], 2);
155 5
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
156 5
        $key = $command . $matches['name'];
157 5
        $option = $this->retrieveOptionFromCache($key, $matches['name']);
158 5
        $value = true;
159
160 5
        if (isset($option['type'])) {
161 4
            if ($matches['equal'] === '=') {
162 1
                $value = $matches['value'];
163
            } else {
164 4
                $value = $this->getNextValueOrFail($arguments, $argPointer, $matches['name']);
165
            }
166
        }
167
168 4
        $this->assignValue($option, $output, $option['name'], $value);
169 4
    }
170
171
    /**
172
     * Parse a short argument that is prefixed with a single dash '-'
173
     *
174
     * @param $command
175
     * @param $arguments
176
     * @param $argPointer
177
     * @throws InvalidValueException
178
     * @throws InvalidArgumentException
179
     */
180
    private function parseShortArgument($command, $arguments, &$argPointer, &$output)
181
    {
182 3
        $argument = $arguments[$argPointer];
183 3
        $key = $command . substr($argument, 1, 1);
184 3
        $option = $this->retrieveOptionFromCache($key, substr($argument, 1, 1));
185 3
        $value = true;
186
187 3
        if (isset($option['type'])) {
188 3
            if (substr($argument, 2) != "") {
189 2
                $value = substr($argument, 2);
190
            } else {
191 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
192
            }
193
        }
194
195 2
        $this->assignValue($option, $output, $option['name'], $value);
196 2
    }
197
198
    private function assignValue($option, &$output, $key, $value)
199
    {
200 5
        if($option['repeats']) {
201 1
            $output[$key] = isset($output[$key]) ? array_merge($output[$key], [$value]) : [$value];
202
        } else {
203 4
            $output[$key] = $value;
204
        }
205 5
    }
206
207
    /**
208
     * @param $arguments
209
     * @param $argPointer
210
     * @param $output
211
     * @throws InvalidValueException
212
     * @throws InvalidArgumentException
213
     */
214
    private function parseArgumentArray($arguments, &$argPointer, &$output)
215
    {
216 7
        $numArguments = count($arguments);
217 7
        $command = $output['__command'] ?? '';
218 7
        for (; $argPointer < $numArguments; $argPointer++) {
219 7
            $arg = $arguments[$argPointer];
220 7
            if (substr($arg, 0, 2) == "--") {
221 5
                $this->parseLongArgument($command, $arguments, $argPointer, $output);
222 3
            } else if ($arg[0] == '-') {
223 3
                $this->parseShortArgument($command, $arguments, $argPointer, $output);
224
            } else {
225 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
226
            }
227
        }
228 5
    }
229
230
    /**
231
     * @param $output
232
     * @throws HelpMessageRequestedException
233
     */
234
    private function maybeShowHelp($output)
235
    {
236 5
        if ((isset($output['help']) && $output['help'] && $this->helpEnabled)) {
237 1
            print $this->getHelpMessage($output['__command'] ?? null);
238 1
            throw new HelpMessageRequestedException();
239
        }
240 4
    }
241
242
    private function parseCommand($arguments, &$argPointer, &$output)
243
    {
244 7
        if (count($arguments) > 1 && isset($this->commands[$arguments[$argPointer]])) {
245 1
            $output["__command"] = $arguments[$argPointer];
246 1
            $argPointer++;
247
        }
248 7
    }
249
250
    /**
251
     * @param $parsed
252
     * @throws InvalidArgumentException
253
     */
254
    private function fillInDefaults(&$parsed)
255
    {
256 5
        foreach($this->options as $option) {
257 5
            if(!isset($parsed[$option['name']]) && isset($option['default'])) {
258 5
                $parsed[$option['name']] = $option['default'];
259
            }
260
        }
261 5
    }
262
263
    /**
264
     * @throws InvalidArgumentException
265
     */
266
    private function validateRequired()
267
    {
268 5
        $required = [];
269 5
        foreach($this->options as $option) {
270 5
            if(isset($option['required']) && $option['required'] && !isset($parsed[$option['name']])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $parsed seems to never exist and therefore isset should always be false.
Loading history...
271 5
                $required[] = $option['name'];
272
            }
273
        }
274
275 5
        if(!empty(($required))) {
276
            throw new InvalidArgumentException(
277
                sprintf("The following options are required: %s. Pass the --help option for more information about possible options.", implode(", $required"))
0 ignored issues
show
Bug introduced by
', '.$required of type string is incompatible with the type array expected by parameter $pieces of implode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

277
                sprintf("The following options are required: %s. Pass the --help option for more information about possible options.", implode(/** @scrutinizer ignore-type */ ", $required"))
Loading history...
278
            );
279
        }
280
281 5
    }
282
283
    /**
284
     * Parses command line arguments and return a structured array of options and their associated values.
285
     *
286
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
287
     * @return array
288
     * @throws InvalidValueException
289
     */
290
    public function parse($arguments = null)
291
    {
292
        try{
293 7
            global $argv;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
294 7
            $arguments = $arguments ?? $argv;
295 7
            $argPointer = 1;
296 7
            $parsed = [];
297 7
            $this->name = $this->name ?? $arguments[0];
298 7
            $this->parseCommand($arguments, $argPointer, $parsed);
299 7
            $this->parseArgumentArray($arguments, $argPointer, $parsed);
300 5
            $this->validateRequired();
301 5
            $this->fillInDefaults($parsed);
302 5
            $this->maybeShowHelp($parsed);
303 4
            return $parsed;
304 3
        } catch (HelpMessageRequestedException $exception) {
305 1
            $this->programControl->quit();
306 2
        } catch (InvalidArgumentException $exception) {
307
            print $exception->getMessage();
308
            $this->programControl->quit();
309
        }
310 1
    }
311
312
    /**
313
     * Enables help messages so they show automatically.
314
     *
315
     * @param string $name
316
     * @param string $description
317
     * @param string $footer
318
     *
319
     * @throws InvalidArgumentDescriptionException
320
     * @throws OptionExistsException
321
     * @throws UnknownCommandException
322
     */
323
    public function enableHelp(string $description = null, string $footer = null, string $name = null) : void
324
    {
325 2
        $this->name = $name;
326 2
        $this->description = $description;
327 2
        $this->footer = $footer;
328 2
        $this->helpEnabled = true;
329 2
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "display this help message"]);
330 2
        foreach($this->commands as $command) {
331 1
            $this->addOption(['name' => 'help', 'help' => 'display this help message', 'command' => $command['name']]);
332
        }
333 2
    }
334
335
    public function getHelpMessage($command = '')
336
    {
337 2
        return $this->helpGenerator->generate(
338 2
            $this->name, $command ?? null,
339 2
            ['options' => $this->options, 'commands' => $this->commands],
340 2
            $this->description, $this->footer
341
        );
342
    }
343
344
    /**
345
     * @param $command
346
     * @throws CommandExistsException
347
     * @throws InvalidArgumentDescriptionException
348
     */
349
    public function addCommand($command)
350
    {
351 5
        if (!isset($command['name'])) {
352 1
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
353
        }
354 5
        if (isset($this->commands[$command['name']])) {
355 1
            throw new CommandExistsException("Command ${command['name']} already exists.");
356
        }
357 5
        $this->commands[$command['name']] = $command;
358 5
    }
359
}
360