[ 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      /**
  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