Completed
Push — master ( bd0acc...9933b8 )
by James Ekow Abaka
04:06
created

ArgumentParser   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 420
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 97.5%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 6
dl 0
loc 420
ccs 156
cts 160
cp 0.975
rs 7.0642
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A setContainer() 0 3 1
A addUnknownOption() 0 3 1
A addParsedOption() 0 3 1
A addParsedMultiOption() 0 3 1
A parse() 0 22 2
A getLookAheadArgument() 0 3 1
A moveToNextArgument() 0 3 1
A executeCommand() 0 12 4
A parseArgument() 0 14 3
A aggregateOptions() 0 11 4
A showHelp() 0 14 4
B showStrictErrors() 0 12 5
A getArgumentWithCommand() 0 14 4
A getCommand() 0 15 4
A stringCommandToArray() 0 21 4
A addCommands() 0 8 3
A addOptions() 0 4 1
A addGroups() 0 6 2
A setStrict() 0 3 1
A addHelp() 0 22 2
A setUsage() 0 3 1
A setDescription() 0 3 1
A setFootnote() 0 3 1
A getHelpMessage() 0 11 1

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
/*
4
 * ClearIce CLI Argument Parser
5
 * Copyright (c) 2012-2014 James Ekow Abaka Ainooson
6
 * 
7
 * Permission is hereby granted, free of charge, to any person obtaining
8
 * a copy of this software and associated documentation files (the
9
 * "Software"), to deal in the Software without restriction, including
10
 * without limitation the rights to use, copy, modify, merge, publish,
11
 * distribute, sublicense, and/or sell copies of the Software, and to
12
 * permit persons to whom the Software is furnished to do so, subject to
13
 * the following conditions:
14
 * 
15
 * The above copyright notice and this permission notice shall be
16
 * included in all copies or substantial portions of the Software.
17
 * 
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
25
 * 
26
 * @author James Ainooson <[email protected]>
27
 * @copyright Copyright 2012-2014 James Ekow Abaka Ainooson
28
 * @license MIT
29
 */
30
31
namespace clearice;
32
33
/**
34
 * Class responsible for parsing individual arguments.
35
 * @internal Composed into the static ClearIce class
36
 */
37
class ArgumentParser
38
{
39
40
    /**
41
     * An array of all the options that are available to the parser. Unlike the
42
     * ClearIce::$optionsMap parameter, this paramter just lists all the options
43
     * and their parameters. Any option added through the ArgumentParser::addOptions()
44
     * parameter is just appended to this array.
45
     * 
46
     * @var \clearice\Options
47
     */
48
    private $options = [];
49
50
    /**
51
     * Specifies whether the parser should be strict about errors or not. 
52
     * 
53
     * @var boolean
54
     */
55
    private $strict = false;
56
57
    /**
58
     * A flag raised when the parser already has the automatic help option 
59
     * added. This is used to prevent multiple help options.
60
     * 
61
     * @var boolean
62
     */
63
    private $hasHelp;
64
65
    /**
66
     * The usage instructions for the application displayed as part of the
67
     * automatically generated help message. This message is usually printed
68
     * after the description.
69
     * 
70
     * @var array|string
71
     */
72
    private $usage;
73
74
    /**
75
     * The description displayed on top of the help message just after the
76
     * usage instructions.
77
     * 
78
     * @var string
79
     */
80
    private $description;
81
82
    /**
83
     * A footnote displayed at the bottom of the help message.
84
     * 
85
     * @var string
86
     */
87
    private $footnote;
88
89
    /**
90
     * An array of all the commands that the script can work with.
91
     * @var array
92
     */
93
    private $commands = [];
94
    private $groups = [];
95
96
    /**
97
     * The current command being run.
98
     * @var string
99
     */
100
    private $command;
101
102
    /**
103
     * Holds all the options that have already been parsed and recognized.
104
     * @var array
105
     */
106
    private $parsedOptions = [];
107
108
    /**
109
     * Holds all the options that have been parsed but are unknown.
110
     * @var array
111
     */
112
    private $unknownOptions = [];
113
114
    /**
115
     * Options that are standing alone.
116
     * @var array
117
     */
118
    private $standAlones = [];
119
120
    /**
121
     * An instance of the long option parser used for the parsing of long options
122
     * which are preceed with a double dash "--".
123
     * @var \clearice\parsers\LongOptionParser
124
     */
125
    private $longOptionParser;
126
127
    /**
128
     * An instance of the short option parser used for the parsing of short optoins
129
     * which are preceeded with a single dash "-".
130
     * @var \clearice\parsers\ShortOptionParser
131
     */
132
    private $shortOptionParser;
133
134
    /**
135
     * The arguments that were passed through the command line to the script or
136
     * application.
137
     * 
138
     * @var array
139
     */
140
    private $arguments = [];
141
    private $argumentPointer;
142
    private $container;
143
144 29
    public function __construct() {
145 29
        $this->options = new Options();
146 29
    }
147
    
148
    public function setContainer($container) {
149
        $this->container = $container;
150
    }
151
152
    /**
153
     * Adds an unknown option to the list of unknown options currently held in
154
     * the parser.
155
     * 
156
     * @param string $unknown
157
     */
158 4
    public function addUnknownOption($unknown) {
159 4
        $this->unknownOptions[] = $unknown;
160 4
    }
161
162
    /**
163
     * Adds a known parsed option to the list of parsed options currently held
164
     * in the parser.
165
     * @param string $key The option.
166
     * @param string $value The value asigned to the option.
167
     */
168 22
    public function addParsedOption($key, $value) {
169 22
        $this->parsedOptions[$key] = $value;
170 22
    }
171
172
    /**
173
     * Adds a new value of a multi option.
174
     * @param string $key The option.
175
     * @param string $value The value to be appended to the list.
176
     */
177 1
    public function addParsedMultiOption($key, $value) {
178 1
        $this->parsedOptions[$key][] = $value;
179 1
    }
180
181
    /**
182
     * Parse the command line arguments and return a structured array which
183
     * represents the options which were interpreted by ClearIce. The array
184
     * returned has the following structure.
185
     * 
186
     * 
187
     * @global type $argv
188
     * @return array
189
     */
190 27
    public function parse() {
191 27
        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...
192 27
        $this->arguments = $argv;
193 27
        $executed = array_shift($this->arguments);
194 27
        $this->command = $this->getCommand();
195
196 27
        $this->parsedOptions = $this->options->getDefaults($this->command);
197 27
        $this->parsedOptions['__command__'] = $this->command;
198 27
        $this->longOptionParser = new parsers\LongOptionParser($this, $this->options->getMap());
199 27
        $this->shortOptionParser = new parsers\ShortOptionParser($this, $this->options->getMap());
200
201 27
        $numArguments = count($this->arguments);
202 27
        for ($this->argumentPointer = 0; $this->argumentPointer < $numArguments; $this->argumentPointer++) {
203 25
            $this->parseArgument($this->arguments[$this->argumentPointer]);
204
        }
205
206 27
        $this->showStrictErrors($executed);
207 27
        $this->aggregateOptions();
208 27
        $this->showHelp();
209
210 27
        return $this->executeCommand($this->command, $this->parsedOptions);
211
    }
212
213 7
    public function getLookAheadArgument() {
214 7
        return $this->arguments[$this->argumentPointer + 1];
215
    }
216
217 7
    public function moveToNextArgument() {
218 7
        $this->argumentPointer++;
219 7
    }
220
221 27
    private function executeCommand($command, $options) {
222 27
        if ($command === '__default__' || !isset($this->commands[$command]['class'])) {
223 25
            return $options;
224
        } else {
225 2
            $class = $this->commands[$command]['class'];
226 2
            $object = $this->container ? $this->container->resolve($class) : new $class();
227 2
            $parameters = $options;
228 2
            unset($parameters['__command__']);
229 2
            $object->run($parameters);
230 2
            return $options;
231
        }
232
    }
233
234 25
    private function parseArgument($argument) {
235 25
        $success = false;
236
        // Try to parse the argument for a command
237 25
        if ($this->parsedOptions['__command__'] != '__default__') {
238 6
            parsers\BaseParser::setLogUnknowns(false);
239 6
            $success = $this->getArgumentWithCommand($argument, $this->parsedOptions['__command__']);
240
        }
241
242
        // If not succesful get argument for the __default__ command.
243 25
        if ($success === false) {
244 22
            parsers\BaseParser::setLogUnknowns(true);
245 22
            $this->getArgumentWithCommand($argument, '__default__');
246
        }
247 25
    }
248
249 27
    private function aggregateOptions() {
250 27
        if (count($this->standAlones))
251 5
            $this->parsedOptions['stand_alones'] = $this->standAlones;
252 27
        if (count($this->unknownOptions))
253 4
            $this->parsedOptions['unknowns'] = $this->unknownOptions;
254
255
        // Hide the __default__ command from the outside world
256 27
        if ($this->parsedOptions['__command__'] == '__default__') {
257 20
            unset($this->parsedOptions['__command__']);
258
        }
259 27
    }
260
261 27
    private function showHelp() {
262 27
        if (isset($this->parsedOptions['help'])) {
263 3
            ClearIce::output($this->getHelpMessage(
264 3
                            isset($this->parsedOptions['__command__']) ?
265 3
                                    $this->parsedOptions['__command__'] : null
266
                    )
267
            );
268 3
            ClearIce::terminate();
269
        }
270 27
        if ($this->command == 'help') {
271 2
            ClearIce::output($this->getHelpMessage($this->standAlones[0]));
272 2
            ClearIce::terminate();
273
        }
274 27
    }
275
276 27
    private function showStrictErrors($executed) {
277 27
        if ($this->strict && count($this->unknownOptions) > 0) {
278 2
            foreach ($this->unknownOptions as $unknown) {
279 2
                ClearIce::error("$executed: invalid option -- {$unknown}\n");
280
            }
281
282 2
            if ($this->hasHelp) {
283 1
                ClearIce::error("Try `$executed --help` for more information\n");
284
            }
285 2
            ClearIce::terminate();
286
        }
287 27
    }
288
289 25
    private function getArgumentWithCommand($argument, $command) {
290 25
        $return = true;
291 25
        if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)(=)(?<value>.*)/i', $argument, $matches)) {
292 7
            $return = $this->longOptionParser->parse($matches['option'], $matches['value'], $command);
293 25
        } else if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)/i', $argument, $matches)) {
294 14
            $return = $this->longOptionParser->parse($matches['option'], true, $command);
295 15
        } else if (preg_match('/^(-)(?<option>[a-zA-z0-9](.*))/i', $argument, $matches)) {
296 13
            $this->shortOptionParser->parse($matches['option'], $command);
297 13
            $return = true;
298
        } else {
299 5
            $this->standAlones[] = $argument;
300
        }
301 25
        return $return;
302
    }
303
304 27
    private function getCommand() {
305 27
        $commands = array_keys($this->commands);
306 27
        if (count($commands) > 0 && count($this->arguments) > 0) {
307 9
            $command = array_search($this->arguments[0], $commands);
308 9
            if ($command === false) {
309 2
                $command = '__default__';
310
            } else {
311 7
                $command = $this->arguments[0];
312 9
                array_shift($this->arguments);
313
            }
314
        } else {
315 18
            $command = '__default__';
316
        }
317 27
        return $command;
318
    }
319
320 8
    private function stringCommandToArray($command) {
321 8
        if (class_exists($command)) {
322
            try {
323 1
                $className = $command;
324 1
                $method = new \ReflectionMethod($className, 'getCommandOptions');
325 1
                $command = $method->invoke(null);
326 1
                $command['class'] = $className;
327 1
                foreach ($command['options'] ?? [] as $option) {
328 1
                    $option['command'] = $command['command'];
329 1
                    ClearIce::addOptions($option);
330
                }
331 1
                return $command;
332
            } catch (\ReflectionException $e) {
333
                // Do nothing
334
            }
335
        }
336
        return [
337 7
            'help' => '',
338 7
            'command' => $command
339
        ];
340
    }
341
342
    /**
343
     * Add commands for parsing. 
344
     * This method takes many arguments with each representing a unique command. 
345
     * 
346
     * @param String
347
     * @see ClearIce::addCommands()
348
     */
349 9
    public function addCommands() {
350 9
        foreach (func_get_args() as $command) {
351 9
            if (is_string($command)) {
352 8
                $command = $this->stringCommandToArray($command);
353
            }
354 9
            $this->commands[$command['command']] = $command;
355
        }
356 9
    }
357
358
    /**
359
     * Add options to be recognized. 
360
     * Options could either be strings or structured arrays. Strings define 
361
     * simple options. Structured arrays describe options in deeper details.
362
     */
363 29
    public function addOptions() {
364 29
        $options = func_get_args();
365 29
        $this->options->add($options);
366 29
    }
367
368 1
    public function addGroups() {
369 1
        $groups = func_get_args();
370 1
        foreach ($groups as $group) {
371 1
            $this->groups[$group['group']] = $group;
372
        }
373 1
    }
374
375
    /**
376
     * Sets whether the parser should be strict or not. A strict parser would 
377
     * terminate the application if it doesn't understand any options. A 
378
     * not-strict parser would just return the unknown options it encountered 
379
     * and expect the application to deal with it appropriately.     
380
     * 
381
     * @param boolean $strict A boolean value for the strictness state
382
     */
383 2
    public function setStrict($strict) {
384 2
        $this->strict = $strict;
385 2
    }
386
387
    /**
388
     * Adds the two automatic help options. A long one represented by --help and
389
     * a short one represented by -h.
390
     */
391 8
    public function addHelp() {
392 8
        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...
393
394 8
        $this->addOptions(
395
                array(
396 8
                    'short' => 'h',
397
                    'long' => 'help',
398
                    'help' => 'Shows this help message'
399
                )
400
        );
401
402 8
        if (count($this->commands) > 0) {
403 4
            $this->addCommands(
404
                    array(
405 4
                        'command' => 'help',
406 4
                        'help' => "Displays specific help for any of the given commands.\nusage: {$argv[0]} help [command]"
407
                    )
408
            );
409
        }
410
411 8
        $this->hasHelp = true;
412 8
    }
413
414
    /**
415
     * Set the usage text which forms part of the help text.
416
     * @param string|array $usage
417
     */
418 7
    public function setUsage($usage) {
419 7
        $this->usage = $usage;
420 7
    }
421
422
    /**
423
     * Set the description text shown on top of the help text.
424
     * @param string $description
425
     */
426 7
    public function setDescription($description) {
427 7
        $this->description = $description;
428 7
    }
429
430
    /**
431
     * Set the footnote text shown at the bottom of the help text.
432
     * @param string $footnote
433
     */
434 7
    public function setFootnote($footnote) {
435 7
        $this->footnote = $footnote;
436 7
    }
437
438
    /**
439
     * Returns the help message as a string.
440
     * 
441
     * @global type $argv
442
     * @return string
443
     */
444 8
    public function getHelpMessage($command) {
445 8
        return (string) new HelpMessage([
446 8
            'options' => $this->options,
447 8
            'description' => $this->description,
448 8
            'usage' => $this->usage,
449 8
            'commands' => $this->commands,
450 8
            'footnote' => $this->footnote,
451 8
            'command' => $command,
452 8
            'groups' => $this->groups
453
        ]);
454
    }
455
456
}
457