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