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