dokuwiki/vendor/splitbrain/php-cli/src/Options.php

469 lines
14 KiB
PHP

<?php
namespace splitbrain\phpcli;
/**
* Class Options
*
* Parses command line options passed to the CLI script. Allows CLI scripts to easily register all accepted options and
* commands and even generates a help text from this setup.
*
* @author Andreas Gohr <andi@splitbrain.org>
* @license MIT
*/
class Options
{
/** @var array keeps the list of options to parse */
protected $setup;
/** @var array store parsed options */
protected $options = array();
/** @var string current parsed command if any */
protected $command = '';
/** @var array passed non-option arguments */
protected $args = array();
/** @var string the executed script */
protected $bin;
/** @var Colors for colored help output */
protected $colors;
/**
* Constructor
*
* @param Colors $colors optional configured color object
* @throws Exception when arguments can't be read
*/
public function __construct(Colors $colors = null)
{
if (!is_null($colors)) {
$this->colors = $colors;
} else {
$this->colors = new Colors();
}
$this->setup = array(
'' => array(
'opts' => array(),
'args' => array(),
'help' => ''
)
); // default command
$this->args = $this->readPHPArgv();
$this->bin = basename(array_shift($this->args));
$this->options = array();
}
/**
* Sets the help text for the tool itself
*
* @param string $help
*/
public function setHelp($help)
{
$this->setup['']['help'] = $help;
}
/**
* Register the names of arguments for help generation and number checking
*
* This has to be called in the order arguments are expected
*
* @param string $arg argument name (just for help)
* @param string $help help text
* @param bool $required is this a required argument
* @param string $command if theses apply to a sub command only
* @throws Exception
*/
public function registerArgument($arg, $help, $required = true, $command = '')
{
if (!isset($this->setup[$command])) {
throw new Exception("Command $command not registered");
}
$this->setup[$command]['args'][] = array(
'name' => $arg,
'help' => $help,
'required' => $required
);
}
/**
* This registers a sub command
*
* Sub commands have their own options and use their own function (not main()).
*
* @param string $command
* @param string $help
* @throws Exception
*/
public function registerCommand($command, $help)
{
if (isset($this->setup[$command])) {
throw new Exception("Command $command already registered");
}
$this->setup[$command] = array(
'opts' => array(),
'args' => array(),
'help' => $help
);
}
/**
* Register an option for option parsing and help generation
*
* @param string $long multi character option (specified with --)
* @param string $help help text for this option
* @param string|null $short one character option (specified with -)
* @param bool|string $needsarg does this option require an argument? give it a name here
* @param string $command what command does this option apply to
* @throws Exception
*/
public function registerOption($long, $help, $short = null, $needsarg = false, $command = '')
{
if (!isset($this->setup[$command])) {
throw new Exception("Command $command not registered");
}
$this->setup[$command]['opts'][$long] = array(
'needsarg' => $needsarg,
'help' => $help,
'short' => $short
);
if ($short) {
if (strlen($short) > 1) {
throw new Exception("Short options should be exactly one ASCII character");
}
$this->setup[$command]['short'][$short] = $long;
}
}
/**
* Checks the actual number of arguments against the required number
*
* Throws an exception if arguments are missing.
*
* This is run from CLI automatically and usually does not need to be called directly
*
* @throws Exception
*/
public function checkArguments()
{
$argc = count($this->args);
$req = 0;
foreach ($this->setup[$this->command]['args'] as $arg) {
if (!$arg['required']) {
break;
} // last required arguments seen
$req++;
}
if ($req > $argc) {
throw new Exception("Not enough arguments", Exception::E_OPT_ARG_REQUIRED);
}
}
/**
* Parses the given arguments for known options and command
*
* The given $args array should NOT contain the executed file as first item anymore! The $args
* array is stripped from any options and possible command. All found otions can be accessed via the
* getOpt() function
*
* Note that command options will overwrite any global options with the same name
*
* This is run from CLI automatically and usually does not need to be called directly
*
* @throws Exception
*/
public function parseOptions()
{
$non_opts = array();
$argc = count($this->args);
for ($i = 0; $i < $argc; $i++) {
$arg = $this->args[$i];
// The special element '--' means explicit end of options. Treat the rest of the arguments as non-options
// and end the loop.
if ($arg == '--') {
$non_opts = array_merge($non_opts, array_slice($this->args, $i + 1));
break;
}
// '-' is stdin - a normal argument
if ($arg == '-') {
$non_opts = array_merge($non_opts, array_slice($this->args, $i));
break;
}
// first non-option
if ($arg{0} != '-') {
$non_opts = array_merge($non_opts, array_slice($this->args, $i));
break;
}
// long option
if (strlen($arg) > 1 && $arg{1} == '-') {
$arg = explode('=', substr($arg, 2), 2);
$opt = array_shift($arg);
$val = array_shift($arg);
if (!isset($this->setup[$this->command]['opts'][$opt])) {
throw new Exception("No such option '$opt'", Exception::E_UNKNOWN_OPT);
}
// argument required?
if ($this->setup[$this->command]['opts'][$opt]['needsarg']) {
if (is_null($val) && $i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) {
$val = $this->args[++$i];
}
if (is_null($val)) {
throw new Exception("Option $opt requires an argument",
Exception::E_OPT_ARG_REQUIRED);
}
$this->options[$opt] = $val;
} else {
$this->options[$opt] = true;
}
continue;
}
// short option
$opt = substr($arg, 1);
if (!isset($this->setup[$this->command]['short'][$opt])) {
throw new Exception("No such option $arg", Exception::E_UNKNOWN_OPT);
} else {
$opt = $this->setup[$this->command]['short'][$opt]; // store it under long name
}
// argument required?
if ($this->setup[$this->command]['opts'][$opt]['needsarg']) {
$val = null;
if ($i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) {
$val = $this->args[++$i];
}
if (is_null($val)) {
throw new Exception("Option $arg requires an argument",
Exception::E_OPT_ARG_REQUIRED);
}
$this->options[$opt] = $val;
} else {
$this->options[$opt] = true;
}
}
// parsing is now done, update args array
$this->args = $non_opts;
// if not done yet, check if first argument is a command and reexecute argument parsing if it is
if (!$this->command && $this->args && isset($this->setup[$this->args[0]])) {
// it is a command!
$this->command = array_shift($this->args);
$this->parseOptions(); // second pass
}
}
/**
* Get the value of the given option
*
* Please note that all options are accessed by their long option names regardless of how they were
* specified on commandline.
*
* Can only be used after parseOptions() has been run
*
* @param mixed $option
* @param bool|string $default what to return if the option was not set
* @return bool|string
*/
public function getOpt($option = null, $default = false)
{
if ($option === null) {
return $this->options;
}
if (isset($this->options[$option])) {
return $this->options[$option];
}
return $default;
}
/**
* Return the found command if any
*
* @return string
*/
public function getCmd()
{
return $this->command;
}
/**
* Get all the arguments passed to the script
*
* This will not contain any recognized options or the script name itself
*
* @return array
*/
public function getArgs()
{
return $this->args;
}
/**
* Builds a help screen from the available options. You may want to call it from -h or on error
*
* @return string
*/
public function help()
{
$tf = new TableFormatter($this->colors);
$text = '';
$hascommands = (count($this->setup) > 1);
foreach ($this->setup as $command => $config) {
$hasopts = (bool)$this->setup[$command]['opts'];
$hasargs = (bool)$this->setup[$command]['args'];
// usage or command syntax line
if (!$command) {
$text .= $this->colors->wrap('USAGE:', Colors::C_BROWN);
$text .= "\n";
$text .= ' ' . $this->bin;
$mv = 2;
} else {
$text .= "\n";
$text .= $this->colors->wrap(' ' . $command, Colors::C_PURPLE);
$mv = 4;
}
if ($hasopts) {
$text .= ' ' . $this->colors->wrap('<OPTIONS>', Colors::C_GREEN);
}
if (!$command && $hascommands) {
$text .= ' ' . $this->colors->wrap('<COMMAND> ...', Colors::C_PURPLE);
}
foreach ($this->setup[$command]['args'] as $arg) {
$out = $this->colors->wrap('<' . $arg['name'] . '>', Colors::C_CYAN);
if (!$arg['required']) {
$out = '[' . $out . ']';
}
$text .= ' ' . $out;
}
$text .= "\n";
// usage or command intro
if ($this->setup[$command]['help']) {
$text .= "\n";
$text .= $tf->format(
array($mv, '*'),
array('', $this->setup[$command]['help'] . "\n")
);
}
// option description
if ($hasopts) {
if (!$command) {
$text .= "\n";
$text .= $this->colors->wrap('OPTIONS:', Colors::C_BROWN);
}
$text .= "\n";
foreach ($this->setup[$command]['opts'] as $long => $opt) {
$name = '';
if ($opt['short']) {
$name .= '-' . $opt['short'];
if ($opt['needsarg']) {
$name .= ' <' . $opt['needsarg'] . '>';
}
$name .= ', ';
}
$name .= "--$long";
if ($opt['needsarg']) {
$name .= ' <' . $opt['needsarg'] . '>';
}
$text .= $tf->format(
array($mv, '30%', '*'),
array('', $name, $opt['help']),
array('', 'green', '')
);
$text .= "\n";
}
}
// argument description
if ($hasargs) {
if (!$command) {
$text .= "\n";
$text .= $this->colors->wrap('ARGUMENTS:', Colors::C_BROWN);
}
$text .= "\n";
foreach ($this->setup[$command]['args'] as $arg) {
$name = '<' . $arg['name'] . '>';
$text .= $tf->format(
array($mv, '30%', '*'),
array('', $name, $arg['help']),
array('', 'cyan', '')
);
}
}
// head line and intro for following command documentation
if (!$command && $hascommands) {
$text .= "\n";
$text .= $this->colors->wrap('COMMANDS:', Colors::C_BROWN);
$text .= "\n";
$text .= $tf->format(
array($mv, '*'),
array('', 'This tool accepts a command as first parameter as outlined below:')
);
$text .= "\n";
}
}
return $text;
}
/**
* Safely read the $argv PHP array across different PHP configurations.
* Will take care on register_globals and register_argc_argv ini directives
*
* @throws Exception
* @return array the $argv PHP array or PEAR error if not registered
*/
private function readPHPArgv()
{
global $argv;
if (!is_array($argv)) {
if (!@is_array($_SERVER['argv'])) {
if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
throw new Exception(
"Could not read cmd args (register_argc_argv=Off?)",
Exception::E_ARG_READ
);
}
return $GLOBALS['HTTP_SERVER_VARS']['argv'];
}
return $_SERVER['argv'];
}
return $argv;
}
}