* * @author Paul M. Jones * * @license http://opensource.org/licenses/bsd-license.php BSD * * @version $Id$ * */ class Solar_Getopt extends Solar_Base { /** * * Default configuration values. * * @config string filter_class The data-filter class to use when * validating and sanitizing parameter values. * * @config bool strict In strict mode, throw an exception when an * unknown option is passed into getopt. * * @var array * */ protected $_Solar_Getopt = array( 'filter_class' => 'Solar_Filter', 'strict' => true, ); /** * * The array of acceptable options. * * The `$options` array contains all options accepted by the * application, including their types, default values, descriptions, * requirements, and validation callbacks. * * In general, you should not try to set $options yourself; * instead, use [[Solar_Getopt::setOption()]] and/or * [[Solar_Getopt::setOptions()]]. * * @var array * */ public $options = array(); /** * * Default option settings. * * `long` * : (string) The long-form of the option name (e.g., "--foo-bar" would * be "foo-bar"). * * `short` * : (string) The short-form of the option, if any (e.g., "-f" would be * "f"). * * `descr` * : (string) A description of the option (used in "help" output). * * `param` * : (string) When the option is present, does it take a parameter? If so, * the param can be "r[eq[uired]]" every time, or be "[o[pt[ional]]". If empty, no * parameter for the option will be recognized (the option's value will be * boolean true when the option is present). Default is null; * recognizes `o`', `opt`, `optional`, `r`, `req`, and `required`. * * `value` * : (mixed) The default value for the option parameter, if any. This way, * options not specified in the arguments can have a default value. * * `require` * : (bool) At validation time, the option must have a non-blank value * of some sort. * * `filters` * : (array) An array of filters to apply to the parameter value. This can * be a single filter (`array('validateInt')`), or a series of filters * (`array('validateInt', array('validateRange', -10, +10)`). * * @var array * */ protected $_default = array( 'long' => null, 'short' => null, 'param' => null, 'value' => null, 'descr' => null, 'require' => false, 'filters' => array(), ); /** * * The arguments passed in from the command line. * * @var array * * @see populate() * */ protected $_argv; /** * * List of names for invalid option values, and error messages. * * @var array * */ protected $_invalid = array(); /** * * Option values parsed from the arguments, as well as remaining (numeric) * arguments. * * @var array * */ protected $_values; /** * * Post-construction tasks to complete object construction. * * @return void * */ protected function _postConstruct() { parent::_postConstruct(); // get the current request environment $this->_request = Solar_Registry::get('request'); // set up the data-filter class $this->_filter = Solar::factory($this->_config['filter_class']); } // ----------------------------------------------------------------- // // Option-management methods // // ----------------------------------------------------------------- /** * * Sets one option for recognition. * * @param string $name The option name to set or add; overrides * $info['short'] if 1 character long, otherwise overrides $info['long']. * * @param array $info Option information using the same keys * as [[Solar_Getopt::$_default]]. * * @return void * */ public function setOption($name, $info) { // prepare the option info $info = array_merge($this->_default, $info); // override the short- or long-form of the option if (strlen($name) == 1) { $info['short'] = $name; } else { // convert underscores to dashes for the *cli* $info['long'] = str_replace('_', '-', $name); } // normalize the "param" setting $param = strtolower($info['param']); if ($param == 'r' || substr($param, 0, 3) == 'req') { $info['param'] = 'required'; } elseif ($param == 'o' || substr($param, 0, 3) == 'opt') { $info['param'] = 'optional'; } else { $info['param'] = null; } // convert dashes to underscores for the *key* $name = str_replace('-', '_', $name); // forcibly cast each of the keys in the options array $this->options[$name] = array( 'long' => $info['long'], 'short' => substr($info['short'], 0, 1), 'param' => $info['param'], 'value' => $info['value'], 'descr' => $info['descr'], 'require' => (bool) $info['require'], 'filters' => array(), 'present' => false, // present in the cli command? ); // retain and fix any filters for the option value if ($info['filters']) { // make sure filters are an array settype($info['filters'], 'array'); // make sure that strings are converted to arrays so that // validate() works properly. foreach ($info['filters'] as $key => $list) { if (is_string($list)) { $info['filters'][$key] = array($list); } } } } /** * * Sets multiple acceptable options. Appends if they do not exist. * * @param array $list Argument information as array(name => info), where * each info value is an array like Solar_Getopt::$_default. * * @return void * */ public function setOptions($list) { if (! empty($list)) { foreach ($list as $name => $info) { $this->setOption($name, $info); } } } /** * * Populates the options with values from $argv. * * For a given option on the command line, these values will result: * * `--foo-bar` * : `'foo_bar' => true` * * `--foo-bar=baz` * : `'foo_bar' => 'baz'` * * `--foo-bar="baz dib zim"` * : `'foo_bar' => 'baz dib zim'` * * `-s` * : `'s' => true` * * `-s dib` * : `'s' => 'dib'` * * `-s "dib zim gir"` * : `'s' => 'dib zim gir'` * * Short-option clusters are parsed as well, so that `-fbz` will result * in `array('f' => true, 'b' => true, 'z' => true)`. Note that you * cannot pass parameters to an option in a cluster. * * If an option is not defined, an exception will be thrown. * * Options values are stored under the option key name, not the short- * or long-format version of the option. For example, an option named * 'foo-bar' with a short-form of 'f' will be stored under 'foo-bar'. * This helps deconflict between long- and short-form aliases. * * @param array $argv The argument values passed on the command line. If * empty, will use $_SERVER['argv'] after shifting off its first element. * * @return void * */ public function populate($argv = null) { // get the command-line arguments if ($argv === null) { $argv = $this->_request->argv(); array_shift($argv); } else { $argv = (array) $argv; } // hold onto the argv source $this->_argv = $argv; // reset values to defaults $this->_values = array(); foreach ($this->options as $name => $info) { $this->_values[$name] = $info['value']; } // flag to say when we've reached the end of options $done = false; // shift each element from the top of the $argv source while (true) { // get the next argument $arg = array_shift($this->_argv); if ($arg === null) { // no more args, we're done break; } // after a plain double-dash, all values are numeric (not options) if ($arg == '--') { $done = true; continue; } // if we're reached the end of options, just add to the numeric // values. if ($done) { $this->_values[] = $arg; continue; } // long, short, or numeric? if (substr($arg, 0, 2) == '--') { // long $this->_values = array_merge( $this->_values, (array) $this->_parseLong($arg) ); } elseif (substr($arg, 0, 1) == '-') { // short $this->_values = array_merge( $this->_values, (array) $this->_parseShort($arg) ); } else { // numeric $this->_values[] = $arg; } } } /** * * Applies validation and sanitizing filters to the option values. * * @return bool True if all values are valid, false if not. * */ public function validate() { // reset previous invalidations $this->_invalid = array(); // reset the filter chain so we can rebuild it $this->_filter->resetChain(); // build the filter chain and requires foreach ($this->options as $name => $info) { if ($info['present'] && $info['param'] == 'required') { $info['filters'][] = 'validateNotBlank'; } $this->_filter->addChainFilters($name, $info['filters']); $this->_filter->setChainRequire($name, $info['require']); } // apply the filter chain to the option values $status = $this->_filter->applyChain($this->_values); // retain any invalidation messages $invalid = $this->_filter->getChainInvalid(); foreach ((array) $invalid as $key => $val) { $this->_invalid[$key] = $val; } // done return $status; } /** * * Returns a list of invalid options and their error messages (if any). * * @return array * */ public function getInvalid() { return $this->_invalid; } /** * * Returns the populated option values. * * @return array * */ public function values() { return $this->_values; } /** * * Parse a long-form option. * * @param string $arg The $argv element, e.g. "--foo" or "--bar=baz". * * @return array An associative array where the key is the option name and * the value is the option value. * */ protected function _parseLong($arg) { // strip the leading "--" $arg = substr($arg, 2); // find the first = sign $eqpos = strpos($arg, '='); // get the key for name lookup if ($eqpos === false) { $key = $arg; $value = null; } else { $key = substr($arg, 0, $eqpos); $value = substr($arg, $eqpos+1); } // is this a recognized option? $name = $this->_getOptionName('long', $key); if (! $name) { return; } // the option is present $this->options[$name]['present'] = true; // was a value specified with equals? if ($eqpos !== false) { // parse the value for the option param return $this->_parseParam($name, $value); } // value was not specified with equals; // is a param needed at all? $info = $this->options[$name]; if (! $info['param']) { // defined as not-needing a param, treat as a flag. return array($name => true); } // the option was defined as needing a param (required or optional), // but there was no equals-sign. this means we need to look at the // next element for a possible param value. // // get the next element from $argv to see if it's a param. $value = array_shift($this->_argv); // make sure the element not an option itself. if (substr($value, 0, 1) == '-') { // the next element is an option, not a param. // this means no param is present. // put the element back into $argv. array_unshift($this->_argv, $value); // was the missing param required? if ($info['param'] == 'required') { // required but not present return array($name => null); } else { // optional but not present, treat as a flag return array($name => true); } } // parse the parameter for a required or optional value return $this->_parseParam($name, $value); } /** * * Parse the parameter value for a named option. * * @param string $name The option name. * * @param string $value The parameter. * * @return array An associative array where the option name is the key, * and the parsed parameter is the value. * */ protected function _parseParam($name, $value) { // get info about the option $info = $this->options[$name]; // is the value blank? if (trim($value) == '') { // value is blank. was it required for the option? if ($info['param'] == 'required') { // required but blank. return array($name => null); } else { // optional but blank, treat as a flag. return array($name => true); } } // param was present and not blank. return array($name => $value); } /** * * Parse a short-form option (or cluster of options). * * @param string $arg The $argv element, e.g. "-f" or "-fbz". * * @param bool $cluster This option is part of a cluster. * * @return array An associative array where the key is the option name and * the value is the option value. * */ protected function _parseShort($arg, $cluster = false) { // strip the leading "-" $arg = substr($arg, 1); // re-process as a cluster? if (strlen($arg) > 1) { $data = array(); foreach (str_split($arg) as $key) { $data = array_merge( $data, (array) $this->_parseShort("-$key", true) ); } return $data; } // is the option defined? $name = $this->_getOptionName('short', $arg); if (! $name) { // not defined return; } else { // keep the option info $info = $this->options[$name]; } // the option is present $this->options[$name]['present'] = true; // are we processing as part of a cluster? if ($cluster) { // is a param required for the option? if ($info['param'] == 'required') { // can't get params when in a cluster. return array($name => null); } else { // param was optional or not needed, treat as a flag. return array($name => true); } } // not processing as part of a cluster. // does the option need a param? if (! $info['param']) { // defined as not-needing a param, treat as a flag. return array($name => true); } // the option was defined as needing a param (required or optional). // get the next element from $argv to see if it's a param. $value = array_shift($this->_argv); // make sure the element not an option itself. if (substr($value, 0, 1) == '-') { // the next element is an option, not a param. // this means no param is present. // put the element back into $argv. array_unshift($this->_argv, $value); // was the missing param required? if ($info['param'] == 'required') { // required but not present return array($name => null); } else { // optional but not present, treat as a flag return array($name => true); } } // parse the parameter for a required or optional value return $this->_parseParam($name, $value); } /** * * Gets an option name from its short or long format. * * @param string $type Look in the 'long' or 'short' key for option names. * * @param string $spec The long or short format of the option name. * * @return string * */ protected function _getOptionName($type, $spec) { foreach ($this->options as $name => $info) { if ($info[$type] == $spec) { return $name; } } // if not in strict mode, we can let this go if (! $this->_config['strict']) { return; } // not found, blow up if ($type == 'short') { $spec = "-$spec"; } else { $spec = "--$spec"; } throw $this->_exception('ERR_UNKNOWN_OPTION', array( 'type' => $type, 'name' => $spec, 'options' => $this->options, )); } }