Completed
Push — master ( cc2e80...f6611d )
by James Ekow Abaka
03:07
created

ArgumentParser::stringCommandToArray()   B

Complexity

Conditions 5
Paths 11

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 14.8579

Importance

Changes 4
Bugs 0 Features 2
Metric Value
c 4
b 0
f 2
dl 0
loc 22
ccs 4
cts 15
cp 0.2667
rs 8.6737
cc 5
eloc 14
nc 11
nop 1
crap 14.8579
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
     * An array of all the options that are available to the parser. Unlike the
41
     * ClearIce::$optionsMap parameter, this paramter just lists all the options
42
     * and their parameters. Any option added through the ArgumentParser::addOptions()
43
     * parameter is just appended to this array.
44
     * 
45
     * @var array
46
     */
47
    private $options = [];
48
49
    /**
50
     * Specifies whether the parser should be strict about errors or not. 
51
     * 
52
     * @var boolean
53
     */
54
    private $strict = false;
55
56
    /**
57
     * A flag raised when the parser already has the automatic help option 
58
     * added. This is used to prevent multiple help options.
59
     * 
60
     * @var boolean
61
     */
62
    private $hasHelp;
63
64
    /**
65
     * The usage instructions for the application displayed as part of the
66
     * automatically generated help message. This message is usually printed
67
     * after the description.
68
     * 
69
     * @var array|string
70
     */
71
    private $usage;
72
73
    /**
74
     * The description displayed on top of the help message just after the
75
     * usage instructions.
76
     * 
77
     * @var string
78
     */
79
    private $description;
80
81
    /**
82
     * A footnote displayed at the bottom of the help message.
83
     * 
84
     * @var string
85
     */
86
    private $footnote;
87
88
    /**
89
     * An array of all the commands that the script can work with.
90
     * @var array
91
     */
92
    private $commands = [];
93
    
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
    
143 27
    public function __construct()
144
    {
145 27
        $this->options = new Options();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \clearice\Options() of type object<clearice\Options> is incompatible with the declared type array of property $options.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
146 27
    }
147
148
    /**
149
     * Adds an unknown option to the list of unknown options currently held in
150
     * the parser.
151
     * 
152
     * @param string $unknown
153
     */
154 4
    public function addUnknownOption($unknown)
155
    {
156 4
        $this->unknownOptions[] = $unknown;
157 4
    }
158
159
    /**
160
     * Adds a known parsed option to the list of parsed options currently held
161
     * in the parser.
162
     * @param string $key The option.
163
     * @param string $value The value asigned to the option.
164
     */
165 21
    public function addParsedOption($key, $value)
166
    {
167 21
        $this->parsedOptions[$key] = $value;
168 21
    }
169
170
    /**
171
     * Adds a new value of a multi option.
172
     * @param string $key The option.
173
     * @param string $value The value to be appended to the list.
174
     */
175 1
    public function addParsedMultiOption($key, $value)
176
    {
177 1
        $this->parsedOptions[$key][] = $value;
178 1
    }
179
180
    /**
181
     * Parse the command line arguments and return a structured array which
182
     * represents the options which were interpreted by ClearIce. The array
183
     * returned has the following structure.
184
     * 
185
     * 
186
     * @global type $argv
187
     * @return array
188
     */
189 25
    public function parse()
190
    {
191 25
        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 25
        $this->arguments = $argv;
193 25
        $executed = array_shift($this->arguments);
194 25
        $this->command = $this->getCommand();
195
196 25
        $this->parsedOptions = $this->options->getDefaults();
0 ignored issues
show
Bug introduced by
The method getDefaults cannot be called on $this->options (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
197 25
        $this->parsedOptions['__command__'] = $this->command;
198 25
        $this->longOptionParser = new parsers\LongOptionParser($this, $this->options->getMap());
0 ignored issues
show
Bug introduced by
The method getMap cannot be called on $this->options (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
199 25
        $this->shortOptionParser = new parsers\ShortOptionParser($this, $this->options->getMap());
0 ignored issues
show
Bug introduced by
The method getMap cannot be called on $this->options (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
200
201 25
        for ($this->argumentPointer = 0; $this->argumentPointer < count($this->arguments); $this->argumentPointer++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
202 24
            $this->parseArgument($this->arguments[$this->argumentPointer]);
203 24
        }
204
205 25
        $this->showStrictErrors($executed);
206 25
        $this->aggregateOptions();
207 25
        $this->showHelp();
208
209 25
        return $this->executeCommand($this->command, $this->parsedOptions);
210
    }
211
212 6
    public function getLookAheadArgument()
213
    {
214 6
        return $this->arguments[$this->argumentPointer + 1];
215
    }
216
217 6
    public function moveToNextArgument()
218
    {
219 6
        $this->argumentPointer++;
220 6
    }
221
222 25
    private function executeCommand($command, $options)
223
    {
224 25
        if ($command === '__default__' || !isset($this->commands[$command]['class'])) {
225 24
            return $options;
226
        } else {
227 1
            $class = $this->commands[$command]['class'];
228 1
            $object = new $class();
229 1
            unset($options['__command__']);
230 1
            $object->run($options);
231 1
            return $options;
232
        }
233
    }
234
235 24
    private function parseArgument($argument)
236
    {
237 24
        $success = FALSE;
238 24
        if ($this->parsedOptions['__command__'] != '__default__') {
239 5
            parsers\BaseParser::setLogUnknowns(false);
240 5
            $success = $this->getArgumentWithCommand($argument, $this->parsedOptions['__command__']);
241 5
        }
242
243 24
        if ($success === false) {
244 22
            parsers\BaseParser::setLogUnknowns(true);
245 22
            $this->getArgumentWithCommand($argument, '__default__');
246 22
        }
247 24
    }
248
249 25
    private function aggregateOptions()
250
    {
251 25
        if (count($this->standAlones))
252 25
            $this->parsedOptions['stand_alones'] = $this->standAlones;
253 25
        if (count($this->unknownOptions))
254 25
            $this->parsedOptions['unknowns'] = $this->unknownOptions;
255
256
        // Hide the __default__ command from the outside world
257 25
        if ($this->parsedOptions['__command__'] == '__default__') {
258 20
            unset($this->parsedOptions['__command__']);
259 20
        }
260 25
    }
261
262 25
    private function showHelp()
263
    {
264 25
        if (isset($this->parsedOptions['help'])) {
265 3
            ClearIce::output($this->getHelpMessage(
266 3
                            isset($this->parsedOptions['__command__']) ?
267 3
                                    $this->parsedOptions['__command__'] : null
268 3
                    )
269 3
            );
270 3
            ClearIce::terminate();
271 3
        }
272 25
        if ($this->command == 'help') {
273 2
            ClearIce::output($this->getHelpMessage($this->standAlones[0]));
274 2
            ClearIce::terminate();
275 2
        }
276 25
    }
277
278 25
    private function showStrictErrors($executed)
279
    {
280 25
        if ($this->strict && count($this->unknownOptions) > 0) {
281 2
            foreach ($this->unknownOptions as $unknown) {
282 2
                ClearIce::error("$executed: invalid option -- {$unknown}\n");
283 2
            }
284
285 2
            if ($this->hasHelp) {
286 1
                ClearIce::error("Try `$executed --help` for more information\n");
287 1
            }
288 2
            ClearIce::terminate();
289 2
        }
290 25
    }
291
292 24
    private function getArgumentWithCommand($argument, $command)
293
    {
294 24
        $return = true;
295 24
        if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)(=)(?<value>.*)/i', $argument, $matches)) {
296 7
            $parser = $this->longOptionParser;
297 7
            $return = $parser->parse($matches['option'], $matches['value'], $command);
298 24
        } else if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)/i', $argument, $matches)) {
299 13
            $parser = $this->longOptionParser;
300 13
            $return = $parser->parse($matches['option'], true, $command);
301 24
        } else if (preg_match('/^(-)(?<option>[a-zA-z0-9](.*))/i', $argument, $matches)) {
302 13
            $parser = $this->shortOptionParser;
303 13
            $parser->parse($matches['option'], $command);
304 13
            $return = true;
305 13
        } else {
306 5
            $this->standAlones[] = $argument;
307
        }
308 24
        return $return;
309
    }
310
311 25
    private function getCommand()
312
    {
313 25
        $commands = array_keys($this->commands);
314 25
        if (count($commands) > 0 && count($this->arguments) > 0) {
315 7
            $command = array_search($this->arguments[0], $commands);
316 7
            if ($command === false) {
317 2
                $command = '__default__';
318 2
            } else {
319 5
                $command = $this->arguments[0];
320 5
                array_shift($this->arguments);
321
            }
322 7
        } else {
323 18
            $command = '__default__';
324
        }
325 25
        return $command;
326
    }
327
328 6
    private function stringCommandToArray($command)
329
    {
330 6
        if(class_exists($command)) {
331
            try{
332
                $method = new \ReflectionMethod($command, 'getCommandOptions');
333
                $command = $method->invoke(null);
334
                if(is_array($command['options'])) {
335
                    foreach($command['options'] as $option) {
336
                        $option['command'] = $command['command'];
337
                        ClearIce::addOptions($option);
338
                    }
339
                }
340
                return $command;
341
            } catch (\ReflectionException $e) {
342
                // Do nothing
343
            }
344
        } 
345
        return [
346 6
            'help' => '',
347
            'command' => $command
348 6
        ];                   
349
    }
350
351
    /**
352
     * Add commands for parsing. 
353
     * This method takes many arguments with each representing a unique command. 
354
     * 
355
     * @param String
356
     * @see ClearIce::addCommands()
357
     */
358 7
    public function addCommands()
359
    {
360 7
        foreach (func_get_args() as $command) {
361 7
            if (is_string($command)) {
362 6
                $this->commands[$command] = $this->stringCommandToArray($command);
363 6
            } else {
364 7
                $this->commands[$command['command']] = $command;
365
            }
366 7
        }
367 7
    }
368
369
    /**
370
     * Add options to be recognized. 
371
     * Options could either be strings or structured arrays. Strings define 
372
     * simple options. Structured arrays describe options in deeper details.
373
     */
374 28
    public function addOptions()
375
    {
376 28
        $options = func_get_args();
377 28
        $this->options->add($options);
0 ignored issues
show
Bug introduced by
The method add cannot be called on $this->options (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
378 28
    }
379
    
380 1
    public function addGroups()
381
    {
382 1
        $groups = func_get_args();
383 1
        foreach($groups as $group) {
384 1
            $this->groups[$group['group']] = $group;
385 1
        }
386 1
    }
387
388
    /**
389
     * Sets whether the parser should be strict or not. A strict parser would 
390
     * terminate the application if it doesn't understand any options. A 
391
     * not-strict parser would just return the unknown options it encountered 
392
     * and expect the application to deal with it appropriately.     
393
     * 
394
     * @param boolean $strict A boolean value for the strictness state
395
     */
396 2
    public function setStrict($strict)
397
    {
398 2
        $this->strict = $strict;
399 2
    }
400
401
    /**
402
     * Adds the two automatic help options. A long one represented by --help and
403
     * a short one represented by -h.
404
     */
405 8
    public function addHelp()
406
    {
407 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...
408
409 8
        $this->addOptions(
410
                array(
411 8
                    'short' => 'h',
412 8
                    'long' => 'help',
413
                    'help' => 'Shows this help message'
414 8
                )
415 8
        );
416
417 8
        if (count($this->commands) > 0) {
418 4
            $this->addCommands(
419
                    array(
420 4
                        'command' => 'help',
421 4
                        'help' => "Displays specific help for any of the given commands.\nusage: {$argv[0]} help [command]"
422 4
                    )
423 4
            );
424 4
        }
425
426 8
        $this->hasHelp = true;
427 8
    }
428
429
    /**
430
     * Set the usage text which forms part of the help text.
431
     * @param string|array $usage
432
     */
433 7
    public function setUsage($usage)
434
    {
435 7
        $this->usage = $usage;
436 7
    }
437
438
    /**
439
     * Set the description text shown on top of the help text.
440
     * @param string $description
441
     */
442 7
    public function setDescription($description)
443
    {
444 7
        $this->description = $description;
445 7
    }
446
447
    /**
448
     * Set the footnote text shown at the bottom of the help text.
449
     * @param string $footnote
450
     */
451 7
    public function setFootnote($footnote)
452
    {
453 7
        $this->footnote = $footnote;
454 7
    }
455
456
    /**
457
     * Returns the help message as a string.
458
     * 
459
     * @global type $argv
460
     * @return string
461
     */
462 8
    public function getHelpMessage($command)
463
    {
464 8
        return (string) new HelpMessage([
465 8
            'options' => $this->options,
466 8
            'description' => $this->description,
467 8
            'usage' => $this->usage,
468 8
            'commands' => $this->commands,
469 8
            'footnote' => $this->footnote,
470 8
            'command' => $command,
471 8
            'groups' => $this->groups
472 8
        ]);
473
    }
474
475
    private function fillCommand($command)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
476
    {
477
        return $command;
478
    }
479
}
480