c248a74dcc964c81c089d1b2204a8cacfe3dcfb0
[pharext/pharext] / Args.php
1 <?php
2
3 namespace pharext\Cli;
4
5 /**
6 * Command line arguments
7 */
8 class Args implements \ArrayAccess
9 {
10 /**
11 * Optional option
12 */
13 const OPTIONAL = 0x000;
14
15 /**
16 * Required Option
17 */
18 const REQUIRED = 0x001;
19
20 /**
21 * Only one value, even when used multiple times
22 */
23 const SINGLE = 0x000;
24
25 /**
26 * Aggregate an array, when used multiple times
27 */
28 const MULTI = 0x010;
29
30 /**
31 * Option takes no argument
32 */
33 const NOARG = 0x000;
34
35 /**
36 * Option requires an argument
37 */
38 const REQARG = 0x100;
39
40 /**
41 * Option takes an optional argument
42 */
43 const OPTARG = 0x200;
44
45 /**
46 * Option halts processing
47 */
48 const HALT = 0x10000000;
49
50 /**
51 * Original option spec
52 * @var array
53 */
54 private $orig = [];
55
56 /**
57 * Compiled spec
58 * @var array
59 */
60 private $spec = [];
61
62 /**
63 * Parsed args
64 * @var array
65 */
66 private $args = [];
67
68 /**
69 * Compile the original spec
70 * @param array|Traversable $spec
71 */
72 public function __construct($spec = null) {
73 if (is_array($spec) || $spec instanceof Traversable) {
74 $this->compile($spec);
75 }
76
77 }
78
79 /**
80 * Compile the original spec
81 * @param array|Traversable $spec
82 * @return pharext\Cli\Args self
83 */
84 public function compile($spec) {
85 foreach ($spec as $arg) {
86 if (isset($arg[0]) && is_numeric($arg[0])) {
87 $arg[3] &= ~0xf00;
88 $this->spec["--".$arg[0]] = $arg;
89 } elseif (isset($arg[0])) {
90 $this->spec["-".$arg[0]] = $arg;
91 $this->spec["--".$arg[1]] = $arg;
92 } else {
93 $this->spec["--".$arg[1]] = $arg;
94 }
95 $this->orig[] = $arg;
96 }
97 return $this;
98 }
99
100 /**
101 * Get original spec
102 * @return array
103 */
104 public function getSpec() {
105 return $this->orig;
106 }
107
108 /**
109 * Get compiled spec
110 * @return array
111 */
112 public function getCompiledSpec() {
113 return $this->spec;
114 }
115
116 /**
117 * Parse command line arguments according to the compiled spec.
118 *
119 * The Generator yields any parsing errors.
120 * Parsing will stop when all arguments are processed or the first option
121 * flagged Cli\Args::HALT was encountered.
122 *
123 * @param int $argc
124 * @param array $argv
125 * @return Generator
126 */
127 public function parse($argc, array $argv) {
128 for ($f = false, $p = 0, $i = 0; $i < $argc; ++$i) {
129 $o = $argv[$i];
130
131 if ($o{0} === "-" && strlen($o) > 2 && $o{1} !== "-") {
132 // multiple short opts, e.g. -vps
133 $argc += strlen($o) - 2;
134 array_splice($argv, $i, 1, array_map(function($s) {
135 return "-$s";
136 }, str_split(substr($o, 1))));
137 $o = $argv[$i];
138 } elseif ($o{0} === "-" && strlen($o) > 2 && $o{1} === "-" && 0 < ($eq = strpos($o, "="))) {
139 // long opt with argument, e.g. --foo=bar
140 $argc++;
141 array_splice($argv, $i, 1, [
142 substr($o, 0, $eq++),
143 substr($o, $eq)
144 ]);
145 $o = $argv[$i];
146 } elseif ($o === "--") {
147 // only positional args following
148 $f = true;
149 continue;
150 }
151
152 if ($f || !isset($this->spec[$o])) {
153 if ($o{0} !== "-" && isset($this->spec["--$p"])) {
154 $this[$p] = $o;
155 if (!$this->optIsMulti($p)) {
156 ++$p;
157 }
158 } else {
159 yield sprintf("Unknown option %s", $o);
160 }
161 } elseif (!$this->optAcceptsArg($o)) {
162 $this[$o] = true;
163 } elseif ($i+1 < $argc && !isset($this->spec[$argv[$i+1]])) {
164 $this[$o] = $argv[++$i];
165 } elseif ($this->optRequiresArg($o)) {
166 yield sprintf("Option --%s requires an argument", $this->optLongName($o));
167 } else {
168 // OPTARG
169 $this[$o] = $this->optDefaultArg($o);
170 }
171
172 if ($this->optHalts($o)) {
173 return;
174 }
175 }
176 }
177
178 /**
179 * Validate that all required options were given.
180 *
181 * The Generator yields any validation errors.
182 *
183 * @return Generator
184 */
185 public function validate() {
186 $required = array_filter($this->orig, function($spec) {
187 return $spec[3] & self::REQUIRED;
188 });
189 foreach ($required as $req) {
190 if ($req[3] & self::MULTI) {
191 if (is_array($this[$req[0]])) {
192 continue;
193 }
194 } elseif (strlen($this[$req[0]])) {
195 continue;
196 }
197 if (is_numeric($req[0])) {
198 yield sprintf("Argument <%s> is required", $req[1]);
199 } else {
200 yield sprintf("Option --%s is required", $req[1]);
201 }
202 }
203 }
204
205
206 public function toArray() {
207 $args = [];
208 foreach ($this->spec as $spec) {
209 $opt = $this->opt($spec[1]);
210 $args[$opt] = $this[$opt];
211 }
212 return $args;
213 }
214
215 /**
216 * Retreive the default argument of an option
217 * @param string $o
218 * @return mixed
219 */
220 private function optDefaultArg($o) {
221 $o = $this->opt($o);
222 if (isset($this->spec[$o][4])) {
223 return $this->spec[$o][4];
224 }
225 return null;
226 }
227
228 /**
229 * Retrieve the help message of an option
230 * @param string $o
231 * @return string
232 */
233 private function optHelp($o) {
234 $o = $this->opt($o);
235 if (isset($this->spec[$o][2])) {
236 return $this->spec[$o][2];
237 }
238 return "";
239 }
240
241 /**
242 * Retrieve option's flags
243 * @param string $o
244 * @return int
245 */
246 private function optFlags($o) {
247 $o = $this->opt($o);
248 if (isset($this->spec[$o])) {
249 return $this->spec[$o][3];
250 }
251 return null;
252 }
253
254 /**
255 * Check whether an option is flagged for halting argument processing
256 * @param string $o
257 * @return boolean
258 */
259 private function optHalts($o) {
260 return $this->optFlags($o) & self::HALT;
261 }
262
263 /**
264 * Check whether an option needs an argument
265 * @param string $o
266 * @return boolean
267 */
268 private function optRequiresArg($o) {
269 return $this->optFlags($o) & self::REQARG;
270 }
271
272 /**
273 * Check wether an option accepts any argument
274 * @param string $o
275 * @return boolean
276 */
277 private function optAcceptsArg($o) {
278 return $this->optFlags($o) & 0xf00;
279 }
280
281 /**
282 * Check whether an option can be used more than once
283 * @param string $o
284 * @return boolean
285 */
286 private function optIsMulti($o) {
287 return $this->optFlags($o) & self::MULTI;
288 }
289
290 /**
291 * Retreive the long name of an option
292 * @param string $o
293 * @return string
294 */
295 private function optLongName($o) {
296 $o = $this->opt($o);
297 return is_numeric($this->spec[$o][0]) ? $this->spec[$o][0] : $this->spec[$o][1];
298 }
299
300 /**
301 * Retreive the short name of an option
302 * @param string $o
303 * @return string
304 */
305 private function optShortName($o) {
306 $o = $this->opt($o);
307 return is_numeric($this->spec[$o][0]) ? null : $this->spec[$o][0];
308 }
309
310 /**
311 * Retreive the canonical name (--long-name) of an option
312 * @param string $o
313 * @return string
314 */
315 private function opt($o) {
316 if (is_numeric($o)) {
317 return "--$o";
318 }
319 if ($o{0} !== '-') {
320 if (strlen($o) > 1) {
321 $o = "-$o";
322 }
323 $o = "-$o";
324 }
325 return $o;
326 }
327
328 /**@+
329 * Implements ArrayAccess and virtual properties
330 */
331 function offsetExists($o) {
332 $o = $this->opt($o);
333 return isset($this->args[$o]);
334 }
335 function __isset($o) {
336 return $this->offsetExists($o);
337 }
338 function offsetGet($o) {
339 $o = $this->opt($o);
340 if (isset($this->args[$o])) {
341 return $this->args[$o];
342 }
343 return $this->optDefaultArg($o);
344 }
345 function __get($o) {
346 return $this->offsetGet($o);
347 }
348 function offsetSet($o, $v) {
349 $osn = $this->optShortName($o);
350 $oln = $this->optLongName($o);
351 if ($this->optIsMulti($o)) {
352 if (isset($osn)) {
353 $this->args["-$osn"][] = $v;
354 }
355 $this->args["--$oln"][] = $v;
356 } else {
357 if (isset($osn)) {
358 $this->args["-$osn"] = $v;
359 }
360 $this->args["--$oln"] = $v;
361 }
362 }
363 function __set($o, $v) {
364 $this->offsetSet($o, $v);
365 }
366 function offsetUnset($o) {
367 unset($this->args["-".$this->optShortName($o)]);
368 unset($this->args["--".$this->optLongName($o)]);
369 }
370 function __unset($o) {
371 $this->offsetUnset($o);
372 }
373 /**@-*/
374 }