1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Garden\Cli; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* A general purpose command line parser. |
7
|
|
|
* |
8
|
|
|
* @author Todd Burry <[email protected]> |
9
|
|
|
* @license MIT |
10
|
|
|
* @copyright 2010-2014 Vanilla Forums Inc. |
11
|
|
|
*/ |
12
|
|
|
class Cli { |
13
|
|
|
/// Constants /// |
14
|
|
|
|
15
|
|
|
const META = '__meta'; |
16
|
|
|
const ARGS = '__args'; |
17
|
|
|
|
18
|
|
|
/// Properties /// |
19
|
|
|
/** |
20
|
|
|
* @var array All of the schemas, indexed by command pattern. |
21
|
|
|
*/ |
22
|
|
|
protected $commandSchemas; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var array A pointer to the current schema. |
26
|
|
|
*/ |
27
|
|
|
protected $currentSchema; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var bool Whether or not to format output with console codes. |
31
|
|
|
*/ |
32
|
|
|
protected $formatOutput; |
33
|
|
|
|
34
|
|
|
protected static $types = [ |
35
|
|
|
// '=' => 'base64', |
|
|
|
|
36
|
|
|
'i' => 'integer', |
37
|
|
|
's' => 'string', |
38
|
|
|
// 'f' => 'float', |
|
|
|
|
39
|
|
|
'b' => 'boolean', |
40
|
|
|
// 'ts' => 'timestamp', |
|
|
|
|
41
|
|
|
// 'dt' => 'datetime' |
42
|
|
|
]; |
43
|
|
|
|
44
|
|
|
|
45
|
|
|
/// Methods /// |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Creates a {@link Cli} instance representing a command line parser for a given schema. |
49
|
|
|
*/ |
50
|
32 |
|
public function __construct() { |
51
|
32 |
|
$this->commandSchemas = ['*' => [Cli::META => []]]; |
52
|
|
|
|
53
|
|
|
// Select the current schema. |
54
|
32 |
|
$this->currentSchema =& $this->commandSchemas['*']; |
55
|
32 |
|
|
56
|
|
|
$this->formatOutput = static::guessFormatOutput(); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Backwards compatibility for the **format** property. |
61
|
|
|
* |
62
|
|
|
* @param string $name Must be **format**. |
63
|
|
|
* @return bool|null Returns {@link getFormatOutput()} or null if {@link $name} isn't **format**. |
64
|
2 |
|
*/ |
65
|
2 |
|
public function __get($name) { |
66
|
2 |
|
if ($name === 'format') { |
67
|
2 |
|
trigger_error("Cli->format is deprecated. Use Cli->getFormatOutput() instead.", E_USER_DEPRECATED); |
68
|
|
|
return $this->getFormatOutput(); |
69
|
2 |
|
} |
70
|
2 |
|
return null; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Backwards compatibility for the **format** property. |
75
|
|
|
* |
76
|
|
|
* @param string $name Must be **format**. |
77
|
|
|
* @param bool $value One of **true** or **false**. |
78
|
|
|
*/ |
79
|
2 |
|
public function __set($name, $value) { |
80
|
2 |
|
if ($name === 'format') { |
81
|
|
|
trigger_error("Cli->format is deprecated. Use Cli->setFormatOutput() instead.", E_USER_DEPRECATED); |
82
|
|
|
$this->setFormatOutput($value); |
83
|
|
|
} |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Get whether or not output should be formatted. |
88
|
|
|
* |
89
|
|
|
* @return boolean Returns **true** if output should be formatted or **false** otherwise. |
90
|
|
|
*/ |
91
|
6 |
|
public function getFormatOutput() { |
92
|
6 |
|
return $this->formatOutput; |
93
|
6 |
|
} |
94
|
|
|
|
95
|
6 |
|
/** |
96
|
|
|
* Set whether or not output should be formatted. |
97
|
6 |
|
* |
98
|
6 |
|
* @param boolean $formatOutput Whether or not to format output. |
99
|
6 |
|
* @return Cli Returns `$this` for fluent calls. |
100
|
|
|
*/ |
101
|
6 |
|
public function setFormatOutput($formatOutput) { |
102
|
|
|
$this->formatOutput = $formatOutput; |
103
|
|
|
return $this; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Add an argument to an {@link Args} object, checking for a correct name. |
108
|
|
|
* |
109
|
|
|
* @param array $schema The schema for the args. |
110
|
|
|
* @param Args $args The args object to add the argument to. |
111
|
|
|
* @param $arg The value of the argument. |
112
|
6 |
|
*/ |
113
|
6 |
|
private function addArg(array $schema, Args $args, $arg) { |
114
|
6 |
|
$argsCount = count($args->getArgs()); |
115
|
|
|
$schemaArgs = isset($schema[self::META][self::ARGS]) ? array_keys($schema[self::META][self::ARGS]) : []; |
116
|
6 |
|
$name = isset($schemaArgs[$argsCount]) ? $schemaArgs[$argsCount] : $argsCount; |
117
|
6 |
|
|
118
|
6 |
|
$args->addArg($arg, $name); |
119
|
|
|
} |
120
|
|
|
|
121
|
6 |
|
/** |
122
|
2 |
|
* Construct and return a new {@link Cli} object. |
123
|
|
|
* |
124
|
|
|
* This method is mainly here so that an entire cli schema can be created and defined with fluent method calls. |
125
|
|
|
* |
126
|
|
|
* @return Cli Returns a new Cli object. |
127
|
|
|
*/ |
128
|
|
|
public static function create() { |
129
|
|
|
return new Cli(); |
130
|
2 |
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Breaks a cell into several lines according to a given width. |
134
|
|
|
* |
135
|
2 |
|
* @param string $text The text of the cell. |
136
|
2 |
|
* @param int $width The width of the cell. |
137
|
|
|
* @param bool $addSpaces Whether or not to right-pad the cell with spaces. |
138
|
2 |
|
* @return array Returns an array of strings representing the lines in the cell. |
139
|
6 |
|
*/ |
140
|
|
|
public static function breakLines($text, $width, $addSpaces = true) { |
141
|
6 |
|
$rawLines = explode("\n", $text); |
142
|
|
|
$lines = []; |
143
|
|
|
|
144
|
6 |
|
foreach ($rawLines as $line) { |
145
|
6 |
|
// Check to see if the line needs to be broken. |
146
|
6 |
|
$sublines = static::breakString($line, $width, $addSpaces); |
147
|
6 |
|
$lines = array_merge($lines, $sublines); |
148
|
|
|
} |
149
|
|
|
|
150
|
6 |
|
return $lines; |
151
|
6 |
|
} |
152
|
|
|
|
153
|
6 |
|
/** |
154
|
|
|
* Breaks a line of text according to a given width. |
155
|
|
|
* |
156
|
|
|
* @param string $line The text of the line. |
157
|
|
|
* @param int $width The width of the cell. |
158
|
|
|
* @param bool $addSpaces Whether or not to right pad the lines with spaces. |
159
|
|
|
* @return array Returns an array of lines, broken on word boundaries. |
160
|
|
|
*/ |
161
|
|
|
protected static function breakString($line, $width, $addSpaces = true) { |
162
|
8 |
|
$words = explode(' ', $line); |
163
|
8 |
|
$result = []; |
164
|
|
|
|
165
|
|
|
$line = ''; |
166
|
|
|
foreach ($words as $word) { |
167
|
|
|
$candidate = trim($line.' '.$word); |
168
|
|
|
|
169
|
|
|
// Check for a new line. |
170
|
|
|
if (strlen($candidate) > $width) { |
171
|
|
|
if ($line === '') { |
172
|
32 |
|
// The word is longer than a line. |
173
|
32 |
|
if ($addSpaces) { |
174
|
|
|
$result[] = substr($candidate, 0, $width); |
175
|
|
|
} else { |
176
|
32 |
|
$result[] = $candidate; |
177
|
32 |
|
} |
178
|
4 |
|
} else { |
179
|
|
|
if ($addSpaces) { |
180
|
32 |
|
$line .= str_repeat(' ', $width - strlen($line)); |
181
|
28 |
|
} |
182
|
|
|
|
183
|
|
|
// Start a new line. |
184
|
|
|
$result[] = $line; |
185
|
|
|
$line = $word; |
186
|
|
|
} |
187
|
|
|
} else { |
188
|
|
|
$line = $candidate; |
189
|
|
|
} |
190
|
|
|
} |
191
|
6 |
|
|
192
|
6 |
|
// Add the remaining line. |
193
|
2 |
|
if ($line) { |
194
|
2 |
|
if ($addSpaces) { |
195
|
|
|
$line .= str_repeat(' ', $width - strlen($line)); |
196
|
4 |
|
} |
197
|
4 |
|
|
198
|
4 |
|
// Start a new line. |
199
|
|
|
$result[] = $line; |
200
|
1 |
|
} |
201
|
|
|
|
202
|
|
|
return $result; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Sets the description for the current command. |
207
|
|
|
* |
208
|
|
|
* @param string $str The description for the current schema or null to get the current description. |
209
|
|
|
* @return Cli Returns this class for fluent calls. |
210
|
|
|
*/ |
211
|
6 |
|
public function description($str = null) { |
212
|
6 |
|
return $this->meta('description', $str); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Determines whether or not the schema has a command. |
217
|
|
|
* |
218
|
|
|
* @param string $name Check for the specific command name. |
219
|
|
|
* @return bool Returns true if the schema has a command. |
220
|
|
|
*/ |
221
|
|
|
public function hasCommand($name = '') { |
222
|
|
|
if ($name) { |
223
|
|
|
return array_key_exists($name, $this->commandSchemas); |
224
|
6 |
|
} else { |
225
|
6 |
|
foreach ($this->commandSchemas as $pattern => $opts) { |
226
|
|
|
if (strpos($pattern, '*') === false) { |
227
|
6 |
|
return true; |
228
|
|
|
} |
229
|
2 |
|
} |
230
|
2 |
|
return false; |
231
|
2 |
|
} |
232
|
2 |
|
} |
233
|
2 |
|
|
234
|
4 |
|
/** |
235
|
4 |
|
* Determines whether a command has options. |
236
|
2 |
|
* |
237
|
2 |
|
* @param string $command The name of the command or an empty string for any command. |
238
|
4 |
|
* @return bool Returns true if the command has options. False otherwise. |
239
|
4 |
|
*/ |
240
|
2 |
|
public function hasOptions($command = '') { |
241
|
|
|
if ($command) { |
242
|
|
|
$def = $this->getSchema($command); |
243
|
|
|
return $this->hasOptionsDef($def); |
244
|
4 |
|
} else { |
245
|
2 |
|
foreach ($this->commandSchemas as $pattern => $def) { |
246
|
|
|
if ($this->hasOptionsDef($def)) { |
247
|
|
|
return true; |
248
|
2 |
|
} |
249
|
2 |
|
} |
250
|
1 |
|
} |
251
|
|
|
return false; |
252
|
1 |
|
} |
253
|
1 |
|
|
254
|
|
|
/** |
255
|
|
|
* Determines whether or not a command definition has options. |
256
|
|
|
* |
257
|
|
|
* @param array $commandDef The command definition as returned from {@link Cli::getSchema()}. |
258
|
|
|
* @return bool Returns true if the command def has options or false otherwise. |
259
|
|
|
*/ |
260
|
|
|
protected function hasOptionsDef($commandDef) { |
261
|
|
|
return count($commandDef) > 1 || (count($commandDef) > 0 && !isset($commandDef[Cli::META])); |
262
|
2 |
|
} |
263
|
2 |
|
|
264
|
|
|
/** |
265
|
|
|
* Determines whether or not a command has args. |
266
|
|
|
* |
267
|
|
|
* @param string $command The command name to check. |
268
|
|
|
* @return int Returns one of the following. |
269
|
|
|
* - 0: The command has no args. |
270
|
|
|
* - 1: The command has optional args. |
271
|
|
|
* - 2: The command has required args. |
272
|
|
|
*/ |
273
|
|
|
public function hasArgs($command = '') { |
274
|
|
|
$args = null; |
275
|
|
|
|
276
|
|
|
if ($command) { |
277
|
|
|
// Check to see if the specific command has args. |
278
|
32 |
|
$def = $this->getSchema($command); |
279
|
|
|
if (isset($def[Cli::META][Cli::ARGS])) { |
280
|
32 |
|
$args = $def[Cli::META][Cli::ARGS]; |
281
|
32 |
|
} |
282
|
31 |
|
} else { |
283
|
31 |
|
foreach ($this->commandSchemas as $pattern => $def) { |
284
|
|
|
if (isset($def[Cli::META][Cli::ARGS])) { |
285
|
32 |
|
$args = $def[Cli::META][Cli::ARGS]; |
286
|
|
|
} |
287
|
32 |
|
} |
288
|
|
|
if (!empty($args)) { |
289
|
|
|
return 1; |
290
|
32 |
|
} |
291
|
|
|
} |
292
|
2 |
|
|
293
|
2 |
|
if (!$args || empty($args)) { |
294
|
2 |
|
return 0; |
295
|
32 |
|
} |
296
|
|
|
|
297
|
4 |
|
foreach ($args as $arg) { |
298
|
4 |
|
if (!Cli::val('required', $arg)) { |
299
|
4 |
|
return 1; |
300
|
4 |
|
} |
301
|
|
|
} |
302
|
26 |
|
return 2; |
303
|
26 |
|
} |
304
|
|
|
|
305
|
32 |
|
/** |
306
|
31 |
|
* Finds our whether a pattern is a command. |
307
|
31 |
|
* |
308
|
14 |
|
* @param string $pattern The pattern being evaluated. |
309
|
|
|
* @return bool Returns `true` if `$pattern` is a command, `false` otherwise. |
310
|
18 |
|
*/ |
311
|
|
|
public static function isCommand($pattern) { |
312
|
|
|
return strpos($pattern, '*') === false; |
313
|
18 |
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Parses and validates a set of command line arguments the schema. |
317
|
|
|
* |
318
|
|
|
* @param array $argv The command line arguments a form compatible with the global `$argv` variable. |
319
|
|
|
* |
320
|
|
|
* Note that the `$argv` array must have at least one element and it must represent the path to the command that |
321
|
|
|
* invoked the command. This is used to write usage information. |
322
|
|
|
* @param bool $exit Whether to exit the application when there is an error or when writing help. |
323
|
|
|
* @return Args|null Returns an {@see Args} instance when a command should be executed |
324
|
|
|
* or `null` when one should not be executed. |
325
|
|
|
* @throws \Exception Throws an exception when {@link $exit} is false and the help or errors need to be displayed. |
326
|
32 |
|
*/ |
327
|
32 |
|
public function parse($argv = null, $exit = true) { |
328
|
|
|
$formatOutputBak = $this->formatOutput; |
329
|
|
|
// Only format commands if we are exiting. |
330
|
|
|
if (!$exit) { |
331
|
32 |
|
$this->formatOutput = false; |
332
|
|
|
} |
333
|
|
|
if (!$exit) { |
334
|
|
|
ob_start(); |
335
|
32 |
|
} |
336
|
32 |
|
|
337
|
|
|
$args = $this->parseRaw($argv); |
338
|
32 |
|
|
339
|
32 |
|
$hasCommand = $this->hasCommand(); |
340
|
32 |
|
|
341
|
|
|
|
342
|
32 |
|
if ($hasCommand && !$args->getCommand()) { |
343
|
|
|
// If no command is given then write a list of commands. |
344
|
32 |
|
$this->writeUsage($args); |
345
|
4 |
|
$this->writeCommands(); |
346
|
4 |
|
$result = null; |
347
|
2 |
|
} elseif ($args->getOpt('help') || $args->getOpt('?')) { |
348
|
2 |
|
// Write the help. |
349
|
2 |
|
$this->writeUsage($args); |
350
|
2 |
|
$this->writeHelp($args->getCommand()); |
351
|
|
|
$result = null; |
352
|
4 |
|
} else { |
353
|
|
|
// Validate the arguments against the schema. |
354
|
32 |
|
$validArgs = $this->validate($args); |
355
|
30 |
|
$result = $validArgs; |
356
|
30 |
|
} |
357
|
32 |
|
if (!$exit) { |
358
|
32 |
|
$this->formatOutput = $formatOutputBak; |
359
|
32 |
|
$output = ob_get_clean(); |
360
|
32 |
|
if ($result === null) { |
361
|
|
|
throw new \Exception(trim($output)); |
362
|
|
|
} |
363
|
30 |
|
} elseif ($result === null) { |
364
|
30 |
|
exit(); |
|
|
|
|
365
|
30 |
|
} |
366
|
30 |
|
return $result; |
367
|
30 |
|
} |
368
|
32 |
|
|
369
|
|
|
/** |
370
|
|
|
* Parse an array of arguments. |
371
|
32 |
|
* |
372
|
32 |
|
* If the first item in the array is in the form of a command (no preceding - or --), |
373
|
|
|
* 'command' is filled with its value. |
374
|
32 |
|
* |
375
|
|
|
* @param array $argv An array of arguments passed in a form compatible with the global `$argv` variable. |
376
|
|
|
* @return Args Returns the raw parsed arguments. |
377
|
|
|
* @throws \Exception Throws an exception when {@see $argv} isn't an array. |
378
|
32 |
|
*/ |
379
|
|
|
protected function parseRaw($argv = null) { |
|
|
|
|
380
|
18 |
|
if ($argv === null) { |
381
|
18 |
|
$argv = $GLOBALS['argv']; |
382
|
18 |
|
} |
383
|
|
|
|
384
|
|
|
if (!is_array($argv)) { |
385
|
|
|
throw new \Exception(__METHOD__ . " expects an array", 400); |
386
|
|
|
} |
387
|
18 |
|
|
388
|
5 |
|
$path = array_shift($argv); |
389
|
5 |
|
$hasCommand = $this->hasCommand(); |
390
|
1 |
|
|
391
|
|
|
$parsed = new Args(); |
392
|
1 |
|
$parsed->setMeta('path', $path); |
393
|
1 |
|
$parsed->setMeta('filename', basename($path)); |
394
|
|
|
|
395
|
|
|
if (count($argv)) { |
396
|
|
|
// Get possible command. |
397
|
1 |
|
if (substr($argv[0], 0, 1) != '-') { |
398
|
4 |
|
$arg0 = array_shift($argv); |
399
|
|
|
if ($hasCommand) { |
400
|
18 |
|
$parsed->setCommand($arg0); |
401
|
8 |
|
} else { |
402
|
8 |
|
$schema = $this->getSchema($parsed->getCommand()); |
403
|
9 |
|
$this->addArg($schema, $parsed, $arg0); |
404
|
|
|
} |
405
|
18 |
|
} |
406
|
32 |
|
// Get the data types for all of the commands. |
407
|
|
|
if (!isset($schema)) { |
408
|
|
|
$schema = $this->getSchema($parsed->getCommand()); |
409
|
8 |
|
} |
410
|
8 |
|
$types = []; |
411
|
8 |
|
foreach ($schema as $sName => $sRow) { |
412
|
|
|
if ($sName === Cli::META) { |
413
|
8 |
|
continue; |
414
|
|
|
} |
415
|
5 |
|
|
416
|
|
|
$type = Cli::val('type', $sRow, 'string'); |
417
|
5 |
|
$types[$sName] = $type; |
418
|
2 |
|
if (isset($sRow['short'])) { |
419
|
|
|
$types[$sRow['short']] = $type; |
420
|
1 |
|
} |
421
|
1 |
|
} |
422
|
1 |
|
|
423
|
2 |
|
// Parse opts. |
424
|
|
|
for ($i = 0; $i < count($argv); $i++) { |
|
|
|
|
425
|
5 |
|
$str = $argv[$i]; |
426
|
|
|
|
427
|
4 |
|
if ($str === '--') { |
428
|
4 |
|
// -- |
429
|
4 |
|
$i++; |
430
|
|
|
break; |
431
|
|
|
} elseif (strlen($str) > 2 && substr($str, 0, 2) == '--') { |
432
|
|
|
// --foo |
433
|
5 |
|
$str = substr($str, 2); |
434
|
|
|
$parts = explode('=', $str); |
435
|
8 |
|
$key = $parts[0]; |
436
|
3 |
|
|
437
|
3 |
|
// Does not have an =, so choose the next arg as its value, |
438
|
|
|
// unless it is defined as 'boolean' in which case there is no |
439
|
8 |
|
// value to seek in next arg |
440
|
22 |
|
if (count($parts) == 1 && isset($argv[$i + 1]) && preg_match('/^--?.+/', $argv[$i + 1]) == 0) { |
441
|
|
|
$v = $argv[$i + 1]; |
442
|
16 |
|
if (Cli::val($key, $types) == 'boolean') { |
443
|
16 |
|
if (in_array($v, ['0', '1', 'true', 'false', 'on', 'off', 'yes', 'no'])) { |
444
|
16 |
|
// The next arg looks like a boolean to me. |
445
|
16 |
|
$i++; |
446
|
|
|
} else { |
447
|
|
|
// Next arg is not a boolean: set the flag on, and use next arg in its own iteration |
448
|
16 |
|
$v = true; |
449
|
2 |
|
} |
450
|
2 |
|
} else { |
451
|
|
|
$i++; |
452
|
|
|
} |
453
|
|
|
} elseif (count($parts) == 2) {// Has a =, so pick the second piece |
454
|
|
|
$v = $parts[1]; |
455
|
2 |
|
} else { |
456
|
|
|
$v = true; |
457
|
16 |
|
} |
458
|
5 |
|
$parsed->setOpt($key, $v); |
459
|
|
|
} elseif (strlen($str) == 2 && $str[0] == '-') { |
460
|
3 |
|
// -a |
461
|
3 |
|
|
462
|
3 |
|
$key = $str[1]; |
463
|
|
|
$type = Cli::val($key, $types, 'boolean'); |
464
|
3 |
|
$v = null; |
465
|
|
|
|
466
|
16 |
|
if (isset($argv[$i + 1])) { |
467
|
|
|
// Try and be smart about the next arg. |
468
|
13 |
|
$nextArg = $argv[$i + 1]; |
469
|
13 |
|
|
470
|
3 |
|
if ($type === 'boolean') { |
471
|
3 |
|
if ($this->isStrictBoolean($nextArg)) { |
472
|
|
|
// The next arg looks like a boolean to me. |
473
|
2 |
|
$v = $nextArg; |
474
|
2 |
|
$i++; |
475
|
2 |
|
} else { |
476
|
|
|
$v = true; |
477
|
2 |
|
} |
478
|
2 |
|
} elseif (!preg_match('/^--?.+/', $argv[$i + 1])) { |
479
|
|
|
// The next arg is not an opt. |
480
|
3 |
|
$v = $nextArg; |
481
|
|
|
$i++; |
482
|
|
|
} else { |
483
|
|
|
// The next arg is another opt. |
484
|
6 |
|
$v = null; |
485
|
16 |
|
} |
486
|
|
|
} |
487
|
1 |
|
|
488
|
|
|
if ($v === null) { |
489
|
31 |
|
$v = Cli::val($type, ['boolean' => true, 'integer' => 1, 'string' => '']); |
490
|
|
|
} |
491
|
|
|
|
492
|
32 |
|
$parsed->setOpt($key, $v); |
493
|
1 |
|
} elseif (strlen($str) > 1 && $str[0] == '-') { |
494
|
1 |
|
// -abcdef |
495
|
32 |
|
for ($j = 1; $j < strlen($str); $j++) { |
496
|
|
|
$opt = $str[$j]; |
497
|
32 |
|
$remaining = substr($str, $j + 1); |
498
|
|
|
$type = Cli::val($opt, $types, 'boolean'); |
499
|
|
|
|
500
|
|
|
// Check for an explicit equals sign. |
501
|
|
|
if (substr($remaining, 0, 1) === '=') { |
502
|
|
|
$remaining = substr($remaining, 1); |
503
|
|
|
if ($type === 'boolean') { |
504
|
|
|
// Bypass the boolean flag checking below. |
505
|
|
|
$parsed->setOpt($opt, $remaining); |
506
|
26 |
|
break; |
507
|
26 |
|
} |
508
|
26 |
|
} |
509
|
26 |
|
|
510
|
26 |
|
if ($type === 'boolean') { |
511
|
26 |
|
if (preg_match('`^([01])`', $remaining, $matches)) { |
512
|
|
|
// Treat the 0 or 1 as a true or false. |
513
|
|
|
$parsed->setOpt($opt, $matches[1]); |
514
|
26 |
|
$j += strlen($matches[1]); |
515
|
26 |
|
} else { |
516
|
26 |
|
// Treat the option as a flag. |
517
|
|
|
$parsed->setOpt($opt, true); |
518
|
|
|
} |
519
|
26 |
|
} elseif ($type === 'string') { |
520
|
|
|
// Treat the option as a set with no = sign. |
521
|
|
|
$parsed->setOpt($opt, $remaining); |
522
|
|
|
break; |
523
|
|
|
} elseif ($type === 'integer') { |
524
|
|
|
if (preg_match('`^(\d+)`', $remaining, $matches)) { |
525
|
26 |
|
// Treat the option as a set with no = sign. |
526
|
|
|
$parsed->setOpt($opt, $matches[1]); |
527
|
26 |
|
$j += strlen($matches[1]); |
528
|
|
|
} else { |
529
|
25 |
|
// Treat the option as either multiple flags. |
530
|
|
|
$optVal = $parsed->getOpt($opt, 0); |
531
|
25 |
|
$parsed->setOpt($opt, $optVal + 1); |
532
|
|
|
} |
533
|
10 |
|
} else { |
534
|
10 |
|
// This should not happen unless we've put a bug in our code. |
535
|
7 |
|
throw new \Exception("Invalid type $type for $opt.", 500); |
536
|
7 |
|
} |
537
|
3 |
|
} |
538
|
|
|
} else { |
539
|
10 |
|
// End of opts |
540
|
25 |
|
break; |
541
|
|
|
} |
542
|
20 |
|
} |
543
|
20 |
|
|
544
|
19 |
|
// Grab the remaining args. |
545
|
19 |
|
for (; $i < count($argv); $i++) { |
|
|
|
|
546
|
2 |
|
$this->addArg($schema, $parsed, $argv[$i]); |
547
|
|
|
} |
548
|
20 |
|
} |
549
|
23 |
|
|
550
|
|
|
return $parsed; |
551
|
4 |
|
} |
552
|
|
|
|
553
|
4 |
|
/** |
554
|
1 |
|
* Validates arguments against the schema. |
555
|
1 |
|
* |
556
|
4 |
|
* @param Args $args The arguments that were returned from {@link Cli::parseRaw()}. |
557
|
2 |
|
* @return Args|null |
558
|
2 |
|
*/ |
559
|
1 |
|
public function validate(Args $args) { |
560
|
|
|
$isValid = true; |
561
|
4 |
|
$command = $args->getCommand(); |
562
|
18 |
|
$valid = new Args($command); |
563
|
|
|
$schema = $this->getSchema($command); |
564
|
2 |
|
ksort($schema); |
565
|
2 |
|
|
566
|
2 |
|
// $meta = $schema[Cli::META]; |
|
|
|
|
567
|
26 |
|
unset($schema[Cli::META]); |
568
|
|
|
$opts = $args->getOpts(); |
569
|
26 |
|
$missing = []; |
570
|
2 |
|
|
571
|
2 |
|
// Check to see if the command is correct. |
572
|
2 |
|
if ($command && !$this->hasCommand($command) && $this->hasCommand()) { |
573
|
2 |
|
echo $this->red("Invalid command: $command.".PHP_EOL); |
574
|
2 |
|
$isValid = false; |
575
|
|
|
} |
576
|
26 |
|
|
577
|
|
|
// Add the args. |
578
|
|
|
$valid->setArgs($args->getArgs()); |
579
|
|
|
|
580
|
|
|
foreach ($schema as $key => $definition) { |
581
|
|
|
// No Parameter (default) |
|
|
|
|
582
|
|
|
$type = Cli::val('type', $definition, 'string'); |
583
|
26 |
|
|
584
|
18 |
|
if (isset($opts[$key])) { |
585
|
|
|
// Check for --key. |
586
|
8 |
|
$value = $opts[$key]; |
587
|
8 |
|
if ($this->validateType($value, $type, $key, $definition)) { |
588
|
|
|
$valid->setOpt($key, $value); |
589
|
|
|
} else { |
590
|
|
|
$isValid = false; |
591
|
|
|
} |
592
|
|
|
unset($opts[$key]); |
593
|
|
|
} elseif (isset($definition['short']) && isset($opts[$definition['short']])) { |
594
|
|
|
// Check for -s. |
595
|
|
|
$value = $opts[$definition['short']]; |
596
|
|
|
if ($this->validateType($value, $type, $key, $definition)) { |
597
|
32 |
|
$valid->setOpt($key, $value); |
598
|
32 |
|
} else { |
599
|
32 |
|
$isValid = false; |
600
|
32 |
|
} |
601
|
32 |
|
unset($opts[$definition['short']]); |
602
|
32 |
|
} elseif (isset($opts['no-'.$key])) { |
603
|
32 |
|
// Check for --no-key. |
604
|
32 |
|
$value = $opts['no-'.$key]; |
605
|
|
|
|
606
|
|
|
if ($type !== 'boolean') { |
607
|
|
|
echo $this->red("Cannot apply the --no- prefix on the non boolean --$key.".PHP_EOL); |
608
|
|
|
$isValid = false; |
609
|
|
|
} elseif ($this->validateType($value, $type, $key, $definition)) { |
610
|
|
|
$valid->setOpt($key, !$value); |
611
|
|
|
} else { |
612
|
|
|
$isValid = false; |
613
|
|
|
} |
614
|
8 |
|
unset($opts['no-'.$key]); |
615
|
8 |
|
} elseif ($definition['required']) { |
616
|
8 |
|
// The key was not supplied. Is it required? |
617
|
8 |
|
$missing[$key] = true; |
618
|
|
|
$valid->setOpt($key, false); |
619
|
|
|
} |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
if (count($missing)) { |
623
|
|
|
$isValid = false; |
624
|
|
|
foreach ($missing as $key => $v) { |
625
|
|
|
echo $this->red("Missing required option: $key".PHP_EOL); |
626
|
|
|
} |
627
|
|
|
} |
628
|
|
|
|
629
|
|
|
if (count($opts)) { |
630
|
|
|
$isValid = false; |
631
|
|
|
foreach ($opts as $key => $v) { |
632
|
|
|
echo $this->red("Invalid option: $key".PHP_EOL); |
633
|
|
|
} |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
if ($isValid) { |
637
|
30 |
|
return $valid; |
638
|
|
|
} else { |
639
|
30 |
|
echo PHP_EOL; |
640
|
30 |
|
return null; |
641
|
28 |
|
} |
642
|
28 |
|
} |
643
|
28 |
|
|
644
|
28 |
|
/** |
645
|
25 |
|
* Gets the full cli schema. |
646
|
25 |
|
* |
647
|
21 |
|
* @param string $command The name of the command. This can be left blank if there is no command. |
648
|
21 |
|
* @return array Returns the schema that matches the command. |
649
|
21 |
|
*/ |
650
|
21 |
|
public function getSchema($command = '') { |
651
|
|
|
$result = []; |
652
|
|
|
foreach ($this->commandSchemas as $pattern => $opts) { |
653
|
|
|
if (fnmatch($pattern, $command)) { |
654
|
|
|
$result = array_replace_recursive($result, $opts); |
655
|
|
|
} |
656
|
30 |
|
} |
657
|
30 |
|
return $result; |
658
|
30 |
|
} |
659
|
|
|
|
660
|
30 |
|
/** |
661
|
30 |
|
* Gets/sets the value for a current meta item. |
662
|
|
|
* |
663
|
|
|
* @param string $name The name of the meta key. |
664
|
|
|
* @param mixed $value Set a new value for the meta key. |
665
|
|
|
* @return Cli|mixed Returns the current value of the meta item or `$this` for fluent setting. |
666
|
|
|
*/ |
667
|
|
|
public function meta($name, $value = null) { |
668
|
|
|
if ($value !== null) { |
669
|
|
|
$this->currentSchema[Cli::META][$name] = $value; |
670
|
|
|
return $this; |
671
|
|
|
} |
672
|
5 |
|
if (!isset($this->currentSchema[Cli::META][$name])) { |
673
|
5 |
|
return null; |
674
|
5 |
|
} |
675
|
5 |
|
return $this->currentSchema[Cli::META][$name]; |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
/** |
679
|
|
|
* Adds an option (opt) to the current schema. |
680
|
|
|
* |
681
|
|
|
* @param string $name The long name(s) of the parameter. |
682
|
|
|
* You can use either just one name or a string in the form 'long:short' to specify the long and short name. |
683
|
|
|
* @param string $description A human-readable description for the column. |
684
|
4 |
|
* @param bool $required Whether or not the opt is required. |
685
|
4 |
|
* @param string $type The type of parameter. |
686
|
4 |
|
* This must be one of string, bool, integer. |
687
|
4 |
|
* @return Cli Returns this object for fluent calls. |
688
|
4 |
|
* @throws \Exception Throws an exception when the type is invalid. |
689
|
|
|
*/ |
690
|
4 |
|
public function opt($name, $description, $required = false, $type = 'string') { |
691
|
|
|
switch ($type) { |
692
|
|
|
case 'str': |
693
|
|
|
case 'string': |
694
|
|
|
$type = 'string'; |
695
|
|
|
break; |
696
|
|
|
case 'bool': |
697
|
|
|
case 'boolean': |
698
|
|
|
$type = 'boolean'; |
699
|
|
|
break; |
700
|
|
|
case 'int': |
701
|
|
|
case 'integer': |
702
|
2 |
|
$type = 'integer'; |
703
|
2 |
|
break; |
704
|
|
|
default: |
705
|
|
|
throw new \Exception("Invalid type: $type. Must be one of string, boolean, or integer.", 422); |
706
|
2 |
|
} |
707
|
1 |
|
|
708
|
1 |
|
// Break the name up into its long and short form. |
709
|
2 |
|
$parts = explode(':', $name, 2); |
710
|
|
|
$long = $parts[0]; |
711
|
|
|
$short = static::val(1, $parts, ''); |
712
|
|
|
|
713
|
2 |
|
$this->currentSchema[$long] = ['description' => $description, 'required' => $required, 'type' => $type, 'short' => $short]; |
714
|
2 |
|
return $this; |
715
|
|
|
} |
716
|
|
|
|
717
|
|
|
/** |
718
|
|
|
* Define an arg on the current command. |
719
|
|
|
* |
720
|
|
|
* @param string $name The name of the arg. |
721
|
|
|
* @param string $description The arg description. |
722
|
|
|
* @param bool $required Whether or not the arg is required. |
723
|
|
|
* @return Cli Returns $this for fluent calls. |
724
|
|
|
*/ |
725
|
|
|
public function arg($name, $description, $required = false) { |
726
|
|
|
$this->currentSchema[Cli::META][Cli::ARGS][$name] = |
727
|
|
|
['description' => $description, 'required' => $required]; |
728
|
|
|
return $this; |
729
|
|
|
} |
730
|
|
|
|
731
|
|
|
/** |
732
|
|
|
* Selects the current command schema name. |
733
|
|
|
* |
734
|
|
|
* @param string $pattern The command pattern. |
735
|
1 |
|
* @return Cli Returns $this for fluent calls. |
736
|
1 |
|
*/ |
737
|
|
|
public function command($pattern) { |
738
|
1 |
|
if (!isset($this->commandSchemas[$pattern])) { |
739
|
1 |
|
$this->commandSchemas[$pattern] = [Cli::META => []]; |
740
|
|
|
} |
741
|
|
|
$this->currentSchema =& $this->commandSchemas[$pattern]; |
742
|
|
|
|
743
|
|
|
return $this; |
744
|
|
|
} |
745
|
|
|
|
746
|
|
|
|
747
|
6 |
|
/** |
748
|
6 |
|
* Determine weather or not a value can be represented as a boolean. |
749
|
|
|
* |
750
|
|
|
* This method is sort of like {@link Cli::validateType()} but requires a more strict check of a boolean value. |
751
|
|
|
* |
752
|
|
|
* @param mixed $value The value to test. |
753
|
|
|
* @return bool |
754
|
|
|
*/ |
755
|
|
|
protected function isStrictBoolean($value, &$boolValue = null) { |
756
|
|
|
if ($value === true || $value === false) { |
757
|
|
|
$boolValue = $value; |
758
|
|
|
return true; |
759
|
|
|
} elseif (in_array($value, ['0', 'false', 'off', 'no'])) { |
760
|
|
|
$boolValue = false; |
761
|
|
|
return true; |
762
|
|
|
} elseif (in_array($value, ['1', 'true', 'on', 'yes'])) { |
763
|
|
|
$boolValue = true; |
764
|
|
|
return true; |
765
|
|
|
} else { |
766
|
|
|
$boolValue = null; |
767
|
8 |
|
return false; |
768
|
8 |
|
} |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* Set the schema for a command. |
773
|
|
|
* |
774
|
|
|
* The schema array uses a short syntax so that commands can be specified as quickly as possible. |
775
|
|
|
* This schema is the exact same as those provided to {@link Schema::create()}. |
776
|
|
|
* The basic format of the array is the following: |
777
|
|
|
* |
778
|
|
|
* ``` |
779
|
|
|
* [ |
780
|
|
|
* type:name[:shortCode][?], |
781
|
|
|
* type:name[:shortCode][?], |
782
|
|
|
* ... |
783
|
|
|
* ] |
784
|
|
|
* ``` |
785
|
|
|
* |
786
|
|
|
* @param array $schema The schema array. |
787
|
|
|
*/ |
788
|
|
|
public function schema(array $schema) { |
789
|
|
|
$parsed = static::parseSchema($schema); |
790
|
|
|
|
791
|
|
|
$this->currentSchema = array_replace($this->currentSchema, $parsed); |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
/** |
795
|
|
|
* Bold some text. |
796
|
|
|
* |
797
|
|
|
* @param string $text The text to format. |
798
|
|
|
* @return string Returns the text surrounded by formatting commands. |
799
|
|
|
*/ |
800
|
|
|
public function bold($text) { |
801
|
|
|
return $this->formatString($text, ["\033[1m", "\033[0m"]); |
802
|
|
|
} |
803
|
|
|
|
804
|
|
|
/** |
805
|
|
|
* Bold some text. |
806
|
|
|
* |
807
|
|
|
* @param string $text The text to format. |
808
|
|
|
* @return string Returns the text surrounded by formatting commands. |
809
|
|
|
*/ |
810
|
|
|
public static function boldText($text) { |
811
|
|
|
return "\033[1m{$text}\033[0m"; |
812
|
|
|
} |
813
|
|
|
|
814
|
|
|
/** |
815
|
|
|
* Make some text red. |
816
|
|
|
* |
817
|
|
|
* @param string $text The text to format. |
818
|
|
|
* @return string Returns text surrounded by formatting commands. |
819
|
|
|
*/ |
820
|
|
|
public function red($text) { |
821
|
|
|
return $this->formatString($text, ["\033[1;31m", "\033[0m"]); |
822
|
|
|
} |
823
|
|
|
|
824
|
|
|
/** |
825
|
|
|
* Make some text red. |
826
|
|
|
* |
827
|
|
|
* @param string $text The text to format. |
828
|
|
|
* @return string Returns text surrounded by formatting commands. |
829
|
|
|
*/ |
830
|
|
|
public static function redText($text) { |
831
|
|
|
return "\033[1;31m{$text}\033[0m"; |
832
|
|
|
} |
833
|
|
|
|
834
|
|
|
/** |
835
|
|
|
* Make some text green. |
836
|
|
|
* |
837
|
|
|
* @param string $text The text to format. |
838
|
|
|
* @return string Returns text surrounded by formatting commands. |
839
|
|
|
*/ |
840
|
|
|
public function green($text) { |
841
|
|
|
return $this->formatString($text, ["\033[1;32m", "\033[0m"]); |
842
|
|
|
} |
843
|
|
|
|
844
|
|
|
/** |
845
|
|
|
* Make some text green. |
846
|
|
|
* |
847
|
|
|
* @param string $text The text to format. |
848
|
14 |
|
* @return string Returns text surrounded by formatting commands. |
849
|
14 |
|
*/ |
850
|
|
|
public static function greenText($text) { |
851
|
|
|
return "\033[1;32m{$text}\033[0m"; |
852
|
14 |
|
} |
853
|
|
|
|
854
|
|
|
/** |
855
|
|
|
* Make some text blue. |
856
|
|
|
* |
857
|
|
|
* @param string $text The text to format. |
858
|
|
|
* @return string Returns text surrounded by formatting commands. |
859
|
|
|
*/ |
860
|
|
|
public function blue($text) { |
861
|
|
|
return $this->formatString($text, ["\033[1;34m", "\033[0m"]); |
862
|
|
|
} |
863
|
|
|
|
864
|
|
|
/** |
865
|
|
|
* Make some text blue. |
866
|
|
|
* |
867
|
|
|
* @param string $text The text to format. |
868
|
|
|
* @return string Returns text surrounded by formatting commands. |
869
|
|
|
*/ |
870
|
|
|
public static function blueText($text) { |
871
|
|
|
return "\033[1;34m{$text}\033[0m"; |
872
|
|
|
} |
873
|
|
|
|
874
|
|
|
/** |
875
|
|
|
* Make some text purple. |
876
|
|
|
* |
877
|
|
|
* @param string $text The text to format. |
878
|
25 |
|
* @return string Returns text surrounded by formatting commands. |
879
|
|
|
*/ |
880
|
25 |
|
public function purple($text) { |
881
|
15 |
|
return $this->formatString($text, ["\033[0;35m", "\033[0m"]); |
882
|
10 |
|
} |
883
|
15 |
|
|
884
|
|
|
/** |
885
|
|
|
* Make some text purple. |
886
|
|
|
* |
887
|
8 |
|
* @param string $text The text to format. |
888
|
5 |
|
* @return string Returns text surrounded by formatting commands. |
889
|
5 |
|
*/ |
890
|
8 |
|
public static function purpleText($text) { |
891
|
2 |
|
return "\033[0;35m{$text}\033[0m"; |
892
|
2 |
|
} |
893
|
2 |
|
|
894
|
3 |
|
/** |
895
|
|
|
* Format some text for the console. |
896
|
15 |
|
* |
897
|
22 |
|
* @param string $text The text to format. |
898
|
9 |
|
* @param string[] $wrap The format to wrap in the form ['before', 'after']. |
899
|
6 |
|
* @return string Returns the string formatted according to {@link Cli::$format}. |
900
|
6 |
|
*/ |
901
|
6 |
|
protected function formatString($text, array $wrap) { |
902
|
3 |
|
if ($this->formatOutput) { |
903
|
|
|
return "{$wrap[0]}$text{$wrap[1]}"; |
904
|
9 |
|
} else { |
905
|
21 |
|
return $text; |
906
|
21 |
|
} |
907
|
21 |
|
} |
908
|
21 |
|
|
909
|
|
|
/** |
910
|
|
|
* Guess whether or not to format the output with colors. |
911
|
|
|
* |
912
|
|
|
* If the current environment is being redirected to a file then output should not be formatted. Also, Windows |
913
|
25 |
|
* machines do not support terminal colors so formatting should be suppressed on them too. |
914
|
6 |
|
* |
915
|
6 |
|
* @return bool Returns **true** if the output can be formatter or **false** otherwise. |
916
|
6 |
|
*/ |
917
|
6 |
|
public static function guessFormatOutput() { |
918
|
|
|
if (defined('PHP_WINDOWS_VERSION_MAJOR')) { |
919
|
25 |
|
return false; |
920
|
|
|
} elseif (function_exists('posix_isatty')) { |
921
|
|
|
return posix_isatty(STDOUT); |
922
|
|
|
} else { |
923
|
|
|
return true; |
924
|
|
|
} |
925
|
2 |
|
} |
926
|
2 |
|
|
927
|
|
|
/** |
928
|
2 |
|
* Sleep for a number of seconds, echoing out a dot on each second. |
929
|
2 |
|
* |
930
|
2 |
|
* @param int $seconds The number of seconds to sleep. |
931
|
|
|
*/ |
932
|
2 |
|
public static function sleep($seconds) { |
933
|
2 |
|
for ($i = 0; $i < $seconds; $i++) { |
934
|
2 |
|
sleep(1); |
935
|
2 |
|
echo '.'; |
936
|
2 |
|
} |
937
|
2 |
|
} |
938
|
2 |
|
|
939
|
|
|
/** |
940
|
|
|
* Validate the type of a value and coerce it into the proper type. |
941
|
|
|
* |
942
|
|
|
* @param mixed &$value The value to validate. |
943
|
|
|
* @param string $type One of: bool, int, string. |
944
|
|
|
* @param string $name The name of the option if you want to print an error message. |
945
|
4 |
|
* @param array|null $def The option def if you want to print an error message. |
946
|
4 |
|
* @return bool Returns `true` if the value is the correct type. |
947
|
4 |
|
* @throws \Exception Throws an exception when {@see $type} is not a known value. |
948
|
4 |
|
*/ |
949
|
|
|
protected function validateType(&$value, $type, $name = '', $def = null) { |
950
|
|
|
switch ($type) { |
951
|
|
|
case 'boolean': |
952
|
|
|
if (is_bool($value)) { |
953
|
|
|
$valid = true; |
954
|
|
|
} elseif ($value === 0) { |
955
|
4 |
|
// 0 doesn't work well with in_array() so check it separately. |
956
|
|
|
$value = false; |
957
|
4 |
|
$valid = true; |
958
|
4 |
|
} elseif (in_array($value, [null, '', '0', 'false', 'no', 'disabled'])) { |
959
|
|
|
$value = false; |
960
|
4 |
|
$valid = true; |
961
|
3 |
|
} elseif (in_array($value, [1, '1', 'true', 'yes', 'enabled'])) { |
962
|
3 |
|
$value = true; |
963
|
|
|
$valid = true; |
964
|
4 |
|
} else { |
965
|
|
|
$valid = false; |
966
|
|
|
} |
967
|
4 |
|
break; |
968
|
4 |
|
case 'integer': |
969
|
4 |
|
if (is_numeric($value)) { |
970
|
|
|
$value = (int)$value; |
971
|
4 |
|
$valid = true; |
972
|
|
|
} else { |
973
|
4 |
|
$valid = false; |
974
|
|
|
} |
975
|
4 |
|
break; |
976
|
|
|
case 'string': |
977
|
4 |
|
$value = (string)$value; |
978
|
4 |
|
$valid = true; |
979
|
|
|
break; |
980
|
4 |
|
default: |
981
|
4 |
|
throw new \Exception("Unknown type: $type.", 400); |
982
|
|
|
} |
983
|
|
|
|
984
|
4 |
|
if (!$valid && $name) { |
985
|
4 |
|
$short = static::val('short', (array)$def); |
986
|
4 |
|
$nameStr = "--$name".($short ? " (-$short)" : ''); |
987
|
4 |
|
echo $this->red("The value of $nameStr is not a valid $type.".PHP_EOL); |
988
|
4 |
|
} |
989
|
2 |
|
|
990
|
2 |
|
return $valid; |
991
|
4 |
|
} |
992
|
|
|
|
993
|
|
|
/** |
994
|
|
|
* Writes a lis of all of the commands. |
995
|
4 |
|
*/ |
996
|
4 |
|
protected function writeCommands() { |
997
|
|
|
echo static::bold("COMMANDS").PHP_EOL; |
998
|
4 |
|
|
999
|
4 |
|
$table = new Table(); |
1000
|
|
|
foreach ($this->commandSchemas as $pattern => $schema) { |
1001
|
4 |
|
if (static::isCommand($pattern)) { |
1002
|
4 |
|
$table |
1003
|
2 |
|
->row() |
1004
|
|
|
->cell($pattern) |
1005
|
2 |
|
->cell(Cli::val('description', Cli::val(Cli::META, $schema), '')); |
1006
|
2 |
|
} |
1007
|
|
|
} |
1008
|
2 |
|
$table->write(); |
1009
|
2 |
|
} |
1010
|
|
|
|
1011
|
2 |
|
/** |
1012
|
1 |
|
* Writes the cli help. |
1013
|
1 |
|
* |
1014
|
1 |
|
* @param string $command The name of the command or blank if there is no command. |
1015
|
|
|
*/ |
1016
|
|
|
public function writeHelp($command = '') { |
1017
|
2 |
|
$schema = $this->getSchema($command); |
1018
|
2 |
|
$this->writeSchemaHelp($schema); |
1019
|
2 |
|
} |
1020
|
2 |
|
|
1021
|
2 |
|
/** |
1022
|
4 |
|
* Writes the help for a given schema. |
1023
|
|
|
* |
1024
|
|
|
* @param array $schema A command line scheme returned from {@see Cli::getSchema()}. |
1025
|
|
|
*/ |
1026
|
|
|
protected function writeSchemaHelp($schema) { |
1027
|
|
|
// Write the command description. |
1028
|
|
|
$meta = Cli::val(Cli::META, $schema, []); |
1029
|
6 |
|
$description = Cli::val('description', $meta); |
1030
|
6 |
|
|
1031
|
6 |
|
if ($description) { |
1032
|
6 |
|
echo implode("\n", Cli::breakLines($description, 80, false)).PHP_EOL.PHP_EOL; |
1033
|
|
|
} |
1034
|
6 |
|
|
1035
|
|
|
unset($schema[Cli::META]); |
1036
|
6 |
|
|
1037
|
4 |
|
// Add the help. |
1038
|
2 |
|
$schema['help'] = [ |
1039
|
|
|
'description' => 'Display this help.', |
1040
|
2 |
|
'type' => 'boolean', |
1041
|
2 |
|
'short' => '?' |
1042
|
|
|
]; |
1043
|
4 |
|
|
1044
|
|
|
echo Cli::bold('OPTIONS').PHP_EOL; |
1045
|
6 |
|
|
1046
|
6 |
|
ksort($schema); |
1047
|
6 |
|
|
1048
|
|
|
$table = new Table(); |
1049
|
6 |
|
$table->setFormatOutput($this->formatOutput); |
1050
|
4 |
|
|
1051
|
4 |
|
foreach ($schema as $key => $definition) { |
1052
|
|
|
$table->row(); |
1053
|
6 |
|
|
1054
|
6 |
|
// Write the keys. |
1055
|
6 |
|
$keys = "--{$key}"; |
1056
|
|
|
if ($shortKey = Cli::val('short', $definition, false)) { |
1057
|
|
|
$keys .= ", -$shortKey"; |
1058
|
|
|
} |
1059
|
|
|
if (Cli::val('required', $definition)) { |
1060
|
|
|
$table->bold($keys); |
1061
|
|
|
} else { |
1062
|
|
|
$table->cell($keys); |
1063
|
|
|
} |
1064
|
1 |
|
|
1065
|
1 |
|
// Write the description. |
1066
|
|
|
$table->cell(Cli::val('description', $definition, '')); |
1067
|
1 |
|
} |
1068
|
1 |
|
|
1069
|
1 |
|
$table->write(); |
1070
|
|
|
echo PHP_EOL; |
1071
|
1 |
|
|
1072
|
1 |
|
$args = Cli::val(Cli::ARGS, $meta, []); |
1073
|
1 |
|
if (!empty($args)) { |
1074
|
1 |
|
echo Cli::bold('ARGUMENTS').PHP_EOL; |
1075
|
|
|
|
1076
|
|
|
$table = new Table(); |
1077
|
1 |
|
$table->setFormatOutput($this->formatOutput); |
1078
|
|
|
|
1079
|
1 |
|
foreach ($args as $argName => $arg) { |
1080
|
1 |
|
$table->row(); |
1081
|
|
|
|
1082
|
1 |
|
if (Cli::val('required', $arg)) { |
1083
|
|
|
$table->bold($argName); |
1084
|
|
|
} else { |
1085
|
|
|
$table->cell($argName); |
1086
|
|
|
} |
1087
|
|
|
|
1088
|
|
|
$table->cell(Cli::val('description', $arg, '')); |
1089
|
|
|
} |
1090
|
|
|
$table->write(); |
1091
|
|
|
echo PHP_EOL; |
1092
|
|
|
} |
1093
|
|
|
} |
1094
|
|
|
|
1095
|
|
|
/** |
1096
|
|
|
* Writes the basic usage information of the command. |
1097
|
|
|
* |
1098
|
|
|
* @param Args $args The parsed args returned from {@link Cli::parseRaw()}. |
1099
|
|
|
*/ |
1100
|
|
|
protected function writeUsage(Args $args) { |
1101
|
|
|
if ($filename = $args->getMeta('filename')) { |
1102
|
1 |
|
$schema = $this->getSchema($args->getCommand()); |
1103
|
1 |
|
unset($schema[Cli::META]); |
1104
|
|
|
|
1105
|
|
|
echo static::bold("usage: ").$filename; |
1106
|
|
|
|
1107
|
|
|
if ($this->hasCommand()) { |
1108
|
|
|
if ($args->getCommand() && isset($this->commandSchemas[$args->getCommand()])) { |
1109
|
|
|
echo ' '.$args->getCommand(); |
1110
|
|
|
|
1111
|
|
|
} else { |
1112
|
|
|
echo ' <command>'; |
1113
|
|
|
} |
1114
|
|
|
} |
1115
|
|
|
|
1116
|
|
|
if ($this->hasOptions($args->getCommand())) { |
1117
|
|
|
echo " [<options>]"; |
1118
|
1 |
|
} |
1119
|
|
|
|
1120
|
1 |
|
if ($hasArgs = $this->hasArgs($args->getCommand())) { |
1121
|
|
|
echo $hasArgs === 2 ? " <args>" : " [<args>]"; |
1122
|
1 |
|
} |
1123
|
|
|
|
1124
|
1 |
|
echo PHP_EOL.PHP_EOL; |
1125
|
|
|
} |
1126
|
1 |
|
} |
1127
|
|
|
|
1128
|
|
|
/** |
1129
|
|
|
* Parse a schema in short form into a full schema array. |
1130
|
|
|
* |
1131
|
|
|
* @param array $arr The array to parse into a schema. |
1132
|
|
|
* @return array The full schema array. |
1133
|
|
|
* @throws \InvalidArgumentException Throws an exception when an item in the schema is invalid. |
1134
|
|
|
*/ |
1135
|
|
|
public static function parseSchema(array $arr) { |
1136
|
|
|
$result = []; |
1137
|
1 |
|
|
1138
|
|
|
foreach ($arr as $key => $value) { |
1139
|
1 |
|
if (is_int($key)) { |
1140
|
1 |
|
if (is_string($value)) { |
1141
|
1 |
|
// This is a short param value. |
1142
|
1 |
|
$param = static::parseShortParam($value); |
1143
|
1 |
|
$name = $param['name']; |
1144
|
|
|
$result[$name] = $param; |
1145
|
|
|
} else { |
1146
|
|
|
throw new \InvalidArgumentException("Schema at position $key is not a valid param.", 500); |
1147
|
1 |
|
} |
1148
|
|
|
} else { |
1149
|
1 |
|
// The parameter is defined in the key. |
1150
|
1 |
|
$param = static::parseShortParam($key, $value); |
1151
|
|
|
$name = $param['name']; |
1152
|
|
|
|
1153
|
1 |
|
if (is_array($value)) { |
1154
|
|
|
// The value describes a bit more about the schema. |
1155
|
1 |
|
switch ($param['type']) { |
1156
|
1 |
|
case 'array': |
1157
|
1 |
|
if (isset($value['items'])) { |
1158
|
|
|
// The value includes array schema information. |
1159
|
1 |
|
$param = array_replace($param, $value); |
1160
|
1 |
|
} else { |
1161
|
1 |
|
// The value is a schema of items. |
1162
|
|
|
$param['items'] = $value; |
1163
|
|
|
} |
1164
|
|
|
break; |
1165
|
1 |
|
case 'object': |
1166
|
1 |
|
// The value is a schema of the object. |
1167
|
1 |
|
$param['properties'] = static::parseSchema($value); |
1168
|
|
|
break; |
1169
|
|
|
default: |
1170
|
1 |
|
$param = array_replace($param, $value); |
1171
|
|
|
break; |
1172
|
1 |
|
} |
1173
|
1 |
|
} elseif (is_string($value)) { |
1174
|
1 |
|
if ($param['type'] === 'array') { |
1175
|
|
|
// Check to see if the value is the item type in the array. |
1176
|
1 |
|
if (isset(self::$types[$value])) { |
1177
|
|
|
$arrType = self::$types[$value]; |
1178
|
|
|
} elseif (($index = array_search($value, self::$types)) !== false) { |
1179
|
|
|
$arrType = self::$types[$value]; |
1180
|
|
|
} |
1181
|
|
|
|
1182
|
|
|
if (isset($arrType)) { |
1183
|
|
|
$param['items'] = ['type' => $arrType]; |
1184
|
|
|
} else { |
1185
|
|
|
$param['description'] = $value; |
1186
|
|
|
} |
1187
|
|
|
} else { |
1188
|
|
|
// The value is the schema description. |
1189
|
33 |
|
$param['description'] = $value; |
1190
|
|
|
} |
1191
|
33 |
|
} |
1192
|
32 |
|
|
1193
|
|
|
$result[$name] = $param; |
1194
|
|
|
} |
1195
|
|
|
} |
1196
|
33 |
|
|
1197
|
31 |
|
return $result; |
1198
|
|
|
} |
1199
|
|
|
|
1200
|
9 |
|
/** |
1201
|
|
|
* Parse a short parameter string into a full array parameter. |
1202
|
|
|
* |
1203
|
|
|
* @param string $str The short parameter string to parse. |
1204
|
|
|
* @param array $other An array of other information that might help resolve ambiguity. |
1205
|
|
|
* @return array Returns an array in the form [name, [param]]. |
1206
|
|
|
* @throws \InvalidArgumentException Throws an exception if the short param is not in the correct format. |
1207
|
|
|
*/ |
1208
|
|
|
protected static function parseShortParam($str, $other = []) { |
1209
|
|
|
// Is the parameter optional? |
1210
|
|
|
if (substr($str, -1) === '?') { |
1211
|
|
|
$required = false; |
1212
|
|
|
$str = substr($str, 0, -1); |
1213
|
|
|
} else { |
1214
|
|
|
$required = true; |
1215
|
|
|
} |
1216
|
|
|
|
1217
|
|
|
// Check for a type. |
1218
|
|
|
$parts = explode(':', $str); |
1219
|
|
|
|
1220
|
|
|
if (count($parts) === 1) { |
1221
|
|
|
if (isset($other['type'])) { |
1222
|
|
|
$type = $other['type']; |
1223
|
|
|
} else { |
1224
|
|
|
$type = 'string'; |
1225
|
|
|
} |
1226
|
|
|
$name = $parts[0]; |
1227
|
|
|
} else { |
1228
|
|
|
$name = $parts[1]; |
1229
|
|
|
|
1230
|
|
|
if (isset(self::$types[$parts[0]])) { |
1231
|
|
|
$type = self::$types[$parts[0]]; |
1232
|
|
|
} else { |
1233
|
|
|
throw new \InvalidArgumentException("Invalid type {$parts[1]} for field $name.", 500); |
1234
|
|
|
} |
1235
|
|
|
|
1236
|
|
|
if (isset($parts[2])) { |
1237
|
|
|
$short = $parts[2]; |
1238
|
|
|
} |
1239
|
|
|
} |
1240
|
|
|
|
1241
|
|
|
$result = ['name' => $name, 'type' => $type, 'required' => $required]; |
1242
|
|
|
|
1243
|
|
|
if (isset($short)) { |
1244
|
|
|
$result['short'] = $short; |
1245
|
|
|
} |
1246
|
|
|
|
1247
|
|
|
return $result; |
1248
|
|
|
} |
1249
|
|
|
|
1250
|
|
|
/** |
1251
|
|
|
* Safely get a value out of an array. |
1252
|
|
|
* |
1253
|
|
|
* This function uses optimizations found in the [facebook libphputil library](https://github.com/facebook/libphutil). |
1254
|
|
|
* |
1255
|
|
|
* @param string|int $key The array key. |
1256
|
|
|
* @param array $array The array to get the value from. |
1257
|
|
|
* @param mixed $default The default value to return if the key doesn't exist. |
1258
|
|
|
* @return mixed The item from the array or `$default` if the array key doesn't exist. |
1259
|
|
|
*/ |
1260
|
|
|
public static function val($key, array $array, $default = null) { |
1261
|
|
|
// isset() is a micro-optimization - it is fast but fails for null values. |
1262
|
|
|
if (isset($array[$key])) { |
1263
|
|
|
return $array[$key]; |
1264
|
|
|
} |
1265
|
|
|
|
1266
|
|
|
// Comparing $default is also a micro-optimization. |
1267
|
|
|
if ($default === null || array_key_exists($key, $array)) { |
1268
|
|
|
return null; |
1269
|
|
|
} |
1270
|
|
|
|
1271
|
|
|
return $default; |
1272
|
|
|
} |
1273
|
|
|
} |
1274
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.