Completed
Push — master ( 3bdcf1...dd2f9b )
by James Ekow Abaka
01:51 queued 11s
created

ArgumentParser::stringCommandToArray()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.009

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 13
cts 14
cp 0.9286
rs 8.5125
c 0
b 0
f 0
cc 5
eloc 16
nc 12
nop 1
crap 5.009
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 \clearice\Options
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 29
    public function __construct()
144
    {
145 29
        $this->options = new Options();
146 29
    }
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 22
    public function addParsedOption($key, $value)
166
    {
167 22
        $this->parsedOptions[$key] = $value;
168 22
    }
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 27
    public function parse()
190
    {
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();
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
    {
215 7
        return $this->arguments[$this->argumentPointer + 1];
216
    }
217
218 7
    public function moveToNextArgument()
219
    {
220 7
        $this->argumentPointer++;
221 7
    }
222
223 27
    private function executeCommand($command, $options)
224
    {
225 27
        if ($command === '__default__' || !isset($this->commands[$command]['class'])) {
226 25
            return $options;
227
        } else {
228 2
            $class = $this->commands[$command]['class'];
229 2
            $object = new $class();
230 2
            $parameters = $options;
231 2
            unset($parameters['__command__']);
232 2
            $object->run($parameters);
233 2
            return $options;
234
        }
235
    }
236
237 25
    private function parseArgument($argument)
238
    {
239 25
        $success = false;
240
        // Try to parse the argument for a command
241 25
        if ($this->parsedOptions['__command__'] != '__default__') {
242 6
            parsers\BaseParser::setLogUnknowns(false);
243 6
            $success = $this->getArgumentWithCommand($argument, $this->parsedOptions['__command__']);
244
        }
245
246
        // If not succesful get argument for the __default__ command.
247 25
        if ($success === false) {
248 22
            parsers\BaseParser::setLogUnknowns(true);
249 22
            $this->getArgumentWithCommand($argument, '__default__');
250
        }
251 25
    }
252
253 27
    private function aggregateOptions()
254
    {
255 27
        if (count($this->standAlones))
256 5
            $this->parsedOptions['stand_alones'] = $this->standAlones;
257 27
        if (count($this->unknownOptions))
258 4
            $this->parsedOptions['unknowns'] = $this->unknownOptions;
259
260
        // Hide the __default__ command from the outside world
261 27
        if ($this->parsedOptions['__command__'] == '__default__') {
262 20
            unset($this->parsedOptions['__command__']);
263
        }
264 27
    }
265
266 27
    private function showHelp()
267
    {
268 27
        if (isset($this->parsedOptions['help'])) {
269 3
            ClearIce::output($this->getHelpMessage(
270 3
                            isset($this->parsedOptions['__command__']) ?
271 3
                                    $this->parsedOptions['__command__'] : null
272
                    )
273
            );
274 3
            ClearIce::terminate();
275
        }
276 27
        if ($this->command == 'help') {
277 2
            ClearIce::output($this->getHelpMessage($this->standAlones[0]));
278 2
            ClearIce::terminate();
279
        }
280 27
    }
281
282 27
    private function showStrictErrors($executed)
283
    {
284 27
        if ($this->strict && count($this->unknownOptions) > 0) {
285 2
            foreach ($this->unknownOptions as $unknown) {
286 2
                ClearIce::error("$executed: invalid option -- {$unknown}\n");
287
            }
288
289 2
            if ($this->hasHelp) {
290 1
                ClearIce::error("Try `$executed --help` for more information\n");
291
            }
292 2
            ClearIce::terminate();
293
        }
294 27
    }
295
296 25
    private function getArgumentWithCommand($argument, $command)
297
    {
298 25
        $return = true;
299 25
        if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)(=)(?<value>.*)/i', $argument, $matches)) {
300 7
            $return = $this->longOptionParser->parse($matches['option'], $matches['value'], $command);
301 25
        } else if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)/i', $argument, $matches)) {
302 14
            $return = $this->longOptionParser->parse($matches['option'], true, $command);
303 15
        } else if (preg_match('/^(-)(?<option>[a-zA-z0-9](.*))/i', $argument, $matches)) {
304 13
            $this->shortOptionParser->parse($matches['option'], $command);
305 13
            $return = true;
306
        } else {
307 5
            $this->standAlones[] = $argument;
308
        }
309 25
        return $return;
310
    }
311
312 27
    private function getCommand()
313
    {
314 27
        $commands = array_keys($this->commands);
315 27
        if (count($commands) > 0 && count($this->arguments) > 0) {
316 9
            $command = array_search($this->arguments[0], $commands);
317 9
            if ($command === false) {
318 2
                $command = '__default__';
319
            } else {
320 7
                $command = $this->arguments[0];
321 9
                array_shift($this->arguments);
322
            }
323
        } else {
324 18
            $command = '__default__';
325
        }
326 27
        return $command;
327
    }
328
329 8
    private function stringCommandToArray($command)
330
    {
331 8
        if(class_exists($command)) {
332
            try{
333 1
                $className = $command;
334 1
                $method = new \ReflectionMethod($className, 'getCommandOptions');
335 1
                $command = $method->invoke(null);
336 1
                $command['class'] = $className;
337 1
                if(is_array($command['options'])) {
338 1
                    foreach($command['options'] as $option) {
339 1
                        $option['command'] = $command['command'];
340 1
                        ClearIce::addOptions($option);
341
                    }
342
                }
343 1
                return $command;
344
            } catch (\ReflectionException $e) {
345
                // Do nothing
346
            }
347
        } 
348
        return [
349 7
            'help' => '',
350 7
            'command' => $command
351
        ];                   
352
    }
353
354
    /**
355
     * Add commands for parsing. 
356
     * This method takes many arguments with each representing a unique command. 
357
     * 
358
     * @param String
359
     * @see ClearIce::addCommands()
360
     */
361 9
    public function addCommands()
362
    {
363 9
        foreach (func_get_args() as $command) {
364 9
            if (is_string($command)) {
365 8
                $command = $this->stringCommandToArray($command);
366
            } 
367 9
            $this->commands[$command['command']] = $command;
368
        }
369 9
    }
370
371
    /**
372
     * Add options to be recognized. 
373
     * Options could either be strings or structured arrays. Strings define 
374
     * simple options. Structured arrays describe options in deeper details.
375
     */
376 29
    public function addOptions()
377
    {
378 29
        $options = func_get_args();
379 29
        $this->options->add($options);
380 29
    }
381
    
382 1
    public function addGroups()
383
    {
384 1
        $groups = func_get_args();
385 1
        foreach($groups as $group) {
386 1
            $this->groups[$group['group']] = $group;
387
        }
388 1
    }
389
390
    /**
391
     * Sets whether the parser should be strict or not. A strict parser would 
392
     * terminate the application if it doesn't understand any options. A 
393
     * not-strict parser would just return the unknown options it encountered 
394
     * and expect the application to deal with it appropriately.     
395
     * 
396
     * @param boolean $strict A boolean value for the strictness state
397
     */
398 2
    public function setStrict($strict)
399
    {
400 2
        $this->strict = $strict;
401 2
    }
402
403
    /**
404
     * Adds the two automatic help options. A long one represented by --help and
405
     * a short one represented by -h.
406
     */
407 8
    public function addHelp()
408
    {
409 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...
410
411 8
        $this->addOptions(
412
                array(
413 8
                    'short' => 'h',
414
                    'long' => 'help',
415
                    'help' => 'Shows this help message'
416
                )
417
        );
418
419 8
        if (count($this->commands) > 0) {
420 4
            $this->addCommands(
421
                    array(
422 4
                        'command' => 'help',
423 4
                        'help' => "Displays specific help for any of the given commands.\nusage: {$argv[0]} help [command]"
424
                    )
425
            );
426
        }
427
428 8
        $this->hasHelp = true;
429 8
    }
430
431
    /**
432
     * Set the usage text which forms part of the help text.
433
     * @param string|array $usage
434
     */
435 7
    public function setUsage($usage)
436
    {
437 7
        $this->usage = $usage;
438 7
    }
439
440
    /**
441
     * Set the description text shown on top of the help text.
442
     * @param string $description
443
     */
444 7
    public function setDescription($description)
445
    {
446 7
        $this->description = $description;
447 7
    }
448
449
    /**
450
     * Set the footnote text shown at the bottom of the help text.
451
     * @param string $footnote
452
     */
453 7
    public function setFootnote($footnote)
454
    {
455 7
        $this->footnote = $footnote;
456 7
    }
457
458
    /**
459
     * Returns the help message as a string.
460
     * 
461
     * @global type $argv
462
     * @return string
463
     */
464 8
    public function getHelpMessage($command)
465
    {
466 8
        return (string) new HelpMessage([
467 8
            'options' => $this->options,
468 8
            'description' => $this->description,
469 8
            'usage' => $this->usage,
470 8
            'commands' => $this->commands,
471 8
            'footnote' => $this->footnote,
472 8
            'command' => $command,
473 8
            'groups' => $this->groups
474
        ]);
475
    }
476
}
477