[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Class DokuCLI 5 * 6 * All DokuWiki commandline scripts should inherit from this class and implement the abstract methods. 7 * 8 * @deprecated 2017-11-10 9 * @author Andreas Gohr <andi@splitbrain.org> 10 */ 11 abstract class DokuCLI { 12 /** @var string the executed script itself */ 13 protected $bin; 14 /** @var DokuCLI_Options the option parser */ 15 protected $options; 16 /** @var DokuCLI_Colors */ 17 public $colors; 18 19 /** 20 * constructor 21 * 22 * Initialize the arguments, set up helper classes and set up the CLI environment 23 */ 24 public function __construct() { 25 set_exception_handler(array($this, 'fatal')); 26 27 $this->options = new DokuCLI_Options(); 28 $this->colors = new DokuCLI_Colors(); 29 30 dbg_deprecated('use \splitbrain\phpcli\CLI instead'); 31 $this->error('DokuCLI is deprecated, use \splitbrain\phpcli\CLI instead.'); 32 } 33 34 /** 35 * Register options and arguments on the given $options object 36 * 37 * @param DokuCLI_Options $options 38 * @return void 39 */ 40 abstract protected function setup(DokuCLI_Options $options); 41 42 /** 43 * Your main program 44 * 45 * Arguments and options have been parsed when this is run 46 * 47 * @param DokuCLI_Options $options 48 * @return void 49 */ 50 abstract protected function main(DokuCLI_Options $options); 51 52 /** 53 * Execute the CLI program 54 * 55 * Executes the setup() routine, adds default options, initiate the options parsing and argument checking 56 * and finally executes main() 57 */ 58 public function run() { 59 if('cli' != php_sapi_name()) throw new DokuCLI_Exception('This has to be run from the command line'); 60 61 // setup 62 $this->setup($this->options); 63 $this->options->registerOption( 64 'no-colors', 65 'Do not use any colors in output. Useful when piping output to other tools or files.' 66 ); 67 $this->options->registerOption( 68 'help', 69 'Display this help screen and exit immediately.', 70 'h' 71 ); 72 73 // parse 74 $this->options->parseOptions(); 75 76 // handle defaults 77 if($this->options->getOpt('no-colors')) { 78 $this->colors->disable(); 79 } 80 if($this->options->getOpt('help')) { 81 echo $this->options->help(); 82 exit(0); 83 } 84 85 // check arguments 86 $this->options->checkArguments(); 87 88 // execute 89 $this->main($this->options); 90 91 exit(0); 92 } 93 94 /** 95 * Exits the program on a fatal error 96 * 97 * @param Exception|string $error either an exception or an error message 98 */ 99 public function fatal($error) { 100 $code = 0; 101 if(is_object($error) && is_a($error, 'Exception')) { 102 /** @var Exception $error */ 103 $code = $error->getCode(); 104 $error = $error->getMessage(); 105 } 106 if(!$code) $code = DokuCLI_Exception::E_ANY; 107 108 $this->error($error); 109 exit($code); 110 } 111 112 /** 113 * Print an error message 114 * 115 * @param string $string 116 */ 117 public function error($string) { 118 $this->colors->ptln("E: $string", 'red', STDERR); 119 } 120 121 /** 122 * Print a success message 123 * 124 * @param string $string 125 */ 126 public function success($string) { 127 $this->colors->ptln("S: $string", 'green', STDERR); 128 } 129 130 /** 131 * Print an info message 132 * 133 * @param string $string 134 */ 135 public function info($string) { 136 $this->colors->ptln("I: $string", 'cyan', STDERR); 137 } 138 139 } 140 141 /** 142 * Class DokuCLI_Colors 143 * 144 * Handles color output on (Linux) terminals 145 * 146 * @author Andreas Gohr <andi@splitbrain.org> 147 */ 148 class DokuCLI_Colors { 149 /** @var array known color names */ 150 protected $colors = array( 151 'reset' => "\33[0m", 152 'black' => "\33[0;30m", 153 'darkgray' => "\33[1;30m", 154 'blue' => "\33[0;34m", 155 'lightblue' => "\33[1;34m", 156 'green' => "\33[0;32m", 157 'lightgreen' => "\33[1;32m", 158 'cyan' => "\33[0;36m", 159 'lightcyan' => "\33[1;36m", 160 'red' => "\33[0;31m", 161 'lightred' => "\33[1;31m", 162 'purple' => "\33[0;35m", 163 'lightpurple' => "\33[1;35m", 164 'brown' => "\33[0;33m", 165 'yellow' => "\33[1;33m", 166 'lightgray' => "\33[0;37m", 167 'white' => "\33[1;37m", 168 ); 169 170 /** @var bool should colors be used? */ 171 protected $enabled = true; 172 173 /** 174 * Constructor 175 * 176 * Tries to disable colors for non-terminals 177 */ 178 public function __construct() { 179 if(function_exists('posix_isatty') && !posix_isatty(STDOUT)) { 180 $this->enabled = false; 181 return; 182 } 183 if(!getenv('TERM')) { 184 $this->enabled = false; 185 return; 186 } 187 } 188 189 /** 190 * enable color output 191 */ 192 public function enable() { 193 $this->enabled = true; 194 } 195 196 /** 197 * disable color output 198 */ 199 public function disable() { 200 $this->enabled = false; 201 } 202 203 /** 204 * Convenience function to print a line in a given color 205 * 206 * @param string $line 207 * @param string $color 208 * @param resource $channel 209 */ 210 public function ptln($line, $color, $channel = STDOUT) { 211 $this->set($color); 212 fwrite($channel, rtrim($line)."\n"); 213 $this->reset(); 214 } 215 216 /** 217 * Set the given color for consecutive output 218 * 219 * @param string $color one of the supported color names 220 * @throws DokuCLI_Exception 221 */ 222 public function set($color) { 223 if(!$this->enabled) return; 224 if(!isset($this->colors[$color])) throw new DokuCLI_Exception("No such color $color"); 225 echo $this->colors[$color]; 226 } 227 228 /** 229 * reset the terminal color 230 */ 231 public function reset() { 232 $this->set('reset'); 233 } 234 } 235 236 /** 237 * Class DokuCLI_Options 238 * 239 * Parses command line options passed to the CLI script. Allows CLI scripts to easily register all accepted options and 240 * commands and even generates a help text from this setup. 241 * 242 * @author Andreas Gohr <andi@splitbrain.org> 243 */ 244 class DokuCLI_Options { 245 /** @var array keeps the list of options to parse */ 246 protected $setup; 247 248 /** @var array store parsed options */ 249 protected $options = array(); 250 251 /** @var string current parsed command if any */ 252 protected $command = ''; 253 254 /** @var array passed non-option arguments */ 255 public $args = array(); 256 257 /** @var string the executed script */ 258 protected $bin; 259 260 /** 261 * Constructor 262 */ 263 public function __construct() { 264 $this->setup = array( 265 '' => array( 266 'opts' => array(), 267 'args' => array(), 268 'help' => '' 269 ) 270 ); // default command 271 272 $this->args = $this->readPHPArgv(); 273 $this->bin = basename(array_shift($this->args)); 274 275 $this->options = array(); 276 } 277 278 /** 279 * Sets the help text for the tool itself 280 * 281 * @param string $help 282 */ 283 public function setHelp($help) { 284 $this->setup['']['help'] = $help; 285 } 286 287 /** 288 * Register the names of arguments for help generation and number checking 289 * 290 * This has to be called in the order arguments are expected 291 * 292 * @param string $arg argument name (just for help) 293 * @param string $help help text 294 * @param bool $required is this a required argument 295 * @param string $command if theses apply to a sub command only 296 * @throws DokuCLI_Exception 297 */ 298 public function registerArgument($arg, $help, $required = true, $command = '') { 299 if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered"); 300 301 $this->setup[$command]['args'][] = array( 302 'name' => $arg, 303 'help' => $help, 304 'required' => $required 305 ); 306 } 307 308 /** 309 * This registers a sub command 310 * 311 * Sub commands have their own options and use their own function (not main()). 312 * 313 * @param string $command 314 * @param string $help 315 * @throws DokuCLI_Exception 316 */ 317 public function registerCommand($command, $help) { 318 if(isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command already registered"); 319 320 $this->setup[$command] = array( 321 'opts' => array(), 322 'args' => array(), 323 'help' => $help 324 ); 325 326 } 327 328 /** 329 * Register an option for option parsing and help generation 330 * 331 * @param string $long multi character option (specified with --) 332 * @param string $help help text for this option 333 * @param string|null $short one character option (specified with -) 334 * @param bool|string $needsarg does this option require an argument? give it a name here 335 * @param string $command what command does this option apply to 336 * @throws DokuCLI_Exception 337 */ 338 public function registerOption($long, $help, $short = null, $needsarg = false, $command = '') { 339 if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered"); 340 341 $this->setup[$command]['opts'][$long] = array( 342 'needsarg' => $needsarg, 343 'help' => $help, 344 'short' => $short 345 ); 346 347 if($short) { 348 if(strlen($short) > 1) throw new DokuCLI_Exception("Short options should be exactly one ASCII character"); 349 350 $this->setup[$command]['short'][$short] = $long; 351 } 352 } 353 354 /** 355 * Checks the actual number of arguments against the required number 356 * 357 * Throws an exception if arguments are missing. Called from parseOptions() 358 * 359 * @throws DokuCLI_Exception 360 */ 361 public function checkArguments() { 362 $argc = count($this->args); 363 364 $req = 0; 365 foreach($this->setup[$this->command]['args'] as $arg) { 366 if(!$arg['required']) break; // last required arguments seen 367 $req++; 368 } 369 370 if($req > $argc) throw new DokuCLI_Exception("Not enough arguments", DokuCLI_Exception::E_OPT_ARG_REQUIRED); 371 } 372 373 /** 374 * Parses the given arguments for known options and command 375 * 376 * The given $args array should NOT contain the executed file as first item anymore! The $args 377 * array is stripped from any options and possible command. All found otions can be accessed via the 378 * getOpt() function 379 * 380 * Note that command options will overwrite any global options with the same name 381 * 382 * @throws DokuCLI_Exception 383 */ 384 public function parseOptions() { 385 $non_opts = array(); 386 387 $argc = count($this->args); 388 for($i = 0; $i < $argc; $i++) { 389 $arg = $this->args[$i]; 390 391 // The special element '--' means explicit end of options. Treat the rest of the arguments as non-options 392 // and end the loop. 393 if($arg == '--') { 394 $non_opts = array_merge($non_opts, array_slice($this->args, $i + 1)); 395 break; 396 } 397 398 // '-' is stdin - a normal argument 399 if($arg == '-') { 400 $non_opts = array_merge($non_opts, array_slice($this->args, $i)); 401 break; 402 } 403 404 // first non-option 405 if($arg[0] != '-') { 406 $non_opts = array_merge($non_opts, array_slice($this->args, $i)); 407 break; 408 } 409 410 // long option 411 if(strlen($arg) > 1 && $arg[1] == '-') { 412 list($opt, $val) = explode('=', substr($arg, 2), 2); 413 414 if(!isset($this->setup[$this->command]['opts'][$opt])) { 415 throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT); 416 } 417 418 // argument required? 419 if($this->setup[$this->command]['opts'][$opt]['needsarg']) { 420 if(is_null($val) && $i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { 421 $val = $this->args[++$i]; 422 } 423 if(is_null($val)) { 424 throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED); 425 } 426 $this->options[$opt] = $val; 427 } else { 428 $this->options[$opt] = true; 429 } 430 431 continue; 432 } 433 434 // short option 435 $opt = substr($arg, 1); 436 if(!isset($this->setup[$this->command]['short'][$opt])) { 437 throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT); 438 } else { 439 $opt = $this->setup[$this->command]['short'][$opt]; // store it under long name 440 } 441 442 // argument required? 443 if($this->setup[$this->command]['opts'][$opt]['needsarg']) { 444 $val = null; 445 if($i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { 446 $val = $this->args[++$i]; 447 } 448 if(is_null($val)) { 449 throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED); 450 } 451 $this->options[$opt] = $val; 452 } else { 453 $this->options[$opt] = true; 454 } 455 } 456 457 // parsing is now done, update args array 458 $this->args = $non_opts; 459 460 // if not done yet, check if first argument is a command and reexecute argument parsing if it is 461 if(!$this->command && $this->args && isset($this->setup[$this->args[0]])) { 462 // it is a command! 463 $this->command = array_shift($this->args); 464 $this->parseOptions(); // second pass 465 } 466 } 467 468 /** 469 * Get the value of the given option 470 * 471 * Please note that all options are accessed by their long option names regardless of how they were 472 * specified on commandline. 473 * 474 * Can only be used after parseOptions() has been run 475 * 476 * @param string $option 477 * @param bool|string $default what to return if the option was not set 478 * @return bool|string 479 */ 480 public function getOpt($option, $default = false) { 481 if(isset($this->options[$option])) return $this->options[$option]; 482 return $default; 483 } 484 485 /** 486 * Return the found command if any 487 * 488 * @return string 489 */ 490 public function getCmd() { 491 return $this->command; 492 } 493 494 /** 495 * Builds a help screen from the available options. You may want to call it from -h or on error 496 * 497 * @return string 498 */ 499 public function help() { 500 $text = ''; 501 502 $hascommands = (count($this->setup) > 1); 503 foreach($this->setup as $command => $config) { 504 $hasopts = (bool) $this->setup[$command]['opts']; 505 $hasargs = (bool) $this->setup[$command]['args']; 506 507 if(!$command) { 508 $text .= 'USAGE: '.$this->bin; 509 } else { 510 $text .= "\n$command"; 511 } 512 513 if($hasopts) $text .= ' <OPTIONS>'; 514 515 foreach($this->setup[$command]['args'] as $arg) { 516 if($arg['required']) { 517 $text .= ' <'.$arg['name'].'>'; 518 } else { 519 $text .= ' [<'.$arg['name'].'>]'; 520 } 521 } 522 $text .= "\n"; 523 524 if($this->setup[$command]['help']) { 525 $text .= "\n"; 526 $text .= $this->tableFormat( 527 array(2, 72), 528 array('', $this->setup[$command]['help']."\n") 529 ); 530 } 531 532 if($hasopts) { 533 $text .= "\n OPTIONS\n\n"; 534 foreach($this->setup[$command]['opts'] as $long => $opt) { 535 536 $name = ''; 537 if($opt['short']) { 538 $name .= '-'.$opt['short']; 539 if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>'; 540 $name .= ', '; 541 } 542 $name .= "--$long"; 543 if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>'; 544 545 $text .= $this->tableFormat( 546 array(2, 20, 52), 547 array('', $name, $opt['help']) 548 ); 549 $text .= "\n"; 550 } 551 } 552 553 if($hasargs) { 554 $text .= "\n"; 555 foreach($this->setup[$command]['args'] as $arg) { 556 $name = '<'.$arg['name'].'>'; 557 558 $text .= $this->tableFormat( 559 array(2, 20, 52), 560 array('', $name, $arg['help']) 561 ); 562 } 563 } 564 565 if($command == '' && $hascommands) { 566 $text .= "\nThis tool accepts a command as first parameter as outlined below:\n"; 567 } 568 } 569 570 return $text; 571 } 572 573 /** 574 * Safely read the $argv PHP array across different PHP configurations. 575 * Will take care on register_globals and register_argc_argv ini directives 576 * 577 * @throws DokuCLI_Exception 578 * @return array the $argv PHP array or PEAR error if not registered 579 */ 580 private function readPHPArgv() { 581 global $argv; 582 if(!is_array($argv)) { 583 if(!@is_array($_SERVER['argv'])) { 584 if(!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { 585 throw new DokuCLI_Exception( 586 "Could not read cmd args (register_argc_argv=Off?)", 587 DOKU_CLI_OPTS_ARG_READ 588 ); 589 } 590 return $GLOBALS['HTTP_SERVER_VARS']['argv']; 591 } 592 return $_SERVER['argv']; 593 } 594 return $argv; 595 } 596 597 /** 598 * Displays text in multiple word wrapped columns 599 * 600 * @param int[] $widths list of column widths (in characters) 601 * @param string[] $texts list of texts for each column 602 * @return string 603 */ 604 private function tableFormat($widths, $texts) { 605 $wrapped = array(); 606 $maxlen = 0; 607 608 foreach($widths as $col => $width) { 609 $wrapped[$col] = explode("\n", wordwrap($texts[$col], $width - 1, "\n", true)); // -1 char border 610 $len = count($wrapped[$col]); 611 if($len > $maxlen) $maxlen = $len; 612 613 } 614 615 $out = ''; 616 for($i = 0; $i < $maxlen; $i++) { 617 foreach($widths as $col => $width) { 618 if(isset($wrapped[$col][$i])) { 619 $val = $wrapped[$col][$i]; 620 } else { 621 $val = ''; 622 } 623 $out .= sprintf('%-'.$width.'s', $val); 624 } 625 $out .= "\n"; 626 } 627 return $out; 628 } 629 } 630 631 /** 632 * Class DokuCLI_Exception 633 * 634 * The code is used as exit code for the CLI tool. This should probably be extended. Many cases just fall back to the 635 * E_ANY code. 636 * 637 * @author Andreas Gohr <andi@splitbrain.org> 638 */ 639 class DokuCLI_Exception extends Exception { 640 const E_ANY = -1; // no error code specified 641 const E_UNKNOWN_OPT = 1; //Unrecognized option 642 const E_OPT_ARG_REQUIRED = 2; //Option requires argument 643 const E_OPT_ARG_DENIED = 3; //Option not allowed argument 644 const E_OPT_ABIGUOUS = 4; //Option abiguous 645 const E_ARG_READ = 5; //Could not read argv 646 647 /** 648 * @param string $message The Exception message to throw. 649 * @param int $code The Exception code 650 * @param Exception $previous The previous exception used for the exception chaining. 651 */ 652 public function __construct($message = "", $code = 0, Exception $previous = null) { 653 if(!$code) $code = DokuCLI_Exception::E_ANY; 654 parent::__construct($message, $code, $previous); 655 } 656 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body