Passed
Push — dev ( 1839bc...aa8518 )
by James Ekow Abaka
01:15
created

ArgumentParser   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 287
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 44
dl 0
loc 287
ccs 97
cts 97
cp 1
rs 8.3396
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
B parseArgumentArray() 0 12 5
A parseShortArgument() 0 16 3
A assignValue() 0 6 3
A getNextValueOrFail() 0 7 3
A fillInDefaults() 0 5 4
A addToOptionCache() 0 11 3
A validateOption() 0 7 4
A addCommand() 0 9 3
A addOption() 0 8 1
B maybeShowHelp() 0 10 5
A __construct() 0 3 1
A parse() 0 12 1
A enableHelp() 0 7 1
A getHelpMessage() 0 3 1
A parseLongArgument() 0 17 3
A parseCommand() 0 5 3

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.

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
namespace clearice\argparser;
4
5
6
/**
7
 * Class ArgumentParser
8
 *
9
 * @package clearice\argparser
10
 */
11
class ArgumentParser
12
{
13
    private $description;
14
15
    private $footer;
16
17
    private $name;
18
19
    private $commands = [];
20
21
    /**
22
     * @var array
23
     */
24
    private $optionsCache = [];
25
26
    /**
27
     * All the possible options for arguments.
28
     * @var array
29
     */
30
    private $options = [];
31
32
    /**
33
     * An instance of the help generator.
34
     * @var HelpMessageGenerator
35
     */
36
    private $helpGenerator;
37
38
    private $helpEnabled = false;
39
40 14
    public function __construct($helpWriter = null)
41
    {
42 14
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
43 14
    }
44
45
    /**
46
     * Add a value to the available possible options for later parsing.
47
     *
48
     * @param string $key
49
     * @param array $value
50
     * @throws OptionExistsException
51
     */
52 13
    private function addToOptionCache(string $key, array $value) : void
53
    {
54 13
        if (!isset($value[$key])) {
55 1
            return;
56
        }
57
        $cacheKey = "${value['command']}${value[$key]}";
58
        if (!isset($this->optionsCache[$cacheKey])) {
59 13
            $this->optionsCache[$cacheKey] = $value;
60
        } else {
61 2
            throw new OptionExistsException(
62 2
                "An argument option with $key {$value['command']} {$value[$key]} already exists."
63
            );
64
        }
65 13
    }
66
67
    /**
68
     * @param array $option
69
     * @throws InvalidArgumentDescriptionException
70
     * @throws UnknownCommandException
71
     */
72
    private function validateOption($option) : void
73
    {
74 14
        if (!isset($option['name'])) {
75 1
            throw new InvalidArgumentDescriptionException("Argument must have a name");
76
        }
77 13
        if (isset($option['command']) && !isset($this->commands[$option['command']])) {
78 1
            throw new UnknownCommandException("The command {$option['command']} is unknown");
79
        }
80 13
    }
81
82
    /**
83
     * Add an option to be parsed.
84
     * Arguments are presented as a structured array with the following possible keys.
85
     *
86
     *  name: The name of the option prefixed with a double dash --
87
     *  short_name: A shorter single character option prefixed with a single dash -
88
     *  type: Required for all options that take values. An option specified without a type is considered to be a
89
     *        boolean flag.
90
     *  repeats: A boolean value that states whether the option can be repeated or not. Repeatable options are returned
91
     *        as arrays.
92
     * default: A default value for the option.
93
     *  help: A help message for the option
94
     *
95
     * @param array $option
96
     * @throws OptionExistsException
97
     * @throws InvalidArgumentDescriptionException
98
     * @throws UnknownCommandException
99
     */
100
    public function addOption(array $option): void
101
    {
102 14
        $this->validateOption($option);
103 13
        $option['command'] = $option['command'] ?? '';
104 13
        $option['repeats'] = $option['repeats'] ?? false;
105 13
        $this->options[] = $option;
106 13
        $this->addToOptionCache('name', $option);
107 13
        $this->addToOptionCache('short_name', $option);
108 13
    }
109
110
    /**
111
     * @param $arguments
112
     * @param $argPointer
113
     * @return mixed
114
     * @throws InvalidValueException
115
     */
116
    private function getNextValueOrFail($arguments, &$argPointer, $name)
117
    {
118 5
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
119 3
            $argPointer++;
120 3
            return $arguments[$argPointer];
121
        } else {
122 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
123
        }
124
    }
125
126
    /**
127
     * Parse a long argument that is prefixed with a double dash "--"
128
     *
129
     * @param $arguments
130
     * @param $argPointer
131
     * @throws InvalidValueException
132
     */
133
    private function parseLongArgument($command, $arguments, &$argPointer, &$output)
134
    {
135 5
        $string = substr($arguments[$argPointer], 2);
136 5
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
137 5
        $name = $command . $matches['name'];
138 5
        $option = $this->optionsCache[$name];
139 5
        $value = true;
140
141 5
        if (isset($option['type'])) {
142 4
            if ($matches['equal'] === '=') {
143 1
                $value = $matches['value'];
144
            } else {
145 4
                $value = $this->getNextValueOrFail($arguments, $argPointer, $name);
146
            }
147
        }
148
149 4
        $this->assignValue($option, $output, $option['name'], $value);
150 4
    }
151
152
    /**
153
     * Parse a short argument that is prefixed with a single dash '-'
154
     *
155
     * @param $command
156
     * @param $arguments
157
     * @param $argPointer
158
     * @throws InvalidValueException
159
     */
160
    public function parseShortArgument($command, $arguments, &$argPointer, &$output)
161
    {
162 3
        $argument = $arguments[$argPointer];
163 3
        $key = $command . substr($argument, 1, 1);
164 3
        $option = $this->optionsCache[$key];
165 3
        $value = true;
166
167 3
        if (isset($option['type'])) {
168 3
            if (substr($argument, 2) != "") {
169 2
                $value = substr($argument, 2);
170
            } else {
171 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
172
            }
173
        }
174
175 2
        $this->assignValue($option, $output, $option['name'], $value);
176 2
    }
177
178
    private function assignValue($option, &$output, $key, $value)
179
    {
180 5
        if($option['repeats']) {
181 1
            $output[$key] = isset($output[$key]) ? array_merge($output[$key], [$value]) : [$value];
182
        } else {
183 4
            $output[$key] = $value;
184
        }
185 5
    }
186
187
    /**
188
     * @param $arguments
189
     * @param $argPointer
190
     * @param $output
191
     * @throws InvalidValueException
192
     */
193
    private function parseArgumentArray($arguments, &$argPointer, &$output)
194
    {
195 7
        $numArguments = count($arguments);
196 7
        $command = $output['__command'] ?? '';
197 7
        for (; $argPointer < $numArguments; $argPointer++) {
198 7
            $arg = $arguments[$argPointer];
199 7
            if (substr($arg, 0, 2) == "--") {
200 5
                $this->parseLongArgument($command, $arguments, $argPointer, $output);
201 3
            } else if ($arg[0] == '-') {
202 3
                $this->parseShortArgument($command, $arguments, $argPointer, $output);
203
            } else {
204 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
205
            }
206
        }
207 5
    }
208
209
    private function maybeShowHelp($output = [], $forced = false)
210
    {
211 6
        if ((isset($output['help']) && $output['help'] && $this->helpEnabled) || $forced) {
212 2
            return $this->helpGenerator->generate(
213 2
                $this->name, $output['command'] ?? null,
214 2
                ['options' => $this->options, 'commands' => $this->commands],
215 2
                $this->description, $this->footer
216
            );
217
        }
218 4
        return '';
219
    }
220
221
    public function parseCommand($arguments, &$argPointer, &$output)
222
    {
223 7
        if (count($arguments) > 1 && isset($this->commands[$arguments[$argPointer]])) {
224 1
            $output["__command"] = $arguments[$argPointer];
225 1
            $argPointer++;
226
        }
227 7
    }
228
229
    public function fillInDefaults(&$parsed)
230
    {
231 5
        foreach($this->options as $option) {
232 5
            if(!isset($parsed[$option['name']]) && isset($option['default'])) {
233 5
                $parsed[$option['name']] = $option['default'];
234
            }
235
        }
236 5
    }
237
238
    /**
239
     * Parses command line arguments and return a structured array of options and their associated values.
240
     *
241
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
242
     * @return array
243
     * @throws InvalidValueException
244
     */
245
    public function parse($arguments = null)
246
    {
247 7
        global $argv;
248 7
        $arguments = $arguments ?? $argv;
249 7
        $argPointer = 1;
250 7
        $parsed = [];
251 7
        $this->name = $this->name ?? $arguments[0];
252 7
        $this->parseCommand($arguments, $argPointer, $parsed);
253 7
        $this->parseArgumentArray($arguments, $argPointer, $parsed);
254 5
        $this->fillInDefaults($parsed);
255 5
        $this->maybeShowHelp($parsed);
256 5
        return $parsed;
257
    }
258
259
    /**
260
     * Enables help messages so they show automatically.
261
     *
262
     * @param string $name
263
     * @param string $description
264
     * @param string $footer
265
     *
266
     * @throws InvalidArgumentDescriptionException
267
     * @throws OptionExistsException
268
     * @throws UnknownCommandException
269
     */
270
    public function enableHelp(string $description = null, string $footer = null, string $name = null) : void
271
    {
272 2
        $this->name = $name;
273 2
        $this->description = $description;
274 2
        $this->footer = $footer;
275 2
        $this->helpEnabled = true;
276 2
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "display this help message"]);
277 2
    }
278
279
    public function getHelpMessage()
280
    {
281 1
        return $this->maybeShowHelp(null, true);
282
    }
283
284
    /**
285
     * @param $command
286
     * @throws CommandExistsException
287
     * @throws InvalidArgumentDescriptionException
288
     */
289
    public function addCommand($command)
290
    {
291 5
        if (!isset($command['name'])) {
292 1
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
293
        }
294 5
        if (isset($this->commands[$command['name']])) {
295 1
            throw new CommandExistsException("Command ${command['name']} already exists.");
296
        }
297 5
        $this->commands[$command['name']] = $command;
298 5
    }
299
}
300