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