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