add CliArgsTest
[pharext/pharext] / src / pharext / CliArgs.php
1 <?php
2
3 namespace pharext;
4
5 /**
6 * Command line arguments
7 */
8 class CliArgs 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 $spec
71 */
72 public function __construct(array $spec = null) {
73 $this->compile($spec);
74 }
75
76 /**
77 * Compile the original spec
78 * @param array $spec
79 * @return pharext\CliArgs self
80 */
81 public function compile(array $spec = null) {
82 $this->orig = $spec;
83 $this->spec = [];
84 foreach ((array) $spec as $arg) {
85 $this->spec["-".$arg[0]] = $arg;
86 $this->spec["--".$arg[1]] = $arg;
87 }
88 return $this;
89 }
90
91 /**
92 * Get compiled spec
93 * @return array
94 */
95 public function getSpec() {
96 return $this->spec;
97 }
98
99 /**
100 * Parse command line arguments according to the compiled spec.
101 *
102 * The Generator yields any parsing errors.
103 * Parsing will stop when all arguments are processed or the first option
104 * flagged CliArgs::HALT was encountered.
105 *
106 * @param int $argc
107 * @param array $argv
108 * @return Generator
109 */
110 public function parse($argc, array $argv) {
111 for ($i = 0; $i < $argc; ++$i) {
112 $o = $argv[$i];
113
114 if ($o{0} === '-' && strlen($o) > 2 && $o{1} !== '-') {
115 // multiple short opts, .e.g -vps
116 $argc += strlen($o) - 2;
117 array_splice($argv, $i, 1, array_map(function($s) {
118 return "-$s";
119 }, str_split(substr($o, 1))));
120 $o = $argv[$i];
121 }
122
123 if (!isset($this->spec[$o])) {
124 yield sprintf("Unknown option %s", $o);
125 } elseif (!$this->optAcceptsArg($o)) {
126 $this[$o] = true;
127 } elseif ($i+1 < $argc && !isset($this->spec[$argv[$i+1]])) {
128 $this[$o] = $argv[++$i];
129 } elseif ($this->optRequiresArg($o)) {
130 yield sprintf("Option --%s requires an argument", $this->optLongName($o));
131 } else {
132 // OPTARG
133 $this[$o] = $this->optDefaultArg($o);
134 }
135
136 if ($this->optHalts($o)) {
137 return;
138 }
139 }
140 }
141
142 /**
143 * Validate that all required options were given.
144 *
145 * The Generator yields any validation errors.
146 *
147 * @return Generator
148 */
149 public function validate() {
150 $required = array_filter($this->orig, function($spec) {
151 return $spec[3] & self::REQUIRED;
152 });
153 foreach ($required as $req) {
154 if (!isset($this[$req[0]])) {
155 yield sprintf("Option --%s is required", $req[1]);
156 }
157 }
158 }
159
160 /**
161 * Output command line help message
162 * @param string $prog
163 */
164 public function help($prog) {
165 printf("\nUsage:\n\n $ %s", $prog);
166 $flags = [];
167 $required = [];
168 $optional = [];
169 foreach ($this->orig as $spec) {
170 if ($spec[3] & self::REQARG) {
171 if ($spec[3] & self::REQUIRED) {
172 $required[] = $spec;
173 } else {
174 $optional[] = $spec;
175 }
176 } else {
177 $flags[] = $spec;
178 }
179 }
180
181 if ($flags) {
182 printf(" [-%s]", implode("|-", array_column($flags, 0)));
183 }
184 foreach ($required as $req) {
185 printf(" -%s <arg>", $req[0]);
186 }
187 if ($optional) {
188 printf(" [-%s <arg>]", implode("|-", array_column($optional, 0)));
189 }
190 printf("\n\n");
191 foreach ($this->orig as $spec) {
192 printf(" -%s|--%s %s", $spec[0], $spec[1], ($spec[3] & self::REQARG) ? "<arg> " : (($spec[3] & self::OPTARG) ? "[<arg>]" : " "));
193 printf("%s%s %s", str_repeat(" ", 16-strlen($spec[1])), $spec[2], ($spec[3] & self::REQUIRED) ? "(REQUIRED)" : "");
194 if (isset($spec[4])) {
195 printf(" [%s]", $spec[4]);
196 }
197 printf("\n");
198 }
199 printf("\n");
200 }
201
202 /**
203 * Retreive the default argument of an option
204 * @param string $o
205 * @return mixed
206 */
207 private function optDefaultArg($o) {
208 $o = $this->opt($o);
209 if (isset($this->spec[$o][4])) {
210 return $this->spec[$o][4];
211 }
212 return null;
213 }
214
215 /**
216 * Retrieve the help message of an option
217 * @param string $o
218 * @return string
219 */
220 private function optHelp($o) {
221 $o = $this->opt($o);
222 if (isset($this->spec[$o][2])) {
223 return $this->spec[$o][2];
224 }
225 return "";
226 }
227
228 /**
229 * Retrieve option's flags
230 * @param string $o
231 * @return int
232 */
233 private function optFlags($o) {
234 $o = $this->opt($o);
235 if (isset($this->spec[$o])) {
236 return $this->spec[$o][3];
237 }
238 return null;
239 }
240
241 /**
242 * Check whether an option is flagged for halting argument processing
243 * @param string $o
244 * @return boolean
245 */
246 private function optHalts($o) {
247 return $this->optFlags($o) & self::HALT;
248 }
249
250 /**
251 * Check whether an option needs an argument
252 * @param string $o
253 * @return boolean
254 */
255 private function optRequiresArg($o) {
256 return $this->optFlags($o) & self::REQARG;
257 }
258
259 /**
260 * Check wether an option accepts any argument
261 * @param string $o
262 * @return boolean
263 */
264 private function optAcceptsArg($o) {
265 return $this->optFlags($o) & 0xf00;
266 }
267
268 /**
269 * Check whether an option can be used more than once
270 * @param string $o
271 * @return boolean
272 */
273 private function optIsMulti($o) {
274 return $this->optFlags($o) & self::MULTI;
275 }
276
277 /**
278 * Retreive the long name of an option
279 * @param string $o
280 * @return string
281 */
282 private function optLongName($o) {
283 $o = $this->opt($o);
284 return $this->spec[$o][1];
285 }
286
287 /**
288 * Retreive the short name of an option
289 * @param string $o
290 * @return string
291 */
292 private function optShortName($o) {
293 $o = $this->opt($o);
294 return $this->spec[$o][0];
295 }
296
297 /**
298 * Retreive the canonical name (--long-name) of an option
299 * @param string $o
300 * @return string
301 */
302 private function opt($o) {
303 if ($o{0} !== '-') {
304 if (strlen($o) > 1) {
305 $o = "-$o";
306 }
307 $o = "-$o";
308 }
309 return $o;
310 }
311
312 /**@+
313 * Implements ArrayAccess and virtual properties
314 */
315 function offsetExists($o) {
316 $o = $this->opt($o);
317 return isset($this->args[$o]);
318 }
319 function __isset($o) {
320 return $this->offsetExists($o);
321 }
322 function offsetGet($o) {
323 $o = $this->opt($o);
324 if (isset($this->args[$o])) {
325 return $this->args[$o];
326 }
327 return $this->optDefaultArg($o);
328 }
329 function __get($o) {
330 return $this->offsetGet($o);
331 }
332 function offsetSet($o, $v) {
333 if ($this->optIsMulti($o)) {
334 $this->args["-".$this->optShortName($o)][] = $v;
335 $this->args["--".$this->optLongName($o)][] = $v;
336 } else {
337 $this->args["-".$this->optShortName($o)] = $v;
338 $this->args["--".$this->optLongName($o)] = $v;
339 }
340 }
341 function __set($o, $v) {
342 $this->offsetSet($o, $v);
343 }
344 function offsetUnset($o) {
345 unset($this->args["-".$this->optShortName($o)]);
346 unset($this->args["--".$this->optLongName($o)]);
347 }
348 function __unset($o) {
349 $this->offsetUnset($o);
350 }
351 /**@-*/
352 }