Completed
Push — master ( ba24d6...47edb6 )
by James Ekow Abaka
10s
created

ArgumentParser   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Test Coverage

Coverage 97.12%

Importance

Changes 0
Metric Value
wmc 43
eloc 110
dl 0
loc 344
ccs 101
cts 104
cp 0.9712
rs 8.96
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getNextValueOrFail() 0 7 3
A parseArgumentArray() 0 12 5
A fillInDefaults() 0 5 5
A addToOptionCache() 0 11 3
A addCommand() 0 4 1
A parseShortArgument() 0 16 3
A addOption() 0 8 1
A assignValue() 0 6 3
A maybeShowHelp() 0 5 4
A __construct() 0 5 1
A parse() 0 19 3
A enableHelp() 0 9 2
A getHelpMessage() 0 6 1
A parseLongArgument() 0 17 3
A parseCommand() 0 5 3
A retrieveOptionFromCache() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like ArgumentParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArgumentParser, and based on these observations, apply Extract Interface, too.

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