ekowabaka /
clearice
| 1 | <?php |
||
| 2 | |||
| 3 | namespace clearice\argparser; |
||
| 4 | |||
| 5 | |||
| 6 | /** |
||
| 7 | * For parsing arguments in ClearIce |
||
| 8 | * |
||
| 9 | * @package clearice\argparser |
||
| 10 | */ |
||
| 11 | class ArgumentParser |
||
| 12 | { |
||
| 13 | /** |
||
| 14 | * Description to put on top of the help message. |
||
| 15 | * @var string |
||
| 16 | */ |
||
| 17 | private $description; |
||
| 18 | |||
| 19 | /** |
||
| 20 | * A little message for the foot of the help message. |
||
| 21 | * @var string |
||
| 22 | */ |
||
| 23 | private $footer; |
||
| 24 | |||
| 25 | /** |
||
| 26 | * The name of the application. |
||
| 27 | * @var string |
||
| 28 | */ |
||
| 29 | private $name; |
||
| 30 | |||
| 31 | /** |
||
| 32 | * Commands that the application can execute. |
||
| 33 | * @var array |
||
| 34 | */ |
||
| 35 | private $commands = []; |
||
| 36 | |||
| 37 | /** |
||
| 38 | * A cache of all the options added. |
||
| 39 | * The array keys represents a concatenation of the command and either the short or long name of the option. Elements |
||
| 40 | * in this array will be the same as those in the options property. However, options that have both a short and long |
||
| 41 | * name would appear twice. |
||
| 42 | * @var array |
||
| 43 | */ |
||
| 44 | private $optionsCache = []; |
||
| 45 | |||
| 46 | /** |
||
| 47 | * All the possible options for arguments. |
||
| 48 | * @var array |
||
| 49 | */ |
||
| 50 | private $options = []; |
||
| 51 | |||
| 52 | /** |
||
| 53 | * An instance of the help generator. |
||
| 54 | * @var HelpMessageGenerator |
||
| 55 | */ |
||
| 56 | private $helpGenerator; |
||
| 57 | |||
| 58 | /** |
||
| 59 | * An instance of the validator. |
||
| 60 | * @var Validator |
||
| 61 | */ |
||
| 62 | private $validator; |
||
| 63 | |||
| 64 | /** |
||
| 65 | * A reference to a function to be called for exitting the entire application. |
||
| 66 | * @var callable |
||
| 67 | */ |
||
| 68 | private $exitFunction; |
||
| 69 | |||
| 70 | /** |
||
| 71 | * Flag raised when help has been enabled. |
||
| 72 | * @var bool |
||
| 73 | */ |
||
| 74 | private $helpEnabled = false; |
||
| 75 | |||
| 76 | /** |
||
| 77 | * ArgumentParser constructor. |
||
| 78 | * |
||
| 79 | * @param ValidatorInterface $validator |
||
| 80 | * @param HelpGeneratorInterface $helpWriter |
||
| 81 | */ |
||
| 82 | 16 | public function __construct(HelpGeneratorInterface $helpWriter = null, ValidatorInterface $validator = null) |
|
| 83 | { |
||
| 84 | 16 | $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator(); |
|
| 85 | 16 | $this->validator = $validator ?? new Validator(); |
|
| 86 | 16 | $this->exitFunction = function ($code) { exit($code); }; |
|
|
0 ignored issues
–
show
|
|||
| 87 | 16 | } |
|
| 88 | |||
| 89 | /** |
||
| 90 | * Add an option to the option cache for easy access through associative arrays. |
||
| 91 | * The option cache associates arguments with their options. |
||
| 92 | * |
||
| 93 | * @param string $identifier |
||
| 94 | * @param mixed $option |
||
| 95 | * @throws OptionExistsException |
||
| 96 | */ |
||
| 97 | 15 | private function addToOptionCache(string $identifier, $option) : void |
|
| 98 | { |
||
| 99 | 15 | if (!isset($option[$identifier])) { |
|
| 100 | 2 | return; |
|
| 101 | } |
||
| 102 | 15 | $cacheKey = "${option['command']}${option[$identifier]}"; |
|
| 103 | 15 | if (!isset($this->optionsCache[$cacheKey])) { |
|
| 104 | 15 | $this->optionsCache[$cacheKey] = $option; |
|
| 105 | } else { |
||
| 106 | 2 | throw new OptionExistsException( |
|
| 107 | 2 | "An argument option with $identifier {$option['command']} {$option[$identifier]} already exists." |
|
| 108 | ); |
||
| 109 | } |
||
| 110 | 15 | } |
|
| 111 | |||
| 112 | /** |
||
| 113 | * @param string $command |
||
| 114 | * @param string $name |
||
| 115 | * @return mixed |
||
| 116 | * @throws InvalidArgumentException |
||
| 117 | */ |
||
| 118 | 8 | private function retrieveOptionFromCache(string $command, string $name) |
|
| 119 | { |
||
| 120 | 8 | $key = $command . $name; |
|
| 121 | 8 | if(isset($this->optionsCache[$key])) { |
|
| 122 | 7 | return $this->optionsCache[$key]; |
|
| 123 | 1 | } else if(isset($this->optionsCache[$name]) && $this->optionsCache[$name]['command'] == "") { |
|
| 124 | 1 | return $this->optionsCache[$name]; |
|
| 125 | } else{ |
||
| 126 | throw new InvalidArgumentException("Unknown option '$name'. Please run with `--help` for more information on valid options."); |
||
| 127 | } |
||
| 128 | } |
||
| 129 | |||
| 130 | /** |
||
| 131 | * Add an option to be parsed. |
||
| 132 | * Arguments are presented as a structured array with the following possible keys. |
||
| 133 | * |
||
| 134 | * name: The name of the option prefixed with a double dash -- |
||
| 135 | * short_name: A shorter single character option prefixed with a single dash - |
||
| 136 | * type: Required for all options that take values. An option specified without a type is considered to be a |
||
| 137 | * boolean flag. |
||
| 138 | * repeats: A boolean value that states whether the option can be repeated or not. Repeatable options are returned |
||
| 139 | * as arrays. |
||
| 140 | * default: A default value for the option. |
||
| 141 | * help: A help message for the option |
||
| 142 | * |
||
| 143 | * @param array $option |
||
| 144 | * @throws OptionExistsException |
||
| 145 | * @throws InvalidArgumentDescriptionException |
||
| 146 | * @throws UnknownCommandException |
||
| 147 | */ |
||
| 148 | 16 | public function addOption(array $option): void |
|
| 149 | { |
||
| 150 | 16 | $this->validator->validateOption($option, $this->commands); |
|
| 151 | 15 | $option['repeats'] = $option['repeats'] ?? false; |
|
| 152 | // Save a copy of the original command definition, so it can be spread out if it's an array. |
||
| 153 | 15 | $commands = $option['command'] ?? ''; |
|
| 154 | 15 | foreach(is_array($commands) ? $commands : [$commands] as $command) { |
|
| 155 | 15 | $option['command'] = $command; |
|
| 156 | 15 | $this->options[] = $option; |
|
| 157 | 15 | $this->addToOptionCache('name', $option); |
|
| 158 | 15 | $this->addToOptionCache('short_name', $option); |
|
| 159 | } |
||
| 160 | 15 | } |
|
| 161 | |||
| 162 | /** |
||
| 163 | * @param $arguments |
||
| 164 | * @param $argPointer |
||
| 165 | * @return mixed |
||
| 166 | * @throws InvalidValueException |
||
| 167 | */ |
||
| 168 | 5 | private function getNextValueOrFail($arguments, &$argPointer, $name) |
|
| 169 | { |
||
| 170 | 5 | if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') { |
|
| 171 | 3 | $argPointer++; |
|
| 172 | 3 | return $arguments[$argPointer]; |
|
| 173 | } else { |
||
| 174 | 2 | throw new InvalidValueException("A value must be passed along with argument $name."); |
|
| 175 | } |
||
| 176 | } |
||
| 177 | |||
| 178 | /** |
||
| 179 | * Parse a long argument that is prefixed with a double dash "--" |
||
| 180 | * |
||
| 181 | * @param $arguments |
||
| 182 | * @param $argPointer |
||
| 183 | * @throws InvalidValueException |
||
| 184 | * @throws InvalidArgumentException |
||
| 185 | */ |
||
| 186 | 6 | private function parseLongArgument($command, $arguments, &$argPointer, &$output) |
|
| 187 | { |
||
| 188 | 6 | $string = substr($arguments[$argPointer], 2); |
|
| 189 | 6 | preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches); |
|
| 190 | 6 | $option = $this->retrieveOptionFromCache($command, $matches['name']); |
|
| 191 | 6 | $value = true; |
|
| 192 | |||
| 193 | 6 | if (isset($option['type'])) { |
|
| 194 | 4 | if ($matches['equal'] === '=') { |
|
| 195 | 1 | $value = $matches['value']; |
|
| 196 | } else { |
||
| 197 | 4 | $value = $this->getNextValueOrFail($arguments, $argPointer, $matches['name']); |
|
| 198 | } |
||
| 199 | } |
||
| 200 | |||
| 201 | 5 | $this->assignValue($option, $output, $option['name'], $value); |
|
| 202 | 5 | } |
|
| 203 | |||
| 204 | /** |
||
| 205 | * Parse a short argument that is prefixed with a single dash '-' |
||
| 206 | * |
||
| 207 | * @param $command |
||
| 208 | * @param $arguments |
||
| 209 | * @param $argPointer |
||
| 210 | * @throws InvalidValueException |
||
| 211 | * @throws InvalidArgumentException |
||
| 212 | */ |
||
| 213 | 3 | private function parseShortArgument($command, $arguments, &$argPointer, &$output) |
|
| 214 | { |
||
| 215 | 3 | $argument = $arguments[$argPointer]; |
|
| 216 | 3 | $option = $this->retrieveOptionFromCache($command, substr($argument, 1, 1)); |
|
| 217 | 3 | $value = true; |
|
| 218 | |||
| 219 | 3 | if (isset($option['type'])) { |
|
| 220 | 3 | if (substr($argument, 2) != "") { |
|
| 221 | 2 | $value = substr($argument, 2); |
|
| 222 | } else { |
||
| 223 | 2 | $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']); |
|
| 224 | } |
||
| 225 | } |
||
| 226 | |||
| 227 | 2 | $this->assignValue($option, $output, $option['name'], $value); |
|
| 228 | 2 | } |
|
| 229 | |||
| 230 | 6 | private function assignValue($option, &$output, $key, $value) |
|
| 231 | { |
||
| 232 | 6 | if($option['repeats']) { |
|
| 233 | 1 | $output[$key] = isset($output[$key]) ? array_merge($output[$key], [$value]) : [$value]; |
|
| 234 | } else { |
||
| 235 | 5 | $output[$key] = $value; |
|
| 236 | } |
||
| 237 | 6 | } |
|
| 238 | |||
| 239 | /** |
||
| 240 | * @param $arguments |
||
| 241 | * @param $argPointer |
||
| 242 | * @param $output |
||
| 243 | * @throws InvalidValueException |
||
| 244 | * @throws InvalidArgumentException |
||
| 245 | */ |
||
| 246 | 8 | private function parseArgumentArray($arguments, &$argPointer, &$output) |
|
| 247 | { |
||
| 248 | 8 | $numArguments = count($arguments); |
|
| 249 | 8 | $command = $output['__command'] ?? ''; |
|
| 250 | 8 | for (; $argPointer < $numArguments; $argPointer++) { |
|
| 251 | 8 | $arg = $arguments[$argPointer]; |
|
| 252 | 8 | if (substr($arg, 0, 2) == "--") { |
|
| 253 | 6 | $this->parseLongArgument($command, $arguments, $argPointer, $output); |
|
| 254 | 3 | } else if ($arg[0] == '-') { |
|
| 255 | 3 | $this->parseShortArgument($command, $arguments, $argPointer, $output); |
|
| 256 | } else { |
||
| 257 | 1 | $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg]; |
|
| 258 | } |
||
| 259 | } |
||
| 260 | 6 | } |
|
| 261 | |||
| 262 | /** |
||
| 263 | * @param $output |
||
| 264 | * @throws HelpMessageRequestedException |
||
| 265 | */ |
||
| 266 | 6 | private function maybeShowHelp($output) |
|
| 267 | { |
||
| 268 | 6 | if ((isset($output['help']) && $output['help'] && $this->helpEnabled)) { |
|
| 269 | 1 | print $this->getHelpMessage($output['__command'] ?? null); |
|
| 270 | 1 | throw new HelpMessageRequestedException(); |
|
| 271 | } |
||
| 272 | |||
| 273 | 5 | if(isset($output['__command']) && $output['__command'] == 'help' && $this->helpEnabled) { |
|
| 274 | print $this->getHelpMessage($output['__args'][0] ?? null); |
||
| 275 | } |
||
| 276 | 5 | } |
|
| 277 | |||
| 278 | 8 | private function parseCommand($arguments, &$argPointer, &$output) |
|
| 279 | { |
||
| 280 | 8 | if (count($arguments) > 1 && isset($this->commands[$arguments[$argPointer]])) { |
|
| 281 | 2 | $output["__command"] = $arguments[$argPointer]; |
|
| 282 | 2 | $argPointer++; |
|
| 283 | } |
||
| 284 | 8 | } |
|
| 285 | |||
| 286 | /** |
||
| 287 | * @param $parsed |
||
| 288 | */ |
||
| 289 | 5 | private function fillInDefaults(&$parsed) |
|
| 290 | { |
||
| 291 | 5 | foreach($this->options as $option) { |
|
| 292 | 5 | if(!isset($parsed[$option['name']]) && isset($option['default']) && ($option['command'] == ($parsed['__command'] ?? "") || $option['command'] == '')) { |
|
| 293 | 1 | $parsed[$option['name']] = $option['default']; |
|
| 294 | } |
||
| 295 | } |
||
| 296 | 5 | } |
|
| 297 | |||
| 298 | /** |
||
| 299 | * A function called to exit the application whenever there's a parsing error or after requested help has been |
||
| 300 | * displayed/ |
||
| 301 | * |
||
| 302 | * @param callable $exit |
||
| 303 | */ |
||
| 304 | 1 | public function setExitCallback(callable $exit) |
|
| 305 | { |
||
| 306 | 1 | $this->exitFunction = $exit; |
|
| 307 | 1 | } |
|
| 308 | |||
| 309 | /** |
||
| 310 | * Parses command line arguments and return a structured array of options and their associated values. |
||
| 311 | * |
||
| 312 | * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI. |
||
| 313 | * @return array |
||
| 314 | * @throws InvalidValueException |
||
| 315 | */ |
||
| 316 | 8 | public function parse($arguments = null) |
|
| 317 | { |
||
| 318 | try{ |
||
| 319 | 8 | global $argv; |
|
| 320 | 8 | $arguments = $arguments ?? $argv; |
|
| 321 | 8 | $argPointer = 1; |
|
| 322 | 8 | $parsed = []; |
|
| 323 | 8 | $this->name = $this->name ?? $arguments[0]; |
|
| 324 | 8 | $this->parseCommand($arguments, $argPointer, $parsed); |
|
| 325 | 8 | $this->parseArgumentArray($arguments, $argPointer, $parsed); |
|
| 326 | 6 | $this->maybeShowHelp($parsed); |
|
| 327 | 5 | $this->validator->validateArguments($this->options, $parsed); |
|
| 328 | 5 | $this->fillInDefaults($parsed); |
|
| 329 | 5 | $parsed['__executed'] = $this->name; |
|
| 330 | 5 | return $parsed; |
|
| 331 | 3 | } catch (HelpMessageRequestedException $exception) { |
|
| 332 | 1 | ($this->exitFunction)(0); |
|
| 333 | 2 | } catch (InvalidArgumentException $exception) { |
|
| 334 | print $exception->getMessage() . PHP_EOL; |
||
| 335 | ($this->exitFunction)(1024); |
||
| 336 | } |
||
| 337 | 1 | } |
|
| 338 | |||
| 339 | /** |
||
| 340 | * Enables help messages so they are shown automatically when the appropriate argument (`--help` or `help`) is passed. |
||
| 341 | * This method also allows you to optionally pass the name of the application, a description header for the help |
||
| 342 | * message and a footer. |
||
| 343 | * |
||
| 344 | * @param string $name The name of the application binary |
||
| 345 | * @param string $description A description to be displayed on top of the help message |
||
| 346 | * @param string $footer A footer message to be displayed after the help message |
||
| 347 | * |
||
| 348 | * @throws InvalidArgumentDescriptionException |
||
| 349 | * @throws OptionExistsException |
||
| 350 | * @throws UnknownCommandException |
||
| 351 | */ |
||
| 352 | 2 | public function enableHelp(string $description = null, string $footer = null, string $name = null) : void |
|
| 353 | { |
||
| 354 | 2 | global $argv; |
|
| 355 | 2 | $this->name = $name ?? $argv[0]; |
|
| 356 | 2 | $this->description = $description; |
|
| 357 | 2 | $this->footer = $footer; |
|
| 358 | 2 | $this->helpEnabled = true; |
|
| 359 | 2 | $this->addOption([ |
|
| 360 | 2 | 'name' => 'help', |
|
| 361 | 'short_name' => 'h', 'help' => "display this help message" |
||
| 362 | ]); |
||
| 363 | 2 | if($this->commands) { |
|
|
0 ignored issues
–
show
The expression
$this->commands of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using Loading history...
|
|||
| 364 | 1 | $this->addCommand(['name' => 'help', 'help' => "display help for any command. Usage: {$this->name} help [command]"]); |
|
| 365 | 1 | foreach($this->commands as $command) { |
|
| 366 | 1 | $this->addOption([ |
|
| 367 | 1 | 'name' => 'help', |
|
| 368 | 1 | 'help' => 'display this help message', |
|
| 369 | 1 | 'command' => $command['name'] |
|
| 370 | ]); |
||
| 371 | } |
||
| 372 | } |
||
| 373 | 2 | } |
|
| 374 | |||
| 375 | 2 | public function getHelpMessage($command = '') |
|
| 376 | { |
||
| 377 | 2 | return $this->helpGenerator->generate( |
|
| 378 | 2 | $this->name, $command ?? null, |
|
| 379 | 2 | ['options' => $this->options, 'commands' => $this->commands], |
|
| 380 | 2 | $this->description, $this->footer |
|
| 381 | ); |
||
| 382 | } |
||
| 383 | |||
| 384 | /** |
||
| 385 | * @param $command |
||
| 386 | * @throws CommandExistsException |
||
| 387 | * @throws InvalidArgumentDescriptionException |
||
| 388 | */ |
||
| 389 | 7 | public function addCommand($command) |
|
| 390 | { |
||
| 391 | 7 | $this->validator->validateCommand($command, $this->commands); |
|
| 392 | 7 | $this->commands[$command['name']] = $command; |
|
| 393 | 7 | } |
|
| 394 | } |
||
| 395 |
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.