[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
1 <?php 2 3 namespace splitbrain\phpcli; 4 5 /** 6 * Class Options 7 * 8 * Parses command line options passed to the CLI script. Allows CLI scripts to easily register all accepted options and 9 * commands and even generates a help text from this setup. 10 * 11 * @author Andreas Gohr <andi@splitbrain.org> 12 * @license MIT 13 */ 14 class Options 15 { 16 /** @var array keeps the list of options to parse */ 17 protected $setup; 18 19 /** @var array store parsed options */ 20 protected $options = array(); 21 22 /** @var string current parsed command if any */ 23 protected $command = ''; 24 25 /** @var array passed non-option arguments */ 26 protected $args = array(); 27 28 /** @var string the executed script */ 29 protected $bin; 30 31 /** @var Colors for colored help output */ 32 protected $colors; 33 34 /** 35 * Constructor 36 * 37 * @param Colors $colors optional configured color object 38 * @throws Exception when arguments can't be read 39 */ 40 public function __construct(Colors $colors = null) 41 { 42 if (!is_null($colors)) { 43 $this->colors = $colors; 44 } else { 45 $this->colors = new Colors(); 46 } 47 48 $this->setup = array( 49 '' => array( 50 'opts' => array(), 51 'args' => array(), 52 'help' => '' 53 ) 54 ); // default command 55 56 $this->args = $this->readPHPArgv(); 57 $this->bin = basename(array_shift($this->args)); 58 59 $this->options = array(); 60 } 61 62 /** 63 * Gets the bin value 64 */ 65 public function getBin() 66 { 67 return $this->bin; 68 } 69 70 /** 71 * Sets the help text for the tool itself 72 * 73 * @param string $help 74 */ 75 public function setHelp($help) 76 { 77 $this->setup['']['help'] = $help; 78 } 79 80 /** 81 * Register the names of arguments for help generation and number checking 82 * 83 * This has to be called in the order arguments are expected 84 * 85 * @param string $arg argument name (just for help) 86 * @param string $help help text 87 * @param bool $required is this a required argument 88 * @param string $command if theses apply to a sub command only 89 * @throws Exception 90 */ 91 public function registerArgument($arg, $help, $required = true, $command = '') 92 { 93 if (!isset($this->setup[$command])) { 94 throw new Exception("Command $command not registered"); 95 } 96 97 $this->setup[$command]['args'][] = array( 98 'name' => $arg, 99 'help' => $help, 100 'required' => $required 101 ); 102 } 103 104 /** 105 * This registers a sub command 106 * 107 * Sub commands have their own options and use their own function (not main()). 108 * 109 * @param string $command 110 * @param string $help 111 * @throws Exception 112 */ 113 public function registerCommand($command, $help) 114 { 115 if (isset($this->setup[$command])) { 116 throw new Exception("Command $command already registered"); 117 } 118 119 $this->setup[$command] = array( 120 'opts' => array(), 121 'args' => array(), 122 'help' => $help 123 ); 124 125 } 126 127 /** 128 * Register an option for option parsing and help generation 129 * 130 * @param string $long multi character option (specified with --) 131 * @param string $help help text for this option 132 * @param string|null $short one character option (specified with -) 133 * @param bool|string $needsarg does this option require an argument? give it a name here 134 * @param string $command what command does this option apply to 135 * @throws Exception 136 */ 137 public function registerOption($long, $help, $short = null, $needsarg = false, $command = '') 138 { 139 if (!isset($this->setup[$command])) { 140 throw new Exception("Command $command not registered"); 141 } 142 143 $this->setup[$command]['opts'][$long] = array( 144 'needsarg' => $needsarg, 145 'help' => $help, 146 'short' => $short 147 ); 148 149 if ($short) { 150 if (strlen($short) > 1) { 151 throw new Exception("Short options should be exactly one ASCII character"); 152 } 153 154 $this->setup[$command]['short'][$short] = $long; 155 } 156 } 157 158 /** 159 * Checks the actual number of arguments against the required number 160 * 161 * Throws an exception if arguments are missing. 162 * 163 * This is run from CLI automatically and usually does not need to be called directly 164 * 165 * @throws Exception 166 */ 167 public function checkArguments() 168 { 169 $argc = count($this->args); 170 171 $req = 0; 172 foreach ($this->setup[$this->command]['args'] as $arg) { 173 if (!$arg['required']) { 174 break; 175 } // last required arguments seen 176 $req++; 177 } 178 179 if ($req > $argc) { 180 throw new Exception("Not enough arguments", Exception::E_OPT_ARG_REQUIRED); 181 } 182 } 183 184 /** 185 * Parses the given arguments for known options and command 186 * 187 * The given $args array should NOT contain the executed file as first item anymore! The $args 188 * array is stripped from any options and possible command. All found otions can be accessed via the 189 * getOpt() function 190 * 191 * Note that command options will overwrite any global options with the same name 192 * 193 * This is run from CLI automatically and usually does not need to be called directly 194 * 195 * @throws Exception 196 */ 197 public function parseOptions() 198 { 199 $non_opts = array(); 200 201 $argc = count($this->args); 202 for ($i = 0; $i < $argc; $i++) { 203 $arg = $this->args[$i]; 204 205 // The special element '--' means explicit end of options. Treat the rest of the arguments as non-options 206 // and end the loop. 207 if ($arg == '--') { 208 $non_opts = array_merge($non_opts, array_slice($this->args, $i + 1)); 209 break; 210 } 211 212 // '-' is stdin - a normal argument 213 if ($arg == '-') { 214 $non_opts = array_merge($non_opts, array_slice($this->args, $i)); 215 break; 216 } 217 218 // first non-option 219 if ($arg[0] != '-') { 220 $non_opts = array_merge($non_opts, array_slice($this->args, $i)); 221 break; 222 } 223 224 // long option 225 if (strlen($arg) > 1 && $arg[1] === '-') { 226 $arg = explode('=', substr($arg, 2), 2); 227 $opt = array_shift($arg); 228 $val = array_shift($arg); 229 230 if (!isset($this->setup[$this->command]['opts'][$opt])) { 231 throw new Exception("No such option '$opt'", Exception::E_UNKNOWN_OPT); 232 } 233 234 // argument required? 235 if ($this->setup[$this->command]['opts'][$opt]['needsarg']) { 236 if (is_null($val) && $i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { 237 $val = $this->args[++$i]; 238 } 239 if (is_null($val)) { 240 throw new Exception("Option $opt requires an argument", 241 Exception::E_OPT_ARG_REQUIRED); 242 } 243 $this->options[$opt] = $val; 244 } else { 245 $this->options[$opt] = true; 246 } 247 248 continue; 249 } 250 251 // short option 252 $opt = substr($arg, 1); 253 if (!isset($this->setup[$this->command]['short'][$opt])) { 254 throw new Exception("No such option $arg", Exception::E_UNKNOWN_OPT); 255 } else { 256 $opt = $this->setup[$this->command]['short'][$opt]; // store it under long name 257 } 258 259 // argument required? 260 if ($this->setup[$this->command]['opts'][$opt]['needsarg']) { 261 $val = null; 262 if ($i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { 263 $val = $this->args[++$i]; 264 } 265 if (is_null($val)) { 266 throw new Exception("Option $arg requires an argument", 267 Exception::E_OPT_ARG_REQUIRED); 268 } 269 $this->options[$opt] = $val; 270 } else { 271 $this->options[$opt] = true; 272 } 273 } 274 275 // parsing is now done, update args array 276 $this->args = $non_opts; 277 278 // if not done yet, check if first argument is a command and reexecute argument parsing if it is 279 if (!$this->command && $this->args && isset($this->setup[$this->args[0]])) { 280 // it is a command! 281 $this->command = array_shift($this->args); 282 $this->parseOptions(); // second pass 283 } 284 } 285 286 /** 287 * Get the value of the given option 288 * 289 * Please note that all options are accessed by their long option names regardless of how they were 290 * specified on commandline. 291 * 292 * Can only be used after parseOptions() has been run 293 * 294 * @param mixed $option 295 * @param bool|string $default what to return if the option was not set 296 * @return bool|string|string[] 297 */ 298 public function getOpt($option = null, $default = false) 299 { 300 if ($option === null) { 301 return $this->options; 302 } 303 304 if (isset($this->options[$option])) { 305 return $this->options[$option]; 306 } 307 return $default; 308 } 309 310 /** 311 * Return the found command if any 312 * 313 * @return string 314 */ 315 public function getCmd() 316 { 317 return $this->command; 318 } 319 320 /** 321 * Get all the arguments passed to the script 322 * 323 * This will not contain any recognized options or the script name itself 324 * 325 * @return array 326 */ 327 public function getArgs() 328 { 329 return $this->args; 330 } 331 332 /** 333 * Builds a help screen from the available options. You may want to call it from -h or on error 334 * 335 * @return string 336 * 337 * @throws Exception 338 */ 339 public function help() 340 { 341 $tf = new TableFormatter($this->colors); 342 $text = ''; 343 344 $hascommands = (count($this->setup) > 1); 345 foreach ($this->setup as $command => $config) { 346 $hasopts = (bool)$this->setup[$command]['opts']; 347 $hasargs = (bool)$this->setup[$command]['args']; 348 349 // usage or command syntax line 350 if (!$command) { 351 $text .= $this->colors->wrap('USAGE:', Colors::C_BROWN); 352 $text .= "\n"; 353 $text .= ' ' . $this->bin; 354 $mv = 2; 355 } else { 356 $text .= "\n"; 357 $text .= $this->colors->wrap(' ' . $command, Colors::C_PURPLE); 358 $mv = 4; 359 } 360 361 if ($hasopts) { 362 $text .= ' ' . $this->colors->wrap('<OPTIONS>', Colors::C_GREEN); 363 } 364 365 if (!$command && $hascommands) { 366 $text .= ' ' . $this->colors->wrap('<COMMAND> ...', Colors::C_PURPLE); 367 } 368 369 foreach ($this->setup[$command]['args'] as $arg) { 370 $out = $this->colors->wrap('<' . $arg['name'] . '>', Colors::C_CYAN); 371 372 if (!$arg['required']) { 373 $out = '[' . $out . ']'; 374 } 375 $text .= ' ' . $out; 376 } 377 $text .= "\n"; 378 379 // usage or command intro 380 if ($this->setup[$command]['help']) { 381 $text .= "\n"; 382 $text .= $tf->format( 383 array($mv, '*'), 384 array('', $this->setup[$command]['help'] . "\n") 385 ); 386 } 387 388 // option description 389 if ($hasopts) { 390 if (!$command) { 391 $text .= "\n"; 392 $text .= $this->colors->wrap('OPTIONS:', Colors::C_BROWN); 393 } 394 $text .= "\n"; 395 foreach ($this->setup[$command]['opts'] as $long => $opt) { 396 397 $name = ''; 398 if ($opt['short']) { 399 $name .= '-' . $opt['short']; 400 if ($opt['needsarg']) { 401 $name .= ' <' . $opt['needsarg'] . '>'; 402 } 403 $name .= ', '; 404 } 405 $name .= "--$long"; 406 if ($opt['needsarg']) { 407 $name .= ' <' . $opt['needsarg'] . '>'; 408 } 409 410 $text .= $tf->format( 411 array($mv, '30%', '*'), 412 array('', $name, $opt['help']), 413 array('', 'green', '') 414 ); 415 $text .= "\n"; 416 } 417 } 418 419 // argument description 420 if ($hasargs) { 421 if (!$command) { 422 $text .= "\n"; 423 $text .= $this->colors->wrap('ARGUMENTS:', Colors::C_BROWN); 424 } 425 $text .= "\n"; 426 foreach ($this->setup[$command]['args'] as $arg) { 427 $name = '<' . $arg['name'] . '>'; 428 429 $text .= $tf->format( 430 array($mv, '30%', '*'), 431 array('', $name, $arg['help']), 432 array('', 'cyan', '') 433 ); 434 } 435 } 436 437 // head line and intro for following command documentation 438 if (!$command && $hascommands) { 439 $text .= "\n"; 440 $text .= $this->colors->wrap('COMMANDS:', Colors::C_BROWN); 441 $text .= "\n"; 442 $text .= $tf->format( 443 array($mv, '*'), 444 array('', 'This tool accepts a command as first parameter as outlined below:') 445 ); 446 $text .= "\n"; 447 } 448 } 449 450 return $text; 451 } 452 453 /** 454 * Safely read the $argv PHP array across different PHP configurations. 455 * Will take care on register_globals and register_argc_argv ini directives 456 * 457 * @throws Exception 458 * @return array the $argv PHP array or PEAR error if not registered 459 */ 460 private function readPHPArgv() 461 { 462 global $argv; 463 if (!is_array($argv)) { 464 if (!@is_array($_SERVER['argv'])) { 465 if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { 466 throw new Exception( 467 "Could not read cmd args (register_argc_argv=Off?)", 468 Exception::E_ARG_READ 469 ); 470 } 471 return $GLOBALS['HTTP_SERVER_VARS']['argv']; 472 } 473 return $_SERVER['argv']; 474 } 475 return $argv; 476 } 477 } 478
title
Description
Body
title
Description
Body
title
Description
Body
title
Body