Completed
Push — dev ( f9d267...f7e851 )
by James Ekow Abaka
01:33
created

ArgumentParser::maybeShowHelp()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

278
        return $this->/** @scrutinizer ignore-call */ maybeShowHelp();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
279
    }
280
281
    /**
282
     * @param $command
283
     * @throws CommandExistsException
284
     * @throws InvalidArgumentDescriptionException
285
     */
286
    public function addCommand($command)
287
    {
288 4
        if (!isset($command['name'])) {
289 1
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
290
        }
291 4
        if (isset($this->commands[$command['name']])) {
292 1
            throw new CommandExistsException("Command ${command['name']} already exists.");
293
        }
294 4
        $this->commands[$command['name']] = $command;
295 4
    }
296
}
297