Completed
Pull Request — master (#2)
by James Ekow Abaka
08:00
created

ArgumentParser   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 99.26%

Importance

Changes 0
Metric Value
wmc 48
lcom 1
cbo 6
dl 0
loc 405
ccs 135
cts 136
cp 0.9926
rs 8.4864
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A addUnknownOption() 0 4 1
A addParsedOption() 0 4 1
A addParsedMultiOption() 0 4 1
B parse() 0 26 3
A getLookAheadArgument() 0 4 1
A moveToNextArgument() 0 4 1
A parseArgument() 0 15 3
A aggregateOptions() 0 14 4
A showHelp() 0 13 4
B showStrictErrors() 0 11 5
A getArgumentWithCommand() 0 15 4
A getCommand() 0 16 4
A stringCommandToArray() 0 4 1
A addCommands() 0 9 3
A addOptions() 0 4 1
A addGroups() 0 6 2
A setStrict() 0 4 1
A addHelp() 0 15 2
A setUsage() 0 4 1
A setDescription() 0 4 1
A setFootnote() 0 4 1
A getHelpMessage() 0 12 1
A getIO() 0 4 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 $io;
143
144 27
    public function __construct(ConsoleIO $io)
145
    {
146 27
        $this->options = new Options();
147 27
        $this->io = $io;
148 27
    }
149
150
    /**
151
     * Adds an unknown option to the list of unknown options currently held in
152
     * the parser.
153
     * 
154
     * @param string $unknown
155
     */
156 4
    public function addUnknownOption($unknown)
157
    {
158 4
        $this->unknownOptions[] = $unknown;
159 4
    }
160
161
    /**
162
     * Adds a known parsed option to the list of parsed options currently held
163
     * in the parser.
164
     * @param string $key The option.
165
     * @param string $value The value asigned to the option.
166
     */
167 20
    public function addParsedOption($key, $value)
168
    {
169 20
        $this->parsedOptions[$key] = $value;
170 20
    }
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
    {
179 1
        $this->parsedOptions[$key][] = $value;
180 1
    }
181
182
    /**
183
     * Parse the command line arguments and return a structured array which
184
     * represents the options which were interpreted by ClearIce. The array
185
     * returned has the following structure.
186
     * 
187
     * 
188
     * @global type $argv
189
     * @return array
190
     */
191 24
    public function parse()
192
    {
193 24
        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...
194 24
        $this->arguments = $argv;
195 24
        $executed = array_shift($this->arguments);
196 24
        $this->command = $this->getCommand();
197
198 24
        $this->parsedOptions = $this->options->getDefaults($this->command);
199 24
        $this->parsedOptions['__command__'] = $this->command;
200 24
        if(isset($this->commands[$this->command]['data'])) {
201
            $this->parsedOptions['data'] = $this->commands[$this->command]['data'];
202
        }
203 24
        $this->longOptionParser = new parsers\LongOptionParser($this, $this->options->getMap());
204 24
        $this->shortOptionParser = new parsers\ShortOptionParser($this, $this->options->getMap());
205
206 24
        $numArguments = count($this->arguments);
207 24
        for ($this->argumentPointer = 0; $this->argumentPointer < $numArguments; $this->argumentPointer++) {
208 23
            $this->parseArgument($this->arguments[$this->argumentPointer]);
209
        }
210
211 24
        $this->showStrictErrors($executed);
212 24
        $this->aggregateOptions();
213 24
        $this->showHelp();
214
215 24
        return $this->parsedOptions;
216
    }
217
218 6
    public function getLookAheadArgument()
219
    {
220 6
        return $this->arguments[$this->argumentPointer + 1];
221
    }
222
223 6
    public function moveToNextArgument()
224
    {
225 6
        $this->argumentPointer++;
226 6
    }
227
228 23
    private function parseArgument($argument)
229
    {
230 23
        $success = false;
231
        // Try to parse the argument for a command
232 23
        if ($this->parsedOptions['__command__'] != '__default__') {
233 4
            parsers\BaseParser::setLogUnknowns(false);
234 4
            $success = $this->getArgumentWithCommand($argument, $this->parsedOptions['__command__']);
235
        }
236
237
        // If not succesful get argument for the __default__ command.
238 23
        if ($success === false) {
239 21
            parsers\BaseParser::setLogUnknowns(true);
240 21
            $this->getArgumentWithCommand($argument, '__default__');
241
        }
242 23
    }
243
244 24
    private function aggregateOptions()
245
    {
246 24
        if (count($this->standAlones)) {
247 5
            $this->parsedOptions['stand_alones'] = $this->standAlones;
248
        }
249 24
        if (count($this->unknownOptions)) {
250 4
            $this->parsedOptions['unknowns'] = $this->unknownOptions;
251
        }
252
253
        // Hide the __default__ command from the outside world
254 24
        if ($this->parsedOptions['__command__'] == '__default__') {
255 20
            unset($this->parsedOptions['__command__']);
256
        }
257 24
    }
258
259 24
    private function showHelp()
260
    {
261 24
        if (isset($this->parsedOptions['help'])) {
262 3
            $this->io->output($this->getHelpMessage(
263 3
                    isset($this->parsedOptions['__command__']) ?
264 3
                        $this->parsedOptions['__command__'] : null
265
                )
266
            );
267
        }
268 24
        if ($this->command == 'help') {
269 2
            $this->io->output($this->getHelpMessage($this->standAlones[0]));
270
        }
271 24
    }
272
273 24
    private function showStrictErrors($executed)
274
    {
275 24
        if ($this->strict && count($this->unknownOptions) > 0) {
276 2
            foreach ($this->unknownOptions as $unknown) {
277 2
                $this->io->error("$executed: invalid option -- {$unknown}\n");
278
            }
279 2
            if ($this->hasHelp) {
280 1
                $this->io->error("Try `$executed --help` for more information\n");
281
            }
282
        }
283 24
    }
284
285 23
    private function getArgumentWithCommand($argument, $command)
286
    {
287 23
        $return = true;
288 23
        if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)(=)(?<value>.*)/i', $argument, $matches)) {
289 7
            $return = $this->longOptionParser->parse($matches['option'], $matches['value'], $command);
290 23
        } else if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)/i', $argument, $matches)) {
291 12
            $return = $this->longOptionParser->parse($matches['option'], true, $command);
292 15
        } else if (preg_match('/^(-)(?<option>[a-zA-z0-9](.*))/i', $argument, $matches)) {
293 13
            $this->shortOptionParser->parse($matches['option'], $command);
294 13
            $return = true;
295
        } else {
296 5
            $this->standAlones[] = $argument;
297
        }
298 23
        return $return;
299
    }
300
301 24
    private function getCommand()
302
    {
303 24
        $commands = array_keys($this->commands);
304 24
        if (count($commands) > 0 && count($this->arguments) > 0) {
305 6
            $command = array_search($this->arguments[0], $commands);
306 6
            if ($command === false) {
307 2
                $command = '__default__';
308
            } else {
309 4
                $command = $this->arguments[0];
310 6
                array_shift($this->arguments);
311
            }
312
        } else {
313 18
            $command = '__default__';
314
        }
315 24
        return $command;
316
    }
317
318 6
    private function stringCommandToArray($command)
319
    {
320 6
        return ['help' => '', 'command' => $command];
321
    }
322
323
    /**
324
     * Add commands for parsing. 
325
     * This method takes many arguments with each representing a unique command. 
326
     * 
327
     * @param String
328
     * @see ClearIce::addCommands()
329
     */
330 6
    public function addCommands($commands)
331
    {
332 6
        foreach ($commands as $command) {
333 6
            if (is_string($command)) {
334 6
                $command = $this->stringCommandToArray($command);
335
            }
336 6
            $this->commands[$command['command']] = $command;
337
        }
338 6
    }
339
340
    /**
341
     * Add options to be recognized. 
342
     * Options could either be strings or structured arrays. Strings define 
343
     * simple options. Structured arrays describe options in deeper details.
344
     */
345 27
    public function addOptions($options)
346
    {
347 27
        $this->options->add($options);
348 27
    }
349
350 1
    public function addGroups($groups)
351
    {
352 1
        foreach ($groups as $group) {
353 1
            $this->groups[$group['group']] = $group;
354
        }
355 1
    }
356
357
    /**
358
     * Sets whether the parser should be strict or not. A strict parser would 
359
     * terminate the application if it doesn't understand any options. A 
360
     * not-strict parser would just return the unknown options it encountered 
361
     * and expect the application to deal with it appropriately.     
362
     * 
363
     * @param boolean $strict A boolean value for the strictness state
364
     */
365 2
    public function setStrict($strict)
366
    {
367 2
        $this->strict = $strict;
368 2
    }
369
370
    /**
371
     * Adds the two automatic help options. A long one represented by --help and
372
     * a short one represented by -h.
373
     */
374 8
    public function addHelp()
375
    {
376 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...
377
378 8
        $this->addOptions([['short' => 'h', 'long' => 'help', 'help' => 'Shows this help message']]);
379
380 8
        if (count($this->commands) > 0) {
381 4
            $this->addCommands([[
382 4
                'command' => 'help',
383 4
                'help' => "Displays specific help for any of the given commands.\nusage: {$argv[0]} help [command]"  
384
            ]]);
385
        }
386
387 8
        $this->hasHelp = true;
388 8
    }
389
390
    /**
391
     * Set the usage text which forms part of the help text.
392
     * @param string|array $usage
393
     */
394 7
    public function setUsage($usage)
395
    {
396 7
        $this->usage = $usage;
397 7
    }
398
399
    /**
400
     * Set the description text shown on top of the help text.
401
     * @param string $description
402
     */
403 7
    public function setDescription($description)
404
    {
405 7
        $this->description = $description;
406 7
    }
407
408
    /**
409
     * Set the footnote text shown at the bottom of the help text.
410
     * @param string $footnote
411
     */
412 7
    public function setFootnote($footnote)
413
    {
414 7
        $this->footnote = $footnote;
415 7
    }
416
417
    /**
418
     * Returns the help message as a string.
419
     * 
420
     * @global type $argv
421
     * @return string
422
     */
423 8
    public function getHelpMessage($command = null)
424
    {
425 8
        return (string) new HelpMessage([
426 8
            'options' => $this->options,
427 8
            'description' => $this->description,
428 8
            'usage' => $this->usage,
429 8
            'commands' => $this->commands,
430 8
            'footnote' => $this->footnote,
431 8
            'command' => $command,
432 8
            'groups' => $this->groups
433
        ]);
434
    }
435
    
436 3
    public function getIO() : ConsoleIO
437
    {
438 3
        return $this->io;
439
    }
440
441
}
442