Completed
Push — master ( 8bde3f...f8d0b5 )
by James Ekow Abaka
02:09
created

ArgumentParser::getHelpMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 5
Bugs 0 Features 2
Metric Value
c 5
b 0
f 2
dl 0
loc 12
ccs 10
cts 10
cp 1
rs 9.4286
cc 1
eloc 9
nc 1
nop 1
crap 1
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 24
    public function parse()
190
    {
191 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...
192 24
        $this->arguments = $argv;
193 24
        $executed = array_shift($this->arguments);
194 24
        $this->command = $this->getCommand();
195
196 24
        $this->parsedOptions['__command__'] = $this->command;
197 24
        $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...
198 24
        $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...
199
200 24
        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...
201 24
            $this->parseArgument($this->arguments[$this->argumentPointer]);
202 24
        }
203
204 24
        $this->showStrictErrors($executed);
205 24
        $this->aggregateOptions();
206 24
        $this->showHelp();
207
208 24
        return $this->executeCommand($this->command, $this->parsedOptions);
209
    }
210
211 6
    public function getLookAheadArgument()
212
    {
213 6
        return $this->arguments[$this->argumentPointer + 1];
214
    }
215
216 6
    public function moveToNextArgument()
217
    {
218 6
        $this->argumentPointer++;
219 6
    }
220
221 24
    private function executeCommand($command, $options)
222
    {
223 24
        if ($command === '__default__' || !isset($this->commands[$command]['class'])) {
224 23
            return $options;
225
        } else {
226 1
            $class = $this->commands[$command]['class'];
227 1
            $object = new $class();
228 1
            unset($options['__command__']);
229 1
            $object->run($options);
230 1
            return $options;
231
        }
232
    }
233
234 24
    private function parseArgument($argument)
235
    {
236 24
        $success = FALSE;
237 24
        if ($this->parsedOptions['__command__'] != '__default__') {
238 5
            parsers\BaseParser::setLogUnknowns(false);
239 5
            $success = $this->getArgumentWithCommand($argument, $this->parsedOptions['__command__']);
240 5
        }
241
242 24
        if ($success === false) {
243 22
            parsers\BaseParser::setLogUnknowns(true);
244 22
            $this->getArgumentWithCommand($argument, '__default__');
245 22
        }
246 24
    }
247
248 24
    private function aggregateOptions()
249
    {
250 24
        if (count($this->standAlones))
251 24
            $this->parsedOptions['stand_alones'] = $this->standAlones;
252 24
        if (count($this->unknownOptions))
253 24
            $this->parsedOptions['unknowns'] = $this->unknownOptions;
254
255
        // Hide the __default__ command from the outside world
256 24
        if ($this->parsedOptions['__command__'] == '__default__') {
257 19
            unset($this->parsedOptions['__command__']);
258 19
        }
259 24
    }
260
261 24
    private function showHelp()
262
    {
263 24
        if (isset($this->parsedOptions['help'])) {
264 3
            ClearIce::output($this->getHelpMessage(
265 3
                            isset($this->parsedOptions['__command__']) ?
266 3
                                    $this->parsedOptions['__command__'] : null
267 3
                    )
268 3
            );
269 3
            ClearIce::terminate();
270 3
        }
271 24
        if ($this->command == 'help') {
272 2
            ClearIce::output($this->getHelpMessage($this->standAlones[0]));
273 2
            ClearIce::terminate();
274 2
        }
275 24
    }
276
277 24
    private function showStrictErrors($executed)
278
    {
279 24
        if ($this->strict && count($this->unknownOptions) > 0) {
280 2
            foreach ($this->unknownOptions as $unknown) {
281 2
                ClearIce::error("$executed: invalid option -- {$unknown}\n");
282 2
            }
283
284 2
            if ($this->hasHelp) {
285 1
                ClearIce::error("Try `$executed --help` for more information\n");
286 1
            }
287 2
            ClearIce::terminate();
288 2
        }
289 24
    }
290
291 24
    private function getArgumentWithCommand($argument, $command)
292
    {
293 24
        $return = true;
294 24
        if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)(=)(?<value>.*)/i', $argument, $matches)) {
295 7
            $parser = $this->longOptionParser;
296 7
            $return = $parser->parse($matches['option'], $matches['value'], $command);
297 24
        } else if (preg_match('/^(--)(?<option>[a-zA-z][0-9a-zA-Z-_\.]*)/i', $argument, $matches)) {
298 13
            $parser = $this->longOptionParser;
299 13
            $return = $parser->parse($matches['option'], true, $command);
300 24
        } else if (preg_match('/^(-)(?<option>[a-zA-z0-9](.*))/i', $argument, $matches)) {
301 13
            $parser = $this->shortOptionParser;
302 13
            $parser->parse($matches['option'], $command);
303 13
            $return = true;
304 13
        } else {
305 5
            $this->standAlones[] = $argument;
306
        }
307 24
        return $return;
308
    }
309
310 24
    private function getCommand()
311
    {
312 24
        $commands = array_keys($this->commands);
313 24
        if (count($commands) > 0 && count($this->arguments) > 0) {
314 7
            $command = array_search($this->arguments[0], $commands);
315 7
            if ($command === false) {
316 2
                $command = '__default__';
317 2
            } else {
318 5
                $command = $this->arguments[0];
319 5
                array_shift($this->arguments);
320
            }
321 7
        } else {
322 17
            $command = '__default__';
323
        }
324 24
        return $command;
325
    }
326
327 6
    private function stringCommandToArray($command)
328
    {
329
        return [
330 6
            'help' => '',
331
            'command' => $command
332 6
        ];
333
    }
334
335
    /**
336
     * Add commands for parsing. 
337
     * This method can take as many commands as possible.
338
     * 
339
     * @param String
340
     * @see ClearIce::addCommands()
341
     */
342 7
    public function addCommands()
343
    {
344 7
        foreach (func_get_args() as $command) {
345 7
            if (is_string($command)) {
346 6
                $this->commands[$command] = $this->fillCommand($this->stringCommandToArray($command));
347 6
            } else {
348 7
                $this->commands[$command['command']] = $this->fillCommand($command);
349
            }
350 7
        }
351 7
    }
352
353
    /**
354
     * Add options to be recognized. 
355
     * Options could either be strings or
356
     * structured arrays. Strings only define simple options. Structured arrays
357
     * describe options in deeper details.
358
     */
359 27
    public function addOptions()
360
    {
361 27
        $options = func_get_args();
362 27
        $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...
363 27
    }
364
    
365 1
    public function addGroups()
366
    {
367 1
        $groups = func_get_args();
368 1
        foreach($groups as $group) {
369 1
            $this->groups[$group['group']] = $group;
370 1
        }
371 1
    }
372
373
    /**
374
     * Sets whether the parser should be strict or not. A strict parser would 
375
     * terminate the application if it doesn't understand any options. A 
376
     * not-strict parser would just return the unknown options it encountered 
377
     * and expect the application to deal with it appropriately.     
378
     * 
379
     * @param boolean $strict A boolean value for the strictness state
380
     */
381 2
    public function setStrict($strict)
382
    {
383 2
        $this->strict = $strict;
384 2
    }
385
386
    /**
387
     * Adds the two automatic help options. A long one represented by --help and
388
     * a short one represented by -h.
389
     */
390 8
    public function addHelp()
391
    {
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 8
                    'long' => 'help',
398
                    'help' => 'Shows this help message'
399 8
                )
400 8
        );
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 4
                    )
408 4
            );
409 4
        }
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
    {
420 7
        $this->usage = $usage;
421 7
    }
422
423
    /**
424
     * Set the description text shown on top of the help text.
425
     * @param string $description
426
     */
427 7
    public function setDescription($description)
428
    {
429 7
        $this->description = $description;
430 7
    }
431
432
    /**
433
     * Set the footnote text shown at the bottom of the help text.
434
     * @param string $footnote
435
     */
436 7
    public function setFootnote($footnote)
437
    {
438 7
        $this->footnote = $footnote;
439 7
    }
440
441
    /**
442
     * Returns the help message as a string.
443
     * 
444
     * @global type $argv
445
     * @return string
446
     */
447 8
    public function getHelpMessage($command)
448
    {
449 8
        return (string) new HelpMessage([
450 8
            'options' => $this->options,
451 8
            'description' => $this->description,
452 8
            'usage' => $this->usage,
453 8
            'commands' => $this->commands,
454 8
            'footnote' => $this->footnote,
455 8
            'command' => $command,
456 8
            'groups' => $this->groups
457 8
        ]);
458
    }
459
460 7
    private function fillCommand($command)
461
    {
462 7
        return $command;
463
    }
464
}
465