Completed
Pull Request — master (#6)
by James Ekow Abaka
02:00
created

ArgumentParser::parseLongArgument()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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