594 lines
19 KiB
PHP
594 lines
19 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
|
|
*/
|
|
|
|
/**
|
|
* Defines the Option class and some standard value-checking functions.
|
|
*
|
|
* Instance attributes:
|
|
* shortOpts : [string]
|
|
* longOpts : [string]
|
|
*
|
|
* action : string
|
|
* type : string
|
|
* dest : string
|
|
* default : any
|
|
* nargs : int
|
|
* const : any
|
|
* choices : [string]
|
|
* callback : function
|
|
* callbackArgs : (any*)
|
|
* help : string
|
|
* metavar : string
|
|
*
|
|
* @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_Option
|
|
{
|
|
const SUPPRESS_HELP = 'SUPPRESS HELP';
|
|
const SUPPRESS_USAGE = 'SUPPRESS USAGE';
|
|
|
|
/**
|
|
* Not supplying a default is different from a default of None,
|
|
* so we need an explicit 'not supplied' value.
|
|
*/
|
|
public static $NO_DEFAULT = array('NO', 'DEFAULT');
|
|
|
|
public static function parseNumber($value)
|
|
{
|
|
if (!strlen($value)) {
|
|
return false;
|
|
}
|
|
|
|
// Values to check against or compute with.
|
|
$first = substr($value, 0, 1);
|
|
$prefix = substr($value, 0, 2);
|
|
$suffix = substr($value, 2);
|
|
|
|
// Hex
|
|
if ($prefix == '0x' || $prefix == '0X') {
|
|
if (strspn($suffix, '0123456789abcdefABCDEF') != strlen($suffix)) {
|
|
return false;
|
|
}
|
|
return hexdec($value);
|
|
}
|
|
|
|
// Binary
|
|
if ($prefix == '0b' || $prefix == '0B') {
|
|
if (strspn($suffix, '01') != strlen($suffix)) {
|
|
return false;
|
|
}
|
|
return bindec($value);
|
|
}
|
|
|
|
// Octal
|
|
if ($first == '0') {
|
|
$suffix = substr($value, 1);
|
|
if (strspn($suffix, '01234567') != strlen($suffix)) {
|
|
return false;
|
|
}
|
|
return octdec($suffix);
|
|
}
|
|
|
|
// Base 10
|
|
if (!is_numeric($value)) {
|
|
return false;
|
|
}
|
|
return intval($value);
|
|
}
|
|
|
|
public function checkBuiltin($opt, $value)
|
|
{
|
|
switch ($this->type) {
|
|
case 'int':
|
|
case 'long':
|
|
$number = self::parseNumber($value);
|
|
if ($number === false) {
|
|
$message = $this->type == 'int'
|
|
? Horde_Argv_Translation::t("option %s: invalid integer value: '%s'")
|
|
: Horde_Argv_Translation::t("option %s: invalid long integer value: '%s'");
|
|
throw new Horde_Argv_OptionValueException(
|
|
sprintf($message, $opt, $value));
|
|
}
|
|
return $number;
|
|
|
|
case 'float':
|
|
if (!is_numeric($value)) {
|
|
throw new Horde_Argv_OptionValueException(
|
|
sprintf(Horde_Argv_Translation::t("option %s: invalid floating-point value: '%s'"),
|
|
$opt, $value));
|
|
}
|
|
return floatval($value);
|
|
}
|
|
}
|
|
|
|
public function checkChoice($opt, $value)
|
|
{
|
|
if (in_array($value, $this->choices)) {
|
|
return $value;
|
|
} else {
|
|
$choices = array();
|
|
foreach ($this->choices as $choice) {
|
|
$choices[] = (string)$choice;
|
|
}
|
|
$choices = "'" . implode("', '", $choices) . "'";
|
|
throw new Horde_Argv_OptionValueException(sprintf(
|
|
Horde_Argv_Translation::t("option %s: invalid choice: '%s' (choose from %s)"),
|
|
$opt, $value, $choices));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* The list of instance attributes that may be set through keyword args to
|
|
* the constructor.
|
|
*/
|
|
public $ATTRS = array(
|
|
'action',
|
|
'type',
|
|
'dest',
|
|
'default',
|
|
'nargs',
|
|
'const',
|
|
'choices',
|
|
'callback',
|
|
'callbackArgs',
|
|
'help',
|
|
'metavar',
|
|
);
|
|
|
|
/**
|
|
* The set of actions allowed by option parsers. Explicitly listed here so
|
|
* the constructor can validate its arguments.
|
|
*/
|
|
public $ACTIONS = array(
|
|
'store',
|
|
'store_const',
|
|
'store_true',
|
|
'store_false',
|
|
'append',
|
|
'append_const',
|
|
'count',
|
|
'callback',
|
|
'help',
|
|
'version',
|
|
);
|
|
|
|
/**
|
|
* The set of actions that involve storing a value somewhere;
|
|
* also listed just for constructor argument validation. (If
|
|
* the action is one of these, there must be a destination.)
|
|
*/
|
|
public $STORE_ACTIONS = array(
|
|
'store',
|
|
'store_const',
|
|
'store_true',
|
|
'store_false',
|
|
'append',
|
|
'append_const',
|
|
'count',
|
|
);
|
|
|
|
/**
|
|
* The set of actions for which it makes sense to supply a value type,
|
|
* ie. which may consume an argument from the command line.
|
|
*/
|
|
public $TYPED_ACTIONS = array(
|
|
'store',
|
|
'append',
|
|
'callback',
|
|
);
|
|
|
|
/**
|
|
* The set of actions which *require* a value type, ie. that always consume
|
|
* an argument from the command line.
|
|
*/
|
|
public $ALWAYS_TYPED_ACTIONS = array('store', 'append');
|
|
|
|
/**
|
|
* The set of actions which take a 'const' attribute.
|
|
*/
|
|
public $CONST_ACTIONS = array('store_const', 'append_const');
|
|
|
|
/**
|
|
* The set of known types for option parsers.
|
|
*
|
|
* Again, listed here for constructor argument validation.
|
|
*/
|
|
public $TYPES = array('string', 'int', 'long', 'float', 'complex', 'choice');
|
|
|
|
/**
|
|
* Dictionary of argument checking functions, which convert and validate
|
|
* option arguments according to the option type.
|
|
*
|
|
* Signature of checking functions is:
|
|
*
|
|
* <code>
|
|
* mixed function(Horde_Argv_Option $option, string $opt, string $value)
|
|
* </code>
|
|
*
|
|
* where
|
|
* - $option is the Horde_Argv_Option instance calling the checker
|
|
* - $opt is the actual option seen on the command-line (eg. '-a', '--file')
|
|
* - $value is the option argument seen on the command-line
|
|
*
|
|
* The return value should be in the appropriate PHP type
|
|
* for $option->type -- eg. an integer if $option->type == 'int'.
|
|
*
|
|
* If no checker is defined for a type, arguments will be unchecked and
|
|
* remain strings.
|
|
*/
|
|
public $TYPE_CHECKER = array('int' => 'checkBuiltin',
|
|
'long' => 'checkBuiltin',
|
|
'float' => 'checkBuiltin',
|
|
'complex' => 'checkBuiltin',
|
|
'choice' => 'checkChoice',
|
|
);
|
|
|
|
/**
|
|
* A list of unbound method objects.
|
|
*
|
|
* They are called by the constructor, in order, after all attributes are
|
|
* initialized. The list is created and filled in later, after all the
|
|
* methods are actually defined. (I just put it here because I like to
|
|
* define and document all class attributes in the same place.) Subclasses
|
|
* that add another _check_*() method should define their own CHECK_METHODS
|
|
* list that adds their check method to those from this class.
|
|
*/
|
|
public $CHECK_METHODS = array('_checkAction',
|
|
'_checkType',
|
|
'_checkChoice',
|
|
'_checkDest',
|
|
'_checkConst',
|
|
'_checkNargs',
|
|
'_checkCallback',
|
|
);
|
|
|
|
// -- Constructor/initialization methods ----------------------------
|
|
|
|
public $shortOpts = array();
|
|
public $longOpts = array();
|
|
public $dest;
|
|
public $default;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
// The last argument to this function is an $attrs hash, if it
|
|
// is present and an array. All other arguments are $opts.
|
|
$opts = func_get_args();
|
|
$num = func_num_args();
|
|
if ($num == 0 || $num == 1 || !is_array($opts[$num - 1])) {
|
|
$attrs = array();
|
|
} else {
|
|
$attrs = array_pop($opts);
|
|
}
|
|
|
|
// Set shortOpts, longOpts attrs from 'opts' tuple.
|
|
// Have to be set now, in case no option strings are supplied.
|
|
$this->shortOpts = array();
|
|
$this->longOpts = array();
|
|
$opts = $this->_checkOptStrings($opts);
|
|
$this->_setOptStrings($opts);
|
|
|
|
// Set all other attrs (action, type, etc.) from 'attrs' dict
|
|
$this->_setAttrs($attrs);
|
|
|
|
// Check all the attributes we just set. There are lots of
|
|
// complicated interdependencies, but luckily they can be farmed
|
|
// out to the _check*() methods listed in CHECK_METHODS -- which
|
|
// could be handy for subclasses! The one thing these all share
|
|
// is that they raise OptionError if they discover a problem.
|
|
foreach ($this->CHECK_METHODS as $checker) {
|
|
call_user_func(array($this, $checker));
|
|
}
|
|
}
|
|
|
|
protected function _checkOptStrings($opts)
|
|
{
|
|
// Filter out None because early versions of Optik had exactly
|
|
// one short option and one long option, either of which
|
|
// could be None.
|
|
$opts = array_filter($opts);
|
|
if (!$opts) {
|
|
throw new InvalidArgumentException('at least one option string must be supplied');
|
|
}
|
|
return $opts;
|
|
}
|
|
|
|
protected function _setOptStrings($opts)
|
|
{
|
|
foreach ($opts as &$opt) {
|
|
$opt = (string)$opt;
|
|
|
|
if (strlen($opt) < 2) {
|
|
throw new Horde_Argv_OptionException(sprintf("invalid option string '%s': must be at least two characters long", $opt), $this);
|
|
} elseif (strlen($opt) == 2) {
|
|
if (!($opt[0] == '-' && $opt[1] != '-')) {
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"invalid short option string '%s': " .
|
|
"must be of the form -x, (x any non-dash char)", $opt), $this);
|
|
}
|
|
$this->shortOpts[] = $opt;
|
|
} else {
|
|
if (!(substr($opt, 0, 2) == '--' && $opt[2] != '-')) {
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"invalid long option string '%s': " .
|
|
"must start with --, followed by non-dash", $opt), $this);
|
|
}
|
|
$this->longOpts[] = $opt;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function _setAttrs($attrs)
|
|
{
|
|
foreach ($this->ATTRS as $attr) {
|
|
if (array_key_exists($attr, $attrs)) {
|
|
$this->$attr = $attrs[$attr];
|
|
unset($attrs[$attr]);
|
|
} else {
|
|
if ($attr == 'default') {
|
|
$this->$attr = self::$NO_DEFAULT;
|
|
} else {
|
|
$this->$attr = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($attrs) {
|
|
$attrs = array_keys($attrs);
|
|
sort($attrs);
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
'invalid keyword arguments: %s', implode(', ', $attrs)), $this);
|
|
}
|
|
}
|
|
|
|
|
|
// -- Constructor validation methods --------------------------------
|
|
|
|
public function _checkAction()
|
|
{
|
|
if (is_null($this->action)) {
|
|
$this->action = 'store';
|
|
} elseif (!in_array($this->action, $this->ACTIONS)) {
|
|
throw new Horde_Argv_OptionException(sprintf("invalid action: '%s'", $this->action), $this);
|
|
}
|
|
}
|
|
|
|
public function _checkType()
|
|
{
|
|
if (is_null($this->type)) {
|
|
if (in_array($this->action, $this->ALWAYS_TYPED_ACTIONS)) {
|
|
if (!is_null($this->choices)) {
|
|
// The 'choices' attribute implies 'choice' type.
|
|
$this->type = 'choice';
|
|
} else {
|
|
// No type given? 'string' is the most sensible default.
|
|
$this->type = 'string';
|
|
}
|
|
}
|
|
} else {
|
|
if ($this->type == 'str') {
|
|
$this->type = 'string';
|
|
}
|
|
|
|
if (!in_array($this->type, $this->TYPES)) {
|
|
throw new Horde_Argv_OptionException(sprintf("invalid option type: '%s'", $this->type), $this);
|
|
}
|
|
|
|
if (!in_array($this->action, $this->TYPED_ACTIONS)) {
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"must not supply a type for action '%s'", $this->action), $this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function _checkChoice()
|
|
{
|
|
if ($this->type == 'choice') {
|
|
if (is_null($this->choices)) {
|
|
throw new Horde_Argv_OptionException(
|
|
"must supply a list of choices for type 'choice'", $this);
|
|
} elseif (!(is_array($this->choices) || $this->choices instanceof Iterator)) {
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"choices must be a list of strings ('%s' supplied)",
|
|
gettype($this->choices)), $this);
|
|
}
|
|
} elseif (!is_null($this->choices)) {
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"must not supply choices for type '%s'", $this->type), $this);
|
|
}
|
|
}
|
|
|
|
public function _checkDest()
|
|
{
|
|
// No destination given, and we need one for this action. The
|
|
// $this->type check is for callbacks that take a value.
|
|
$takes_value = (in_array($this->action, $this->STORE_ACTIONS) ||
|
|
!is_null($this->type));
|
|
if (is_null($this->dest) && $takes_value) {
|
|
// Glean a destination from the first long option string,
|
|
// or from the first short option string if no long options.
|
|
if ($this->longOpts) {
|
|
// eg. '--foo-bar' -> 'foo_bar'
|
|
$this->dest = str_replace('-', '_', substr($this->longOpts[0], 2));
|
|
} else {
|
|
$this->dest = $this->shortOpts[0][1];
|
|
}
|
|
}
|
|
}
|
|
|
|
public function _checkConst()
|
|
{
|
|
if (!in_array($this->action, $this->CONST_ACTIONS) && !is_null($this->const)) {
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"'const' must not be supplied for action '%s'", $this->action),
|
|
$this);
|
|
}
|
|
}
|
|
|
|
public function _checkNargs()
|
|
{
|
|
if (in_array($this->action, $this->TYPED_ACTIONS)) {
|
|
if (is_null($this->nargs)) {
|
|
$this->nargs = 1;
|
|
}
|
|
} elseif (!is_null($this->nargs)) {
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"'nargs' must not be supplied for action '%s'", $this->action),
|
|
$this);
|
|
}
|
|
}
|
|
|
|
public function _checkCallback()
|
|
{
|
|
if ($this->action == 'callback') {
|
|
if (!is_callable($this->callback)) {
|
|
$callback_name = is_array($this->callback) ?
|
|
is_object($this->callback[0]) ? get_class($this->callback[0] . '#' . $this->callback[1]) : implode('#', $this->callback) :
|
|
$this->callback;
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"callback not callable: '%s'", $callback_name), $this);
|
|
}
|
|
if (!is_null($this->callbackArgs) && !is_array($this->callbackArgs)) {
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"callbackArgs, if supplied, must be an array: not '%s'",
|
|
$this->callbackArgs), $this);
|
|
}
|
|
} else {
|
|
if (!is_null($this->callback)) {
|
|
$callback_name = is_array($this->callback) ?
|
|
is_object($this->callback[0]) ? get_class($this->callback[0] . '#' . $this->callback[1]) : implode('#', $this->callback) :
|
|
$this->callback;
|
|
throw new Horde_Argv_OptionException(sprintf(
|
|
"callback supplied ('%s') for non-callback option",
|
|
$callback_name), $this);
|
|
}
|
|
if (!is_null($this->callbackArgs)) {
|
|
throw new Horde_Argv_OptionException(
|
|
'callbackArgs supplied for non-callback option', $this);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// -- Miscellaneous methods -----------------------------------------
|
|
|
|
public function __toString()
|
|
{
|
|
return implode('/', array_merge($this->shortOpts, $this->longOpts));
|
|
}
|
|
|
|
public function takesValue()
|
|
{
|
|
return !is_null($this->type);
|
|
}
|
|
|
|
public function hasDefault()
|
|
{
|
|
return $this->default !== self::$NO_DEFAULT;
|
|
}
|
|
|
|
public function getOptString()
|
|
{
|
|
if ($this->longOpts) {
|
|
return $this->longOpts[0];
|
|
} else {
|
|
return $this->shortOpts[0];
|
|
}
|
|
}
|
|
|
|
|
|
// -- Processing methods --------------------------------------------
|
|
|
|
public function checkValue($opt, $value)
|
|
{
|
|
if (!isset($this->TYPE_CHECKER[$this->type])) {
|
|
return $value;
|
|
}
|
|
$checker = $this->TYPE_CHECKER[$this->type];
|
|
return call_user_func(array($this, $checker), $opt, $value);
|
|
}
|
|
|
|
public function convertValue($opt, $value)
|
|
{
|
|
if (!is_null($value)) {
|
|
if ($this->nargs == 1) {
|
|
return $this->checkValue($opt, $value);
|
|
} else {
|
|
$return = array();
|
|
foreach ($value as $v) {
|
|
$return[] = $this->checkValue($opt, $v);
|
|
}
|
|
return $return;
|
|
}
|
|
}
|
|
}
|
|
|
|
public function process($opt, $value, $values, $parser)
|
|
{
|
|
// First, convert the value(s) to the right type. Howl if any
|
|
// value(s) are bogus.
|
|
$value = $this->convertValue($opt, $value);
|
|
|
|
// And then take whatever action is expected of us.
|
|
// This is a separate method to make life easier for
|
|
// subclasses to add new actions.
|
|
return $this->takeAction(
|
|
$this->action, $this->dest, $opt, $value, $values, $parser);
|
|
}
|
|
|
|
public function takeAction($action, $dest, $opt, $value, $values, $parser)
|
|
{
|
|
if ($action == 'store') {
|
|
$values->$dest = $value;
|
|
} elseif ($action == 'store_const') {
|
|
$values->$dest = $this->const;
|
|
} elseif ($action == 'store_true') {
|
|
$values->$dest = true;
|
|
} elseif ($action == 'store_false') {
|
|
$values->$dest = false;
|
|
} elseif ($action == 'append') {
|
|
$values->{$dest}[] = $value;
|
|
} elseif ($action == 'append_const') {
|
|
$values->{$dest}[] = $this->const;
|
|
} elseif ($action == 'count') {
|
|
$values->ensureValue($dest, 0);
|
|
$values->$dest++;
|
|
} elseif ($action == 'callback') {
|
|
call_user_func($this->callback, $this, $opt, $value, $parser, $this->callbackArgs);
|
|
} elseif ($action == 'help') {
|
|
$parser->printHelp();
|
|
$parser->parserExit();
|
|
} elseif ($action == 'version') {
|
|
$parser->printVersion();
|
|
$parser->parserExit();
|
|
} else {
|
|
throw new RuntimeException('unknown action ' . $this->action);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
}
|