be more strict in what arguments to accept
[pharext/pharext] / src / pharext / Cli / 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 $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 = array_merge($this->orig, (array) $spec);
83 foreach ((array) $spec as $arg) {
84 if (isset($arg[0])) {
85 $this->spec["-".$arg[0]] = $arg;
86 }
87 $this->spec["--".$arg[1]] = $arg;
88 }
89 return $this;
90 }
91
92 /**
93 * Get original spec
94 * @return array
95 */
96 public function getSpec() {
97 return $this->orig;
98 }
99
100 /**
101 * Get compiled spec
102 * @return array
103 */
104 public function getCompiledSpec() {
105 return $this->spec;
106 }
107
108 /**
109 * Parse command line arguments according to the compiled spec.
110 *
111 * The Generator yields any parsing errors.
112 * Parsing will stop when all arguments are processed or the first option
113 * flagged CliArgs::HALT was encountered.
114 *
115 * @param int $argc
116 * @param array $argv
117 * @return Generator
118 */
119 public function parse($argc, array $argv) {
120 for ($i = 0; $i < $argc; ++$i) {
121 $o = $argv[$i];
122
123 if ($o{0} === '-' && strlen($o) > 2 && $o{1} !== '-') {
124 // multiple short opts, .e.g -vps
125 $argc += strlen($o) - 2;
126 array_splice($argv, $i, 1, array_map(function($s) {
127 return "-$s";
128 }, str_split(substr($o, 1))));
129 $o = $argv[$i];
130 } elseif ($o{0} === '-' && strlen($o) > 2 && $o{1} === '-' && 0 < ($eq = strpos($o, "="))) {
131 $argc++;
132 array_splice($argv, $i, 1, [
133 substr($o, 0, $eq++),
134 substr($o, $eq)
135 ]);
136 $o = $argv[$i];
137 }
138
139 if (!isset($this->spec[$o])) {
140 yield sprintf("Unknown option %s", $o);
141 } elseif (!$this->optAcceptsArg($o)) {
142 $this[$o] = true;
143 } elseif ($i+1 < $argc && !isset($this->spec[$argv[$i+1]])) {
144 $this[$o] = $argv[++$i];
145 } elseif ($this->optRequiresArg($o)) {
146 yield sprintf("Option --%s requires an argument", $this->optLongName($o));
147 } else {
148 // OPTARG
149 $this[$o] = $this->optDefaultArg($o);
150 }
151
152 if ($this->optHalts($o)) {
153 return;
154 }
155 }
156 }
157
158 /**
159 * Validate that all required options were given.
160 *
161 * The Generator yields any validation errors.
162 *
163 * @return Generator
164 */
165 public function validate() {
166 $required = array_filter($this->orig, function($spec) {
167 return $spec[3] & self::REQUIRED;
168 });
169 foreach ($required as $req) {
170 if (!strlen($this[$req[0]])) {
171 yield sprintf("Option --%s is required", $req[1]);
172 }
173 }
174 }
175
176 /**
177 * Retreive the default argument of an option
178 * @param string $o
179 * @return mixed
180 */
181 private function optDefaultArg($o) {
182 $o = $this->opt($o);
183 if (isset($this->spec[$o][4])) {
184 return $this->spec[$o][4];
185 }
186 return null;
187 }
188
189 /**
190 * Retrieve the help message of an option
191 * @param string $o
192 * @return string
193 */
194 private function optHelp($o) {
195 $o = $this->opt($o);
196 if (isset($this->spec[$o][2])) {
197 return $this->spec[$o][2];
198 }
199 return "";
200 }
201
202 /**
203 * Retrieve option's flags
204 * @param string $o
205 * @return int
206 */
207 private function optFlags($o) {
208 $o = $this->opt($o);
209 if (isset($this->spec[$o])) {
210 return $this->spec[$o][3];
211 }
212 return null;
213 }
214
215 /**
216 * Check whether an option is flagged for halting argument processing
217 * @param string $o
218 * @return boolean
219 */
220 private function optHalts($o) {
221 return $this->optFlags($o) & self::HALT;
222 }
223
224 /**
225 * Check whether an option needs an argument
226 * @param string $o
227 * @return boolean
228 */
229 private function optRequiresArg($o) {
230 return $this->optFlags($o) & self::REQARG;
231 }
232
233 /**
234 * Check wether an option accepts any argument
235 * @param string $o
236 * @return boolean
237 */
238 private function optAcceptsArg($o) {
239 return $this->optFlags($o) & 0xf00;
240 }
241
242 /**
243 * Check whether an option can be used more than once
244 * @param string $o
245 * @return boolean
246 */
247 private function optIsMulti($o) {
248 return $this->optFlags($o) & 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 $osn = $this->optShortName($o);
308 $oln = $this->optLongName($o);
309 if ($this->optIsMulti($o)) {
310 if (isset($osn)) {
311 $this->args["-$osn"][] = $v;
312 }
313 $this->args["--$oln"][] = $v;
314 } else {
315 if (isset($osn)) {
316 $this->args["-$osn"] = $v;
317 }
318 $this->args["--$oln"] = $v;
319 }
320 }
321 function __set($o, $v) {
322 $this->offsetSet($o, $v);
323 }
324 function offsetUnset($o) {
325 unset($this->args["-".$this->optShortName($o)]);
326 unset($this->args["--".$this->optLongName($o)]);
327 }
328 function __unset($o) {
329 $this->offsetUnset($o);
330 }
331 /**@-*/
332 }