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
|
7 |
|
public function __construct($helpWriter = null) |
39
|
|
|
{ |
40
|
7 |
|
$this->helpGenerator = $helpWriter ?? new HelpMessageGenerator(); |
41
|
7 |
|
} |
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
|
6 |
|
private function addToOptionCache($key, $value) |
51
|
|
|
{ |
52
|
6 |
|
if(!isset($value[$key])) { |
53
|
1 |
|
return; |
54
|
|
|
} |
55
|
|
|
$cacheKey = isset($value['command']) ? "${value['command']}:${value[$key]}" : $value[$key]; |
56
|
|
|
if (!isset($this->optionsCache[$cacheKey])) { |
57
|
6 |
|
$this->optionsCache[$cacheKey] = $value; |
58
|
|
|
} else { |
59
|
2 |
|
throw new OptionExistsException( |
60
|
2 |
|
"An argument option with $key {$value[$key]} already exists" |
61
|
2 |
|
. (isset($value['command']) ? " for command ${value['command']}." : '.') |
62
|
|
|
); |
63
|
|
|
} |
64
|
6 |
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Add an option to be parsed. |
68
|
|
|
* Arguments are presented as a structured array with the following possible keys. |
69
|
|
|
* |
70
|
|
|
* name: The name of the option prefixed with a double dash -- |
71
|
|
|
* short_name: A shorter single character option prefixed with a single dash - |
72
|
|
|
* type: Required for all options that take values. An option specified without a type is considered to be a |
73
|
|
|
* boolean flag. |
74
|
|
|
* help: A help message for the option |
75
|
|
|
* |
76
|
|
|
* @param $option |
77
|
|
|
* @throws OptionExistsException |
78
|
|
|
* @throws InvalidArgumentDescriptionException |
79
|
|
|
* @throws UnknownCommandException |
80
|
|
|
*/ |
81
|
|
|
public function addOption($option) |
82
|
|
|
{ |
83
|
7 |
|
if (!isset($option['name'])) { |
84
|
1 |
|
throw new InvalidArgumentDescriptionException("Argument must have a name"); |
85
|
|
|
} |
86
|
6 |
|
if(isset($option['command']) && !isset($this->commands[$option['command']])) { |
87
|
|
|
throw new UnknownCommandException("The command {$option['command']} is unknown"); |
88
|
|
|
} |
89
|
6 |
|
$this->options[] = $option; |
90
|
6 |
|
$this->addToOptionCache('name', $option); |
91
|
6 |
|
$this->addToOptionCache('short_name', $option); |
92
|
6 |
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* @param $arguments |
96
|
|
|
* @param $argPointer |
97
|
|
|
* @return mixed |
98
|
|
|
* @throws InvalidValueException |
99
|
|
|
*/ |
100
|
|
|
private function getNextValueOrFail($arguments, &$argPointer, $name) |
101
|
|
|
{ |
102
|
3 |
|
if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') { |
103
|
1 |
|
$argPointer++; |
104
|
1 |
|
return $arguments[$argPointer]; |
105
|
|
|
} else { |
106
|
2 |
|
throw new InvalidValueException("A value must be passed along with argument $name."); |
107
|
|
|
} |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Parse a long argument that is prefixed with a double dash "--" |
112
|
|
|
* |
113
|
|
|
* @param $arguments |
114
|
|
|
* @param $argPointer |
115
|
|
|
* @return array |
116
|
|
|
* @throws InvalidValueException |
117
|
|
|
*/ |
118
|
|
|
private function parseLongArgument($arguments, &$argPointer) |
119
|
|
|
{ |
120
|
2 |
|
$string = substr($arguments[$argPointer], 2); |
121
|
2 |
|
preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches); |
122
|
2 |
|
$name = $matches['name']; |
123
|
2 |
|
$option = $this->optionsCache[$name]; |
124
|
2 |
|
$value = true; |
125
|
|
|
|
126
|
2 |
|
if (isset($option['type'])) { |
127
|
2 |
|
if ($matches['equal'] === '=') { |
128
|
1 |
|
$value = $matches['value']; |
129
|
|
|
} else { |
130
|
2 |
|
$value = $this->getNextValueOrFail($arguments, $argPointer, $name); |
131
|
|
|
} |
132
|
|
|
} |
133
|
|
|
|
134
|
1 |
|
return [$name, $this->castType($value, $option['type'] ?? null)]; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Parse a short argument that is prefixed with a single dash '-' |
139
|
|
|
* |
140
|
|
|
* @param $arguments |
141
|
|
|
* @param $argPointer |
142
|
|
|
* @return array |
143
|
|
|
* @throws InvalidValueException |
144
|
|
|
*/ |
145
|
|
|
public function parseShortArgument($arguments, &$argPointer) |
146
|
|
|
{ |
147
|
2 |
|
$argument = $arguments[$argPointer]; |
148
|
2 |
|
$key = substr($argument, 1, 1); |
149
|
2 |
|
$option = $this->optionsCache[$key]; |
150
|
2 |
|
$value = true; |
151
|
|
|
|
152
|
2 |
|
if (isset($option['type'])) { |
153
|
2 |
|
if (substr($argument, 2) != "") { |
154
|
1 |
|
$value = substr($argument, 2); |
155
|
|
|
} else { |
156
|
2 |
|
$value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']); |
157
|
|
|
} |
158
|
|
|
} |
159
|
|
|
|
160
|
1 |
|
return [$option['name'], $this->castType($value, $option['type'] ?? null)]; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
private function castType($value, $type) |
164
|
|
|
{ |
165
|
|
|
switch ($type) { |
166
|
1 |
|
case 'integer': |
|
|
|
|
167
|
|
|
return (int)$value; |
168
|
1 |
|
case 'float': |
|
|
|
|
169
|
|
|
return (float)$value; |
170
|
|
|
default: |
|
|
|
|
171
|
1 |
|
return $value; |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @param $arguments |
177
|
|
|
* @param $argPointer |
178
|
|
|
* @param $output |
179
|
|
|
* @throws InvalidValueException |
180
|
|
|
*/ |
181
|
|
|
private function parseArgumentArray($arguments, &$argPointer, &$output) |
182
|
|
|
{ |
183
|
4 |
|
$numArguments = count($arguments); |
184
|
4 |
|
for (; $argPointer < $numArguments; $argPointer++) { |
185
|
3 |
|
$arg = $arguments[$argPointer]; |
186
|
3 |
|
if (substr($arg, 0, 2) == "--") { |
187
|
2 |
|
$argument = $this->parseLongArgument($arguments, $argPointer); |
188
|
1 |
|
$output[$argument[0]] = $argument[1]; |
189
|
2 |
|
} else if ($arg[0] == '-') { |
190
|
2 |
|
$argument = $this->parseShortArgument($arguments, $argPointer); |
191
|
1 |
|
$output[$argument[0]] = $argument[1]; |
192
|
|
|
} else { |
193
|
1 |
|
$output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg]; |
194
|
|
|
} |
195
|
|
|
} |
196
|
2 |
|
} |
197
|
|
|
|
198
|
|
|
private function maybeShowHelp($name, $output) |
199
|
|
|
{ |
200
|
2 |
|
if (isset($output['help']) && $output['help']) { |
201
|
|
|
$this->helpGenerator->generate($name, $this->optionsCache, $this->description, $this->footer); |
|
|
|
|
202
|
|
|
} |
203
|
2 |
|
} |
204
|
|
|
|
205
|
|
|
public function parseCommand($arguments, &$argPointer, &$output) |
206
|
|
|
{ |
207
|
4 |
|
if(isset($this->commands[$arguments[$argPointer]])) { |
208
|
1 |
|
$output["__command"] = $arguments[$argPointer]; |
209
|
1 |
|
$argPointer++; |
210
|
|
|
} |
211
|
4 |
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Parses command line arguments and return a structured array of options and their associated values. |
215
|
|
|
* |
216
|
|
|
* @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI. |
217
|
|
|
* @return array |
218
|
|
|
* @throws InvalidValueException |
219
|
|
|
*/ |
220
|
|
|
public function parse($arguments = null) |
221
|
|
|
{ |
222
|
4 |
|
global $argv; |
223
|
4 |
|
$arguments = $arguments ?? $argv; |
224
|
4 |
|
$argPointer = 1; |
225
|
4 |
|
$parsed = []; |
226
|
4 |
|
$this->parseCommand($arguments, $argPointer, $parsed); |
227
|
4 |
|
$this->parseArgumentArray($arguments, $argPointer, $parsed); |
228
|
2 |
|
$this->maybeShowHelp($arguments[0], $parsed); |
229
|
2 |
|
return $parsed; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* @param $name |
234
|
|
|
* @param null $description |
|
|
|
|
235
|
|
|
* @param null $footer |
|
|
|
|
236
|
|
|
* @throws InvalidArgumentDescriptionException |
237
|
|
|
* @throws OptionExistsException |
238
|
|
|
* @throws UnknownCommandException |
239
|
|
|
*/ |
240
|
|
|
public function enableHelp($name, $description = null, $footer = null) |
241
|
|
|
{ |
242
|
|
|
$this->name = $name; |
243
|
|
|
$this->description = $description; |
244
|
|
|
$this->footer = $footer; |
245
|
|
|
|
246
|
|
|
$this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "get help on how to use this app $name"]); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* @param $command |
251
|
|
|
* @throws CommandExistsException |
252
|
|
|
* @throws InvalidArgumentDescriptionException |
253
|
|
|
*/ |
254
|
|
|
public function addCommand($command) |
255
|
|
|
{ |
256
|
1 |
|
if(!isset($command['name'])) { |
257
|
|
|
throw new InvalidArgumentDescriptionException("Command description must contain a name"); |
258
|
|
|
} |
259
|
1 |
|
if(isset($this->commands[$command['name']])) { |
260
|
|
|
throw new CommandExistsException("Command ${command['name']} already exists."); |
261
|
|
|
} |
262
|
1 |
|
$this->commands[$command['name']] = $command; |
263
|
1 |
|
} |
264
|
|
|
} |
265
|
|
|
|
As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next
break
.There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.