[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/vendor/splitbrain/php-cli/src/ -> Options.php (source)

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