Completed
Push — dev ( d72ebb...504cdd )
by James Ekow Abaka
01:42
created

ArgumentParser::assignValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 4
nc 3
nop 4
crap 3
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 12
    public function __construct($helpWriter = null)
39
    {
40 12
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
41 12
    }
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 11
    private function addToOptionCache($key, $value)
51
    {
52 11
        if (!isset($value[$key])) {
53 1
            return;
54
        }
55
        $cacheKey = "${value['command']}${value[$key]}";
56
        if (!isset($this->optionsCache[$cacheKey])) {
57 11
            $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 11
    }
64
65
    /**
66
     * @param $option
67
     * @throws InvalidArgumentDescriptionException
68
     * @throws UnknownCommandException
69
     */
70
    private function validateOption($option)
71
    {
72 12
        if (!isset($option['name'])) {
73 1
            throw new InvalidArgumentDescriptionException("Argument must have a name");
74
        }
75 11
        if (isset($option['command']) && !isset($this->commands[$option['command']])) {
76 1
            throw new UnknownCommandException("The command {$option['command']} is unknown");
77
        }
78 11
    }
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
     *  help: A help message for the option
91
     *
92
     * @param $option
93
     * @throws OptionExistsException
94
     * @throws InvalidArgumentDescriptionException
95
     * @throws UnknownCommandException
96
     */
97
    public function addOption($option)
98
    {
99 12
        $this->validateOption($option);
100 11
        $option['command'] = $option['command'] ?? '';
101 11
        $option['repeats'] = $option['repeats'] ?? false;
102 11
        $this->options[] = $option;
103 11
        $this->addToOptionCache('name', $option);
104 11
        $this->addToOptionCache('short_name', $option);
105 11
    }
106
107
    /**
108
     * @param $arguments
109
     * @param $argPointer
110
     * @return mixed
111
     * @throws InvalidValueException
112
     */
113
    private function getNextValueOrFail($arguments, &$argPointer, $name)
114
    {
115 5
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
116 3
            $argPointer++;
117 3
            return $arguments[$argPointer];
118
        } else {
119 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
120
        }
121
    }
122
123
    /**
124
     * Parse a long argument that is prefixed with a double dash "--"
125
     *
126
     * @param $arguments
127
     * @param $argPointer
128
     * @return array
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
     * @return array
157
     * @throws InvalidValueException
158
     */
159
    public function parseShortArgument($command, $arguments, &$argPointer, &$output)
160
    {
161 2
        $argument = $arguments[$argPointer];
162 2
        $key = $command . substr($argument, 1, 1);
163 2
        $option = $this->optionsCache[$key];
164 2
        $value = true;
165
166 2
        if (isset($option['type'])) {
167 2
            if (substr($argument, 2) != "") {
168 1
                $value = substr($argument, 2);
169
            } else {
170 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
171
            }
172
        }
173
174 1
        $this->assignValue($option, $output, $option['name'], $value);
175 1
    }
176
177
    private function assignValue($option, &$output, $key, $value)
178
    {
179 4
        if($option['repeats']) {
180 1
            $output[$key] = isset($output[$key]) ? array_merge($output[$key], [$value]) : [$value];
181
        } else {
182 3
            $output[$key] = $value;
183
        }
184 4
    }
185
186
    /**
187
     * @param $arguments
188
     * @param $argPointer
189
     * @param $output
190
     * @throws InvalidValueException
191
     */
192
    private function parseArgumentArray($arguments, &$argPointer, &$output)
193
    {
194 6
        $numArguments = count($arguments);
195 6
        $command = $output['__command'] ?? '';
196 6
        for (; $argPointer < $numArguments; $argPointer++) {
197 6
            $arg = $arguments[$argPointer];
198 6
            if (substr($arg, 0, 2) == "--") {
199 5
                $this->parseLongArgument($command, $arguments, $argPointer, $output);
200 2
            } else if ($arg[0] == '-') {
201 2
                $this->parseShortArgument($command, $arguments, $argPointer, $output);
202
            } else {
203 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
204
            }
205
        }
206 4
    }
207
208
    private function maybeShowHelp($name, $output)
209
    {
210 4
        if (isset($output['help']) && $output['help']) {
211 1
            $this->helpGenerator->generate(
212 1
                $name, $output['command'] ?? null,
213 1
                $this->optionsCache, $this->description, $this->footer
214
            );
215
        }
216 4
    }
217
218
    public function parseCommand($arguments, &$argPointer, &$output)
219
    {
220 6
        if (isset($this->commands[$arguments[$argPointer]])) {
221 1
            $output["__command"] = $arguments[$argPointer];
222 1
            $argPointer++;
223
        }
224 6
    }
225
226
    /**
227
     * Parses command line arguments and return a structured array of options and their associated values.
228
     *
229
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
230
     * @return array
231
     * @throws InvalidValueException
232
     */
233
    public function parse($arguments = null)
234
    {
235 6
        global $argv;
236 6
        $arguments = $arguments ?? $argv;
237 6
        $argPointer = 1;
238 6
        $parsed = [];
239 6
        $this->parseCommand($arguments, $argPointer, $parsed);
240 6
        $this->parseArgumentArray($arguments, $argPointer, $parsed);
241 4
        $this->maybeShowHelp($arguments[0], $parsed);
242 4
        return $parsed;
243
    }
244
245
    /**
246
     * Enables help messages so they show automatically.
247
     *
248
     * @param $name
249
     * @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...
250
     * @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...
251
     * @throws InvalidArgumentDescriptionException
252
     * @throws OptionExistsException
253
     * @throws UnknownCommandException
254
     */
255
    public function enableHelp($name, $description = null, $footer = null)
256
    {
257 1
        $this->name = $name;
258 1
        $this->description = $description;
259 1
        $this->footer = $footer;
260
261 1
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "get help on how to use this app $name"]);
262 1
    }
263
264
    /**
265
     * @param $command
266
     * @throws CommandExistsException
267
     * @throws InvalidArgumentDescriptionException
268
     */
269
    public function addCommand($command)
270
    {
271 4
        if (!isset($command['name'])) {
272 1
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
273
        }
274 4
        if (isset($this->commands[$command['name']])) {
275 1
            throw new CommandExistsException("Command ${command['name']} already exists.");
276
        }
277 4
        $this->commands[$command['name']] = $command;
278 4
    }
279
}
280