Completed
Pull Request — master (#2)
by James Ekow Abaka
01:15
created

ArgumentParser::showStrictErrors()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5

Importance

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