Location: PHPKode > projects > SolarPHP > solar-system-1.1.1/solar/source/solar/Solar/Getopt.php
<?php
/**
 * 
 * Retrieves and validates command-line options and parameter values.
 * 
 * @category Solar
 * 
 * @package Solar_Getopt Command-line option parsing.
 * 
 * @author Clay Loveless <hide@address.com>
 * 
 * @author Paul M. Jones <hide@address.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id: Getopt.php 4384 2010-02-14 16:56:47Z pmjones $
 * 
 */
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,
        ));
    }
}
Return current item: SolarPHP