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