Completed
Push — dev ( 504cdd...f9d267 )
by James Ekow Abaka
03:05 queued 48s
created

ArgumentParser::fillInDefaults()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 3
nc 3
nop 1
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 $key
47
     * @param $value
48
     * @throws OptionExistsException
49
     */
50 12
    private function addToOptionCache($key, $value)
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 $option
67
     * @throws InvalidArgumentDescriptionException
68
     * @throws UnknownCommandException
69
     */
70
    private function validateOption($option)
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 $option
94
     * @throws OptionExistsException
95
     * @throws InvalidArgumentDescriptionException
96
     * @throws UnknownCommandException
97
     */
98
    public function addOption($option)
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
     * @return array
130
     * @throws InvalidValueException
131
     */
132
    private function parseLongArgument($command, $arguments, &$argPointer, &$output)
133
    {
134 5
        $string = substr($arguments[$argPointer], 2);
135 5
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
136 5
        $name = $command . $matches['name'];
137 5
        $option = $this->optionsCache[$name];
138 5
        $value = true;
139
140 5
        if (isset($option['type'])) {
141 4
            if ($matches['equal'] === '=') {
142 1
                $value = $matches['value'];
143
            } else {
144 4
                $value = $this->getNextValueOrFail($arguments, $argPointer, $name);
145
            }
146
        }
147
148 4
        $this->assignValue($option, $output, $option['name'], $value);
149 4
    }
150
151
    /**
152
     * Parse a short argument that is prefixed with a single dash '-'
153
     *
154
     * @param $command
155
     * @param $arguments
156
     * @param $argPointer
157
     * @return array
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($name, $output)
210
    {
211 5
        if (isset($output['help']) && $output['help']) {
212 1
            $this->helpGenerator->generate(
213 1
                $name, $output['command'] ?? null,
214 1
                $this->optionsCache, $this->description, $this->footer
215
            );
216
        }
217 5
    }
218
219
    public function parseCommand($arguments, &$argPointer, &$output)
220
    {
221 7
        if (count($arguments) > 1 && isset($this->commands[$arguments[$argPointer]])) {
222 1
            $output["__command"] = $arguments[$argPointer];
223 1
            $argPointer++;
224
        }
225 7
    }
226
227
    public function fillInDefaults(&$parsed)
228
    {
229 5
        foreach($this->options as $option) {
230 5
            if(!isset($parsed[$option['name']]) && isset($option['default'])) {
231 5
                $parsed[$option['name']] = $option['default'];
232
            }
233
        }
234 5
    }
235
236
    /**
237
     * Parses command line arguments and return a structured array of options and their associated values.
238
     *
239
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
240
     * @return array
241
     * @throws InvalidValueException
242
     */
243
    public function parse($arguments = null)
244
    {
245 7
        global $argv;
246 7
        $arguments = $arguments ?? $argv;
247 7
        $argPointer = 1;
248 7
        $parsed = [];
249 7
        $this->parseCommand($arguments, $argPointer, $parsed);
250 7
        $this->parseArgumentArray($arguments, $argPointer, $parsed);
251 5
        $this->fillInDefaults($parsed);
252 5
        $this->maybeShowHelp($arguments[0], $parsed);
253 5
        return $parsed;
254
    }
255
256
    /**
257
     * Enables help messages so they show automatically.
258
     *
259
     * @param $name
260
     * @param null $description
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $description is correct as it would always require null to be passed?
Loading history...
261
     * @param null $footer
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $footer is correct as it would always require null to be passed?
Loading history...
262
     * @throws InvalidArgumentDescriptionException
263
     * @throws OptionExistsException
264
     * @throws UnknownCommandException
265
     */
266
    public function enableHelp($name, $description = null, $footer = null)
267
    {
268 1
        $this->name = $name;
269 1
        $this->description = $description;
270 1
        $this->footer = $footer;
271
272 1
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "get help on how to use this app $name"]);
273 1
    }
274
275
    /**
276
     * @param $command
277
     * @throws CommandExistsException
278
     * @throws InvalidArgumentDescriptionException
279
     */
280
    public function addCommand($command)
281
    {
282 4
        if (!isset($command['name'])) {
283 1
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
284
        }
285 4
        if (isset($this->commands[$command['name']])) {
286 1
            throw new CommandExistsException("Command ${command['name']} already exists.");
287
        }
288 4
        $this->commands[$command['name']] = $command;
289 4
    }
290
}
291