[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> cli.php (source)

   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  }