Files
server/usr/share/psa-pear/pear/php/Horde/Argv/Parser.php
2026-01-07 20:52:11 +01:00

702 lines
24 KiB
PHP

<?php
/**
* Copyright 2010-2017 Horde LLC (http://www.horde.org/)
*
* This package is ported from Python's Optik (http://optik.sourceforge.net/).
*
* See the enclosed file LICENSE for license information (BSD). If you
* did not receive this file, see http://www.horde.org/licenses/bsd.
*
* @author Chuck Hagenbuch <chuck@horde.org>
* @author Mike Naberezny <mike@maintainable.com>
* @license http://www.horde.org/licenses/bsd BSD
* @category Horde
* @package Argv
*/
/**
* Horde command-line argument parsing package.
*
* Class attributes:
* standardOptionList : [Option]
* list of standard options that will be accepted by all instances
* of this parser class (intended to be overridden by subclasses).
*
* Instance attributes:
* usage : string
* a usage string for your program. Before it is displayed
* to the user, "%prog" will be expanded to the name of
* your program ($this->prog or os.path.basename(sys.argv[0])).
* prog : string
* the name of the current program (to override
* os.path.basename(sys.argv[0])).
* epilog : string
* paragraph of help text to print after option help
*
* optionGroups : [OptionGroup]
* list of option groups in this parser (option groups are
* irrelevant for parsing the command-line, but very useful
* for generating help)
*
* allowInterspersedArgs : bool = true
* if true, positional arguments may be interspersed with options.
* Assuming -a and -b each take a single argument, the command-line
* -ablah foo bar -bboo baz
* will be interpreted the same as
* -ablah -bboo -- foo bar baz
* If this flag were false, that command line would be interpreted as
* -ablah -- foo bar -bboo baz
* -- ie. we stop processing options as soon as we see the first
* non-option argument. (This is the tradition followed by
* Python's getopt module, Perl's Getopt::Std, and other argument-
* parsing libraries, but it is generally annoying to users.)
*
* allowUnknownArgs : bool = false
* if true, unrecognized arguments will be auto-created, instead
* of throwing a BadOptionException.
*
* ignoreUnknownArgs : bool = false
* if true, unrecognized arguments will be silently skipped, instead of
* throwing a BadOptionException.
*
* rargs : [string]
* the argument list currently being parsed. Only set when
* parseArgs() is active, and continually trimmed down as
* we consume arguments. Mainly there for the benefit of
* callback options.
* largs : [string]
* the list of leftover arguments that we have skipped while
* parsing options. If allowInterspersedArgs is false, this
* list is always empty.
* values : Values
* the set of option values currently being accumulated. Only
* set when parseArgs() is active. Also mainly for callbacks.
*
* @category Horde
* @package Argv
* @author Chuck Hagenbuch <chuck@horde.org>
* @author Mike Naberezny <mike@maintainable.com>
* @copyright 2010-2017 Horde LLC
* @license http://www.horde.org/licenses/bsd BSD
*/
class Horde_Argv_Parser extends Horde_Argv_OptionContainer
{
public $standardOptionList = array();
protected $_usage;
public $optionGroups = array();
public function __construct($args = array())
{
$args = array_merge(array(
'usage' => null,
'optionList' => null,
'optionClass' => 'Horde_Argv_Option',
'version' => null,
'conflictHandler' => "error",
'description' => null,
'formatter' => null,
'addHelpOption' => true,
'prog' => null,
'epilog' => null,
'allowInterspersedArgs' => true,
'allowUnknownArgs' => false,
'ignoreUnknownArgs' => false,
), $args);
parent::__construct($args['optionClass'], $args['conflictHandler'], $args['description']);
$this->setUsage($args['usage']);
$this->prog = $args['prog'];
$this->version = $args['version'];
$this->allowInterspersedArgs = $args['allowInterspersedArgs'];
$this->allowUnknownArgs = $args['allowUnknownArgs'];
$this->ignoreUnknownArgs = $args['ignoreUnknownArgs'];
if (is_null($args['formatter'])) {
$args['formatter'] = new Horde_Argv_IndentedHelpFormatter();
}
$this->formatter = $args['formatter'];
$this->formatter->setParser($this);
$this->epilog = $args['epilog'];
// Populate the option list; initial sources are the
// standardOptionList class attribute, the 'optionList'
// argument, and (if applicable) the _addVersionOption() and
// _addHelpOption() methods.
$this->_populateOptionList($args['optionList'],
$args['addHelpOption']);
$this->_initParsingState();
}
// -- Private methods -----------------------------------------------
// (used by our OptionContainer's constructor)
protected function _createOptionList()
{
$this->optionList = array();
$this->optionGroups = array();
$this->_createOptionMappings();
}
protected function _addHelpOption()
{
$this->addOption('-h', '--help', array('action' => 'help',
'help' => Horde_Argv_Translation::t("show this help message and exit")));
}
protected function _addVersionOption()
{
$this->addOption('--version', array('action' => 'version',
'help' => Horde_Argv_Translation::t("show program's version number and exit")));
}
protected function _populateOptionList($optionList, $add_help = true)
{
if ($this->standardOptionList)
$this->addOptions($this->standardOptionList);
if ($optionList)
$this->addOptions($optionList);
if ($this->version)
$this->_addVersionOption();
if ($add_help)
$this->_addHelpOption();
}
protected function _initParsingState()
{
// These are set in parseArgs() for the convenience of callbacks.
$this->rargs = null;
$this->largs = null;
$this->values = null;
}
// -- Simple modifier methods ---------------------------------------
public function setUsage($usage)
{
if (is_null($usage))
$this->_usage = '%prog ' . Horde_Argv_Translation::t("[options]");
elseif ($usage == Horde_Argv_Option::SUPPRESS_USAGE)
$this->_usage = null;
else
$this->_usage = $usage;
}
public function enableInterspersedArgs()
{
$this->allowInterspersedArgs = true;
}
public function disableInterspersedArgs()
{
$this->allowInterspersedArgs = false;
}
public function setDefault($dest, $value)
{
$this->defaults[$dest] = $value;
}
public function setDefaults($defaults)
{
$this->defaults = array_merge($this->defaults, $defaults);
}
protected function _getAllOptions()
{
$options = $this->optionList;
foreach ($this->optionGroups as $group) {
$options = array_merge($options, $group->optionList);
}
return $options;
}
public function getDefaultValues()
{
$defaults = $this->defaults;
foreach ($this->_getAllOptions() as $option) {
$default = isset($defaults[$option->dest]) ? $defaults[$option->dest] : null;
if (is_string($default)) {
$opt_str = $option->getOptString();
$defaults[$option->dest] = $option->checkValue($opt_str, $default);
}
}
return new Horde_Argv_Values($defaults);
}
// -- OptionGroup methods -------------------------------------------
public function addOptionGroup()
{
// XXX lots of overlap with OptionContainer::addOption()
$args = func_get_args();
if (count($args) && is_string($args[0])) {
$groupFactory = new ReflectionClass('Horde_Argv_OptionGroup');
array_unshift($args, $this);
$group = $groupFactory->newInstanceArgs($args);
} elseif (count($args) == 1) {
$group = $args[0];
if (!$group instanceof Horde_Argv_OptionGroup)
throw new InvalidArgumentException("not an OptionGroup instance: " . var_export($group, true));
if ($group->parser !== $this)
throw new InvalidArgumentException("invalid OptionGroup (wrong parser)");
} else {
throw new InvalidArgumentException('invalid arguments');
}
$this->optionGroups[] = $group;
$this->defaults = array_merge($this->defaults, $group->defaults);
return $group;
}
public function getOptionGroup($opt_str)
{
if (isset($this->shortOpt[$opt_str])) {
$option = $this->shortOpt[$opt_str];
} elseif (isset($this->longOpt[$opt_str])) {
$option = $this->longOpt[$opt_str];
} else {
return null;
}
if ($option->container !== $this) {
return $option->container;
}
return null;
}
// -- Option-parsing methods ----------------------------------------
protected function _getArgs($args = null)
{
if (is_null($args)) {
$args = $_SERVER['argv'];
array_shift($args);
return $args;
} else {
return $args;
}
}
/**
* Parse the command-line options found in 'args' (default:
* sys.argv[1:]). Any errors result in a call to 'parserError()', which
* by default prints the usage message to stderr and calls
* exit() with an error message. On success returns a pair
* (values, args) where 'values' is an Values instance (with all
* your option values) and 'args' is the list of arguments left
* over after parsing options.
*/
public function parseArgs($args = null, $values = null)
{
$rargs = $this->_getArgs($args);
$largs = array();
if (is_null($values))
$values = $this->getDefaultValues();
// Store the halves of the argument list as attributes for the
// convenience of callbacks:
// rargs
// the rest of the command-line (the "r" stands for
// "remaining" or "right-hand")
// largs
// the leftover arguments -- ie. what's left after removing
// options and their arguments (the "l" stands for "leftover"
// or "left-hand")
$this->rargs =& $rargs;
$this->largs =& $largs;
$this->values = $values;
try {
$this->_processArgs($largs, $rargs, $values);
} catch (Horde_Argv_BadOptionException $e) {
$this->parserError($e->getMessage());
} catch (Horde_Argv_OptionValueException $e) {
$this->parserError($e->getMessage());
}
$args = array_merge($largs, $rargs);
return $this->checkValues($values, $args);
}
/**
* Check that the supplied option values and leftover arguments are
* valid. Returns the option values and leftover arguments
* (possibly adjusted, possibly completely new -- whatever you
* like). Default implementation just returns the passed-in
* values; subclasses may override as desired.
*/
public function checkValues($values, $args)
{
return array($values, $args);
}
/**
* _process_args(largs : [string],
* rargs : [string],
* values : Values)
*
* Process command-line arguments and populate 'values', consuming
* options and arguments from 'rargs'. If 'allowInterspersedArgs' is
* false, stop at the first non-option argument. If true, accumulate any
* interspersed non-option arguments in 'largs'.
*/
protected function _processArgs(&$largs, &$rargs, &$values)
{
while ($rargs) {
$arg = $rargs[0];
// We handle bare "--" explicitly, and bare "-" is handled by the
// standard arg handler since the short arg case ensures that the
// len of the opt string is greater than 1.
if ($arg == '--') {
array_shift($rargs);
return;
} elseif (substr($arg, 0, 2) == '--') {
// process a single long option (possibly with value(s))
$this->_processLongOpt($rargs, $values);
} elseif (substr($arg, 0, 1) == '-' && strlen($arg) > 1) {
// process a cluster of short options (possibly with
// value(s) for the last one only)
$this->_processShortOpts($rargs, $values);
} elseif ($this->allowInterspersedArgs) {
$largs[] = $arg;
array_shift($rargs);
} else {
// stop now, leave this arg in rargs
return;
}
}
// Say this is the original argument list:
// [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
// ^
// (we are about to process arg(i)).
//
// Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
// [arg0, ..., arg(i-1)] (any options and their arguments will have
// been removed from largs).
//
// The while loop will usually consume 1 or more arguments per pass.
// If it consumes 1 (eg. arg is an option that takes no arguments),
// then after _process_arg() is done the situation is:
//
// largs = subset of [arg0, ..., arg(i)]
// rargs = [arg(i+1), ..., arg(N-1)]
//
// If allowInterspersedArgs is false, largs will always be
// *empty* -- still a subset of [arg0, ..., arg(i-1)], but
// not a very interesting subset!
}
/**
* opt : string) -> string
*
* Determine which long option string 'opt' matches, ie. which one
* it is an unambiguous abbrevation for. Raises BadOptionError if
* 'opt' doesn't unambiguously match any long option string.
*/
protected function _matchLongOpt($opt)
{
return self::matchAbbrev($opt, $this->longOpt);
}
/**
* (s : string, wordmap : {string : Option}) -> string
*
* Return the string key in 'wordmap' for which 's' is an unambiguous
* abbreviation. If 's' is found to be ambiguous or doesn't match any of
* 'words', raise BadOptionError.
*/
public static function matchAbbrev($s, $wordmap)
{
// Is there an exact match?
if (array_key_exists($s, $wordmap)) {
return $s;
}
// Isolate all words with s as a prefix.
$possibilities = array();
foreach (array_keys($wordmap) as $word) {
if (strncmp($word, $s, strlen($s)) === 0) {
$possibilities[] = $word;
}
}
// No exact match, so there had better be just one possibility.
if (count($possibilities) == 1) {
return $possibilities[0];
} elseif (!$possibilities) {
throw new Horde_Argv_BadOptionException($s);
} else {
// More than one possible completion: ambiguous prefix.
sort($possibilities);
throw new Horde_Argv_AmbiguousOptionException($s, $possibilities);
}
}
protected function _processLongOpt(&$rargs, &$values)
{
$arg = array_shift($rargs);
// Value explicitly attached to arg? Pretend it's the next
// argument.
if (strpos($arg, '=') !== false) {
list($opt, $next_arg) = explode('=', $arg, 2);
array_unshift($rargs, $next_arg);
$had_explicit_value = true;
} else {
$opt = $arg;
$had_explicit_value = false;
}
try {
$opt = $this->_matchLongOpt($opt);
$option = $this->longOpt[$opt];
} catch (Horde_Argv_BadOptionException $e) {
if ($this->ignoreUnknownArgs) {
return;
}
if ($this->allowUnknownArgs) {
$option = $this->addOption($opt, array('default' => true, 'action' => 'append'));
} else {
throw $e;
}
}
if ($option->takesValue()) {
$nargs = $option->nargs;
if (count($rargs) < $nargs) {
if (!$option->hasDefault()) {
if ($nargs == 1) {
$this->parserError(sprintf(Horde_Argv_Translation::t("%s option requires an argument"), $opt));
} else {
$this->parserError(sprintf(Horde_Argv_Translation::t("%s option requires %d arguments"), $opt, $nargs));
}
}
} elseif ($nargs == 1) {
$value = array_shift($rargs);
} else {
$value = array_splice($rargs, 0, $nargs);
}
} elseif ($had_explicit_value) {
$this->parserError(sprintf(Horde_Argv_Translation::t("%s option does not take a value"), $opt));
} else {
$value = null;
}
$option->process($opt, $value, $values, $this);
}
protected function _processShortOpts(&$rargs, &$values)
{
$arg = array_shift($rargs);
$stop = false;
$i = 1;
for ($c = 1, $c_max = strlen($arg); $c < $c_max; $c++) {
$ch = $arg[$c];
$opt = '-' . $ch;
$option = isset($this->shortOpt[$opt]) ? $this->shortOpt[$opt] : null;
$i++; // we have consumed a character
if (!$option) {
if ($this->allowUnknownArgs) {
$option = $this->addOption($opt, array('default' => true, 'action' => 'append'));
} else {
throw new Horde_Argv_BadOptionException($opt);
}
}
if ($option->takesValue()) {
// Any characters left in arg? Pretend they're the
// next arg, and stop consuming characters of arg.
if ($i < strlen($arg)) {
array_unshift($rargs, substr($arg, $i));
$stop = true;
}
$nargs = $option->nargs;
if (count($rargs) < $nargs) {
if (!$option->hasDefault()) {
if ($nargs == 1) {
$this->parserError(sprintf(Horde_Argv_Translation::t("%s option requires an argument"), $opt));
} else {
$this->parserError(sprintf(Horde_Argv_Translation::t("%s option requires %d arguments"), $opt, $nargs));
}
}
} elseif ($nargs == 1) {
$value = array_shift($rargs);
} else {
$value = array_splice($rargs, 0, $nargs);
}
} else {
// option doesn't take a value
$value = null;
}
$option->process($opt, $value, $values, $this);
if ($stop) { break; }
}
}
// -- Feedback methods ----------------------------------------------
public function getProgName()
{
if (is_null($this->prog))
return basename($_SERVER['argv'][0]);
else
return $this->prog;
}
public function expandProgName($s)
{
return str_replace("%prog", $this->getProgName(), $s);
}
public function getDescription()
{
return $this->expandProgName($this->description);
}
public function parserExit($status = 0, $msg = null)
{
if ($msg)
fwrite(STDERR, $msg);
exit($status);
}
/**
* Print a usage message incorporating $msg to stderr and exit.
* If you override this in a subclass, it should not return -- it
* should either exit or raise an exception.
*
* @param string $msg
*/
public function parserError($msg)
{
$this->printUsage(STDERR);
$this->parserExit(2, sprintf("%s: error: %s\n", $this->getProgName(), $msg));
}
public function getUsage($formatter = null)
{
if (is_null($formatter))
$formatter = $this->formatter;
if ($this->_usage)
return $formatter->formatUsage($this->expandProgName($this->_usage));
else
return '';
}
/**
* (file : file = stdout)
*
* Print the usage message for the current program ($this->_usage) to
* 'file' (default stdout). Any occurence of the string "%prog" in
* $this->_usage is replaced with the name of the current program
* (basename of sys.argv[0]). Does nothing if $this->_usage is empty
* or not defined.
*/
public function printUsage($file = null)
{
if (!$this->_usage)
return;
if (is_null($file))
echo $this->getUsage();
else
fwrite($file, $this->getUsage());
}
public function getVersion()
{
if ($this->version)
return $this->expandProgName($this->version);
else
return '';
}
/**
* file : file = stdout
*
* Print the version message for this program ($this->version) to
* 'file' (default stdout). As with printUsage(), any occurence
* of "%prog" in $this->version is replaced by the current program's
* name. Does nothing if $this->version is empty or undefined.
*/
public function printVersion($file = null)
{
if (!$this->version)
return;
if (is_null($file))
echo $this->getVersion() . "\n";
else
fwrite($file, $this->getVersion() . "\n");
}
public function formatOptionHelp($formatter = null)
{
if (is_null($formatter))
$formatter = $this->formatter;
$formatter->storeOptionStrings($this);
$result = array();
$result[] = $formatter->formatHeading(Horde_Argv_Translation::t("Options"));
$formatter->indent();
if ($this->optionList) {
$result[] = parent::formatOptionHelp($formatter);
$result[] = "\n";
}
foreach ($this->optionGroups as $group) {
$result[] = $group->formatHelp($formatter);
$result[] = "\n";
}
$formatter->dedent();
// Drop the last "\n", or the header if no options or option groups:
array_pop($result);
return implode('', $result);
}
public function formatEpilog($formatter)
{
return $formatter->formatEpilog($this->epilog);
}
public function formatHelp($formatter = null)
{
if (is_null($formatter))
$formatter = $this->formatter;
$result = array();
if ($this->_usage)
$result[] = $this->getUsage($formatter) . "\n";
if ($this->description)
$result[] = $this->formatDescription($formatter) . "\n";
$result[] = $this->formatOptionHelp($formatter);
$result[] = $this->formatEpilog($formatter);
return implode('', $result);
}
/**
* file : file = stdout
*
* Print an extended help message, listing all options and any
* help text provided with them, to 'file' (default stdout).
*/
public function printHelp($file = null)
{
if (is_null($file))
echo $this->formatHelp();
else
fwrite($file, $this->formatHelp());
}
}