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