Passed
Push — dev ( ad2891...d9deb8 )
by James Ekow Abaka
01:31
created

ArgumentParser::parseShortArgument()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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

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

82
    public function __construct(/** @scrutinizer ignore-unused */ $validator = null, $helpWriter = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
83
    {
84 14
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
85
        //$this->programControl = $programControl ?? new ProgramControl();
86 14
        $this->validator = new Validator();
87 14
    }
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 13
    private function addToOptionCache(string $identifier, $option) : void
98
    {
99 13
        if (!isset($option[$identifier])) {
100 2
            return;
101
        }
102 13
        $cacheKey = "${option['command']}${option[$identifier]}";
103 13
        if (!isset($this->optionsCache[$cacheKey])) {
104 13
            $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 13
    }
111
112
    /**
113
     * @param string $command
114
     * @param string $name
115
     * @return mixed
116
     * @throws InvalidArgumentException
117
     */
118 7
    private function retrieveOptionFromCache(string $command, string $name)
119
    {
120 7
        $key = $command . $name;
121 7
        if(isset($this->optionsCache[$key])) {
122 6
            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 14
    public function addOption(array $option): void
149
    {
150 14
        $this->validator->validateOption($option, $this->commands);
151 13
        $option['command'] = $option['command'] ?? '';
152 13
        $option['repeats'] = $option['repeats'] ?? false;
153 13
        $this->options[] = $option;
154 13
        $this->addToOptionCache('name', $option);
155 13
        $this->addToOptionCache('short_name', $option);
156 13
    }
157
158
    /**
159
     * @param $arguments
160
     * @param $argPointer
161
     * @return mixed
162
     * @throws InvalidValueException
163
     */
164 5
    private function getNextValueOrFail($arguments, &$argPointer, $name)
165
    {
166 5
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
167 3
            $argPointer++;
168 3
            return $arguments[$argPointer];
169
        } else {
170 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
171
        }
172
    }
173
174
    /**
175
     * Parse a long argument that is prefixed with a double dash "--"
176
     *
177
     * @param $arguments
178
     * @param $argPointer
179
     * @throws InvalidValueException
180
     * @throws InvalidArgumentException
181
     */
182 5
    private function parseLongArgument($command, $arguments, &$argPointer, &$output)
183
    {
184 5
        $string = substr($arguments[$argPointer], 2);
185 5
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
186 5
        $option = $this->retrieveOptionFromCache($command, $matches['name']);
187 5
        $value = true;
188
189 5
        if (isset($option['type'])) {
190 4
            if ($matches['equal'] === '=') {
191 1
                $value = $matches['value'];
192
            } else {
193 4
                $value = $this->getNextValueOrFail($arguments, $argPointer, $matches['name']);
194
            }
195
        }
196
197 4
        $this->assignValue($option, $output, $option['name'], $value);
198 4
    }
199
200
    /**
201
     * Parse a short argument that is prefixed with a single dash '-'
202
     *
203
     * @param $command
204
     * @param $arguments
205
     * @param $argPointer
206
     * @throws InvalidValueException
207
     * @throws InvalidArgumentException
208
     */
209 3
    private function parseShortArgument($command, $arguments, &$argPointer, &$output)
210
    {
211 3
        $argument = $arguments[$argPointer];
212 3
        $option = $this->retrieveOptionFromCache($command, substr($argument, 1, 1));
213 3
        $value = true;
214
215 3
        if (isset($option['type'])) {
216 3
            if (substr($argument, 2) != "") {
217 2
                $value = substr($argument, 2);
218
            } else {
219 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
220
            }
221
        }
222
223 2
        $this->assignValue($option, $output, $option['name'], $value);
224 2
    }
225
226 5
    private function assignValue($option, &$output, $key, $value)
227
    {
228 5
        if($option['repeats']) {
229 1
            $output[$key] = isset($output[$key]) ? array_merge($output[$key], [$value]) : [$value];
230
        } else {
231 4
            $output[$key] = $value;
232
        }
233 5
    }
234
235
    /**
236
     * @param $arguments
237
     * @param $argPointer
238
     * @param $output
239
     * @throws InvalidValueException
240
     * @throws InvalidArgumentException
241
     */
242 7
    private function parseArgumentArray($arguments, &$argPointer, &$output)
243
    {
244 7
        $numArguments = count($arguments);
245 7
        $command = $output['__command'] ?? '';
246 7
        for (; $argPointer < $numArguments; $argPointer++) {
247 7
            $arg = $arguments[$argPointer];
248 7
            if (substr($arg, 0, 2) == "--") {
249 5
                $this->parseLongArgument($command, $arguments, $argPointer, $output);
250 3
            } else if ($arg[0] == '-') {
251 3
                $this->parseShortArgument($command, $arguments, $argPointer, $output);
252
            } else {
253 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
254
            }
255
        }
256 5
    }
257
258
    /**
259
     * @param $output
260
     * @throws HelpMessageRequestedException
261
     */
262 5
    private function maybeShowHelp($output)
263
    {
264 5
        if ((isset($output['help']) && $output['help'] && $this->helpEnabled)) {
265
            print $this->getHelpMessage($output['__command'] ?? null);
266
            throw new HelpMessageRequestedException();
267
        }
268 5
    }
269
270 7
    private function parseCommand($arguments, &$argPointer, &$output)
271
    {
272 7
        if (count($arguments) > 1 && isset($this->commands[$arguments[$argPointer]])) {
273 2
            $output["__command"] = $arguments[$argPointer];
274 2
            $argPointer++;
275
        }
276 7
    }
277
278
    /**
279
     * @param $parsed
280
     * @throws InvalidArgumentException
281
     */
282 5
    private function fillInDefaults(&$parsed)
283
    {
284 5
        foreach($this->options as $option) {
285 5
            if(!isset($parsed[$option['name']]) && isset($option['default']) && $option['command'] == ($parsed['__command'] ?? "")) {
286 1
                $parsed[$option['name']] = $option['default'];
287
            }
288
        }
289 5
    }
290
291
    /**
292
     * Parses command line arguments and return a structured array of options and their associated values.
293
     *
294
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
295
     * @return array
296
     * @throws InvalidValueException
297
     */
298 7
    public function parse($arguments = null)
299
    {
300
        try{
301 7
            global $argv;
302 7
            $arguments = $arguments ?? $argv;
303 7
            $argPointer = 1;
304 7
            $parsed = [];
305 7
            $this->name = $this->name ?? $arguments[0];
306 7
            $this->parseCommand($arguments, $argPointer, $parsed);
307 7
            $this->parseArgumentArray($arguments, $argPointer, $parsed);
308 5
            $this->maybeShowHelp($parsed);
309 5
            $this->validator->validateArguments($this->options, $parsed);
310 5
            $this->fillInDefaults($parsed);
311 5
            $parsed['__executed'] = $this->name;
312 5
            return $parsed;
313 2
        } catch (HelpMessageRequestedException $exception) {
314
            $this->programControl->quit();
315 2
        } catch (InvalidArgumentException $exception) {
316
            print $exception->getMessage() . PHP_EOL;
317
            $this->programControl->quit();
318
        }
319
    }
320
321
    /**
322
     * Enables help messages so they show automatically.
323
     * This method also allows you to optionally pass the name of the application, a description header for the help 
324
     * message and a footer.
325
     *
326
     * @param string $name The name of the application binary
327
     * @param string $description A description to be displayed on top of the help message
328
     * @param string $footer A footer message to be displayed after the help message
329
     *
330
     * @throws InvalidArgumentDescriptionException
331
     * @throws OptionExistsException
332
     * @throws UnknownCommandException
333
     */
334 1
    public function enableHelp(string $description = null, string $footer = null, string $name = null) : void
335
    {
336 1
        $this->name = $name;
337 1
        $this->description = $description;
338 1
        $this->footer = $footer;
339 1
        $this->helpEnabled = true;
340 1
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "display this help message"]);
341 1
        foreach($this->commands as $command) {
342 1
            $this->addOption(['name' => 'help', 'help' => 'display this help message', 'command' => $command['name']]);
343
        }
344 1
    }
345
346 1
    public function getHelpMessage($command = '')
347
    {
348 1
        return $this->helpGenerator->generate(
349 1
            $this->name, $command ?? null,
350 1
            ['options' => $this->options, 'commands' => $this->commands],
351 1
            $this->description, $this->footer
352
        );
353
    }
354
355
    /**
356
     * @param $command
357
     * @throws CommandExistsException
358
     * @throws InvalidArgumentDescriptionException
359
     */
360 6
    public function addCommand($command)
361
    {
362 6
        $this->validator->validateCommand($command, $this->commands);
363 6
        $this->commands[$command['name']] = $command;
364 6
    }
365
}
366