558 lines
12 KiB
PHP
Executable File
558 lines
12 KiB
PHP
Executable File
<?php
|
|
/*
|
|
* This file is part of the GetOptionKit package.
|
|
*
|
|
* (c) Yo-An Lin <cornelius.howl@gmail.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*
|
|
*/
|
|
|
|
namespace GetOptionKit;
|
|
|
|
|
|
use Exception;
|
|
use LogicException;
|
|
use InvalidArgumentException;
|
|
use GetOptionKit\Exception\InvalidOptionValueException;
|
|
|
|
class Option
|
|
{
|
|
public $short;
|
|
|
|
public $long;
|
|
|
|
/**
|
|
* @var string the description of this option
|
|
*/
|
|
public $desc;
|
|
|
|
/**
|
|
* @var string The option key
|
|
*/
|
|
public $key; /* key to store values */
|
|
|
|
public $value;
|
|
|
|
public $type;
|
|
|
|
public $valueName; /* name for the value place holder, for printing */
|
|
|
|
public $isa;
|
|
|
|
public $isaOption;
|
|
|
|
public $validValues;
|
|
|
|
public $suggestions;
|
|
|
|
public $defaultValue;
|
|
|
|
public $incremental = false;
|
|
|
|
/**
|
|
* @var Closure The filter closure of the option value.
|
|
*/
|
|
public $filter;
|
|
|
|
public $validator;
|
|
|
|
public $multiple = false;
|
|
|
|
public $optional = false;
|
|
|
|
public $required = false;
|
|
|
|
public $flag = false;
|
|
|
|
/**
|
|
* @var callable trigger callback after value is set.
|
|
*/
|
|
protected $trigger;
|
|
|
|
public function __construct($spec)
|
|
{
|
|
$this->initFromSpecString($spec);
|
|
}
|
|
|
|
/**
|
|
* Build spec attributes from spec string.
|
|
*
|
|
* @param string $specString
|
|
*/
|
|
protected function initFromSpecString($specString)
|
|
{
|
|
$pattern = '/
|
|
(
|
|
(?:[a-zA-Z0-9-]+)
|
|
(?:
|
|
\|
|
|
(?:[a-zA-Z0-9-]+)
|
|
)?
|
|
)
|
|
|
|
# option attribute operators
|
|
([:+?])?
|
|
|
|
# value types
|
|
(?:=(boolean|string|number|date|file|dir|url|email|ip|ipv6|ipv4))?
|
|
/x';
|
|
$ret = preg_match($pattern, $specString, $regs);
|
|
if ($ret === false || $ret === 0) {
|
|
throw new Exception('Incorrect spec string');
|
|
}
|
|
|
|
$orig = $regs[0];
|
|
$name = $regs[1];
|
|
$attributes = isset($regs[2]) ? $regs[2] : null;
|
|
$type = isset($regs[3]) ? $regs[3] : null;
|
|
|
|
$short = null;
|
|
$long = null;
|
|
|
|
// check long,short option name.
|
|
if (strpos($name, '|') !== false) {
|
|
list($short, $long) = explode('|', $name);
|
|
} else if (strlen($name) === 1) {
|
|
$short = $name;
|
|
} else if (strlen($name) > 1) {
|
|
$long = $name;
|
|
}
|
|
|
|
$this->short = $short;
|
|
$this->long = $long;
|
|
|
|
// option is required.
|
|
if (strpos($attributes, ':') !== false) {
|
|
$this->required();
|
|
} else if (strpos($attributes, '+') !== false) {
|
|
// option with multiple value
|
|
$this->multiple();
|
|
} else if (strpos($attributes, '?') !== false) {
|
|
// option is optional.(zero or one value)
|
|
$this->optional();
|
|
} else {
|
|
$this->flag();
|
|
}
|
|
if ($type) {
|
|
$this->isa($type);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* get the option key for result key mapping.
|
|
*/
|
|
public function getId()
|
|
{
|
|
return $this->key ?: $this->long ?: $this->short;
|
|
}
|
|
|
|
/**
|
|
* To make -v, -vv, -vvv works.
|
|
*/
|
|
public function incremental()
|
|
{
|
|
$this->incremental = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function required()
|
|
{
|
|
$this->required = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set default value
|
|
*
|
|
* @param mixed|Closure $value
|
|
*/
|
|
public function defaultValue($value)
|
|
{
|
|
$this->defaultValue = $value;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function multiple()
|
|
{
|
|
$this->multiple = true;
|
|
$this->value = array(); # for value pushing
|
|
return $this;
|
|
}
|
|
|
|
public function optional()
|
|
{
|
|
$this->optional = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function flag()
|
|
{
|
|
$this->flag = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function trigger(callable $trigger)
|
|
{
|
|
$this->trigger = $trigger;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function isIncremental()
|
|
{
|
|
return $this->incremental;
|
|
}
|
|
|
|
public function isFlag()
|
|
{
|
|
return $this->flag;
|
|
}
|
|
|
|
public function isMultiple()
|
|
{
|
|
return $this->multiple;
|
|
}
|
|
|
|
public function isRequired()
|
|
{
|
|
return $this->required;
|
|
}
|
|
|
|
public function isOptional()
|
|
{
|
|
return $this->optional;
|
|
}
|
|
|
|
public function isTypeNumber()
|
|
{
|
|
return $this->isa == 'number';
|
|
}
|
|
|
|
public function isType($type)
|
|
{
|
|
return $this->isa === $type;
|
|
}
|
|
|
|
public function getTypeClass()
|
|
{
|
|
$class = 'GetOptionKit\\ValueType\\'.ucfirst($this->isa).'Type';
|
|
if (class_exists($class, true)) {
|
|
return new $class($this->isaOption);
|
|
}
|
|
throw new Exception("Type class '$class' not found.");
|
|
}
|
|
|
|
public function testValue($value)
|
|
{
|
|
$type = $this->getTypeClass();
|
|
return $type->test($value);
|
|
}
|
|
|
|
protected function _preprocessValue($value)
|
|
{
|
|
$val = $value;
|
|
|
|
if ($isa = ucfirst($this->isa)) {
|
|
$type = $this->getTypeClass();
|
|
if ($type->test($value)) {
|
|
$val = $type->parse($value);
|
|
} else {
|
|
if (strtolower($isa) === 'regex') {
|
|
$isa .= '('.$this->isaOption.')';
|
|
}
|
|
throw new InvalidOptionValueException("Invalid value for {$this->renderReadableSpec(false)}. Requires a type $isa.");
|
|
}
|
|
}
|
|
|
|
// check pre-filter for option value
|
|
if ($this->filter) {
|
|
$val = call_user_func($this->filter, $val);
|
|
}
|
|
|
|
// check validValues
|
|
if ($validValues = $this->getValidValues()) {
|
|
if (!in_array($value, $validValues)) {
|
|
throw new InvalidOptionValueException('valid values are: '.implode(', ', $validValues));
|
|
}
|
|
}
|
|
|
|
if (!$this->validate($value)[0]) {
|
|
throw new InvalidOptionValueException('option is invalid');
|
|
}
|
|
|
|
return $val;
|
|
}
|
|
|
|
protected function callTrigger()
|
|
{
|
|
if ($this->trigger) {
|
|
if ($ret = call_user_func($this->trigger, $this->value)) {
|
|
$this->value = $ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* set option value
|
|
*/
|
|
public function setValue($value)
|
|
{
|
|
$this->value = $this->_preprocessValue($value);
|
|
$this->callTrigger();
|
|
}
|
|
|
|
/**
|
|
* This method is for incremental option.
|
|
*/
|
|
public function increaseValue()
|
|
{
|
|
if (!$this->value) {
|
|
$this->value = 1;
|
|
} else {
|
|
++$this->value;
|
|
}
|
|
$this->callTrigger();
|
|
}
|
|
|
|
/**
|
|
* push option value, when the option accept multiple values.
|
|
*
|
|
* @param mixed
|
|
*/
|
|
public function pushValue($value)
|
|
{
|
|
$value = $this->_preprocessValue($value);
|
|
$this->value[] = $value;
|
|
$this->callTrigger();
|
|
}
|
|
|
|
public function desc($desc)
|
|
{
|
|
$this->desc = $desc;
|
|
}
|
|
|
|
/**
|
|
* valueName is for option value hinting:.
|
|
*
|
|
* --name=<name>
|
|
*/
|
|
public function valueName($name)
|
|
{
|
|
$this->valueName = $name;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function renderValueHint()
|
|
{
|
|
$n = null;
|
|
if ($this->valueName) {
|
|
$n = $this->valueName;
|
|
} else if ($values = $this->getValidValues()) {
|
|
$n = '('.implode(',', $values).')';
|
|
} else if ($values = $this->getSuggestions()) {
|
|
$n = '['.implode(',', $values).']';
|
|
} else if ($val = $this->getDefaultValue()) {
|
|
// This allows for `0` and `false` values to be displayed also.
|
|
if ((is_scalar($val) && strlen((string) $val)) || is_bool($val)) {
|
|
if (is_bool($val)) {
|
|
$n = ($val ? 'true' : 'false');
|
|
} else {
|
|
$n = $val;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$n && $this->isa !== null) {
|
|
$n = '<'.$this->isa.'>';
|
|
}
|
|
if ($this->isRequired()) {
|
|
return sprintf('=%s', $n);
|
|
} else if ($this->isOptional() || $this->defaultValue) {
|
|
return sprintf('[=%s]', $n);
|
|
} else if ($n) {
|
|
return '='.$n;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
public function getDefaultValue()
|
|
{
|
|
if (is_callable($this->defaultValue)) {
|
|
return $this->defaultValue;
|
|
}
|
|
|
|
return $this->defaultValue;
|
|
}
|
|
|
|
public function getValue()
|
|
{
|
|
if (null !== $this->value) {
|
|
if (is_callable($this->value)) {
|
|
return call_user_func($this->value);
|
|
}
|
|
return $this->value;
|
|
}
|
|
|
|
return $this->getDefaultValue();
|
|
}
|
|
|
|
/**
|
|
* get readable spec for printing.
|
|
*
|
|
* @param string $renderHint render also value hint
|
|
*/
|
|
public function renderReadableSpec($renderHint = true)
|
|
{
|
|
$c1 = '';
|
|
if ($this->short && $this->long) {
|
|
$c1 = sprintf('-%s, --%s', $this->short, $this->long);
|
|
} else if ($this->short) {
|
|
$c1 = sprintf('-%s', $this->short);
|
|
} else if ($this->long) {
|
|
$c1 = sprintf('--%s', $this->long);
|
|
}
|
|
if ($renderHint) {
|
|
return $c1.$this->renderValueHint();
|
|
}
|
|
|
|
return $c1;
|
|
}
|
|
|
|
public function __toString()
|
|
{
|
|
$c1 = $this->renderReadableSpec();
|
|
$return = '';
|
|
$return .= sprintf('* key:%-8s spec:%s desc:%s', $this->getId(), $c1, $this->desc)."\n";
|
|
$val = $this->getValue();
|
|
if (is_array($val)) {
|
|
$return .= ' value => ' . join(',', array_map(function($v) { return var_export($v, true); }, $val))."\n";
|
|
} else {
|
|
$return .= sprintf(' value => %s', $val)."\n";
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Value Type Setters.
|
|
*
|
|
* @param string $type the value type, valid values are 'number', 'string',
|
|
* 'file', 'boolean', you can also use your own value type name.
|
|
* @param mixed $option option(s) for value type class (optionnal)
|
|
*/
|
|
public function isa($type, $option = null)
|
|
{
|
|
// "bool" was kept for backward compatibility
|
|
if ($type === 'bool') {
|
|
$type = 'boolean';
|
|
}
|
|
$this->isa = $type;
|
|
$this->isaOption = $option;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Assign validValues to member value.
|
|
*/
|
|
public function validValues($values)
|
|
{
|
|
$this->validValues = $values;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Assign suggestions.
|
|
*
|
|
* @param Closure|array
|
|
*/
|
|
public function suggestions($suggestions)
|
|
{
|
|
$this->suggestions = $suggestions;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return valud values array.
|
|
*
|
|
* @return string[] or nil
|
|
*/
|
|
public function getValidValues()
|
|
{
|
|
if ($this->validValues) {
|
|
if (is_callable($this->validValues)) {
|
|
return call_user_func($this->validValues);
|
|
}
|
|
|
|
return $this->validValues;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Return suggestions.
|
|
*
|
|
* @return string[] or nil
|
|
*/
|
|
public function getSuggestions()
|
|
{
|
|
if ($this->suggestions) {
|
|
if (is_callable($this->suggestions)) {
|
|
return call_user_func($this->suggestions);
|
|
}
|
|
|
|
return $this->suggestions;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
public function validate($value)
|
|
{
|
|
if ($this->validator) {
|
|
$ret = call_user_func($this->validator, $value);
|
|
if (is_array($ret)) {
|
|
return $ret;
|
|
} else if ($ret === false) {
|
|
return array(false, "Invalid value: $value");
|
|
} else if ($ret === true) {
|
|
return array(true, 'Successfully validated.');
|
|
}
|
|
throw new InvalidArgumentException('Invalid return value from the validator.');
|
|
}
|
|
|
|
return array(true);
|
|
}
|
|
|
|
public function validator($cb)
|
|
{
|
|
$this->validator = $cb;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set up a filter function for the option value.
|
|
*
|
|
* todo: add "callable" type hint later.
|
|
*/
|
|
public function filter($cb)
|
|
{
|
|
$this->filter = $cb;
|
|
|
|
return $this;
|
|
}
|
|
}
|