Location: PHPKode > scripts > PHPOptParse > Ephigenia-PHPOptParse-b0374a2/PHPOptParse.php
<?php

/**
 * Optparse
 * 
 * This class should or maybe is the optparse conversion of the well known
 * Optparser from the other famous programming languages such as python.
 * 
 * [here comes more docu soon, as for now, check the test.php file for an
 * exmple]
 * 
 * // todo split the parser from the options
 * // todo implement simple error checking on arguments
 * // todo implement custom error checking on arguments (or in ConsoleApp?)
 * 
 * @package ephFrame
 * @subpackage ephFrame.lib
 * @author Marcel Eichner // Ephigenia <hide@address.com>
 * @since 19.07.2008

 */
class OptParse extends Object
{	
	const TYPE_CALLBACK = 'callback';
	const TYPE_ARRAY 	= 'array';
	const TYPE_BOOL 	= 'boolean';
	const TYPE_STRING 	= 'string';
	const TYPE_INTEGER	= 'integer';
	const TYPE_FLOAT	= 'float';
	
	public $validTypes = array(
		self::TYPE_ARRAY,
		self::TYPE_BOOL,
		self::TYPE_CALLBACK,
		self::TYPE_FLOAT,
		self::TYPE_INTEGER,
		self::TYPE_STRING,
	);
	
	/**
	 * @var string
	 */
	protected $longArgRegExp = '/^-{1,2}([a-zA-z0-9]+)=?(.+)?$/';
	
	/**
	 * @var string
	 */
	protected $shortArgRegExp = '/^[-\/]([a-zA-z0-9]+)$/';
	
	/**
	 * Stores the raw arguments
	 * @var array(string)
	 */
	protected $rawArgs;
	
	/**
	 * Configuration for possible arguments and parameters this OptParse knows
	 * and can parse.
	 * @var array(string)
	 */
	public $config = array(
		// the help option should always be there
		array(
			'short' => 'h',
			'long' 	=> 'help',
			'help' 	=> 'display usage message and exit',
			'dest' 	=> 'help',
			'type' 	=> self::TYPE_BOOL,
			'store'	=> true
		)
	);
	
	/**
	 * Stores the string that is printed after 'Usage: '... so this might be
     * something like: '[options] [path] [url]
	 * @var string
	 */
	public $usage = '[options]';
	
	/**
	 * Stores the parsed options
	 * @var array(string)
	 */
	public $options = array();
	
	/**
	 * Stores the parsed arguments (everything that did not match in the options)
	 * @var array(string)
	 */
	public $arguments = array();
	
	/**
	 * OptParse constructor
	 * @param array(string)
	 * @return OptParse
	 */
	public function __construct(Array $args = array()) 
	{
		if (func_num_args() == 1) {
			$this->rawArgs = $args;
		} else {
			global $argv;
			$this->rawArgs = array_slice($argv, 1);
		}
		// merge config with parent class configs
		$this->parse();
		return $this;
	}
	
	/**
	 * Adds an other option info config array to the {@link config}.
	 * The type of the option is validated against {@link validTypes} array.
	 * @var array(string)
	 * @return OptParse
	 */
	public function addOption(Array $optionInfo) 
	{
		if (isset($optionInfo) && !in_array($optionInfo['type'], $this->validTypes)) {
			throw new OptParseInvalidTypeException($this, $optionInfo['type']);
		}
		$this->config[] = $optionInfo;
		return $this;
	}
	
	private function renderOptUsage(Array $optionInfo, $width = 80)
	{
		$optionValue = '';
		if (isset($optionInfo['dest'])
			&& isset($optionInfo['type'])
			&& $optionInfo['type'] != self::TYPE_BOOL)
			{
			$optionValue .= ' '.strtoupper($optionInfo['dest']);
		}
		// collect short and long options
		$argList = array();
		if (!empty($optionInfo['short'])) {
			$argList[] = '-'.$optionInfo['short'].$optionValue;
		}
		if (!empty($optionInfo['long'])) {
			$argList[] = '--'.$optionInfo['long'].$optionValue;
		}
		$argListRendered = '  '.implode(', ',$argList);
		$usage = $argListRendered;
		// add description to argument hint if set
		if (isset($optionInfo['help'])) {
			// if the usage parameters is to long, put help to the next line
			if (strlen($usage) > 22) {
				$usage .= LF;
				$helpMessage = wordwrap($optionInfo['help'], $width - 24 - 2, LF, true);
				$usage .= String::indent($helpMessage, 24, ' ');
			// or add the parameter help message in the next line, indented
			} else {
				$usage .= str_repeat(' ', 24 - strlen($usage));
				$m = wordwrap($optionInfo['help'], $width - 24, LF, true);
				$a = explode(LF, $m);
				$usage .= array_shift($a);
				if (count($a) > 0) {
					$usage .= LF;
				}
				$usage .= String::indent(implode(LF, $a), 24, ' ');
			}
		}
		return $usage;
	}
	
	/**
	 * Returns a usage Message for this ArgParser Options
	 * @param integer $width
	 */
	public function usage($width = 80) 
	{
		$r = 'Usage: '.basename($_SERVER['PHP_SELF']);
		if ($this->usage) {
			$r .= ' '.$this->usage;
		}
		$r .= LF;
		if (count($this->config) > 0) {
			$r .= LF.'Options:'.LF;
			foreach($this->config as $index => $optionInfo) {
				$r .= $this->renderOptUsage($optionInfo, $width).LF;
			}
		}
		return $r;
	}
	
	/**
	 * Parses the arguments and returns an array of options and arguments
	 * @return array(mixed)
	 */
	public function parse() 
	{
		// parse all arguments from the rawArguments array
		for($i = 0; $i < count($this->rawArgs); $i++) {
			// --[argumentname]=[value]
			if (preg_match($this->longArgRegExp, $this->rawArgs[$i], $found)) {
				$this->setOption($found[1], @$found[2]);
			// [-|/][argumentname] [value]
			} elseif (preg_match($this->shortArgRegExp, $this->rawArgs[$i], $found)) {
				$this->setOption($found[1], $found[2]);
			// -/ [optioname] value <-- value is the match
			// at least if [optionname] is no boolean
			} elseif (
				isset($this->rawArgs[$i-1])
				&& isset($this->rawArgs[$i])
				&& preg_match($this->shortArgRegExp, $this->rawArgs[$i-1])
				&& !preg_match($this->longArgRegExp, $this->rawArgs[$i])
				&& !preg_match($this->shortArgRegExp, $this->rawArgs[$i]))
				{
				// get name of last short arg
				preg_match($this->shortArgRegExp, $this->rawArgs[$i-1], $found);
				$lastShortArg = $found[1];
				$optionInfo = $this->getOptionDefinition($lastShortArg);
				if (isset($optionInfo['type']) && $optionInfo['type'] == self::TYPE_BOOL) {
					$this->arguments[] = $this->rawArgs[$i];
				} else {
					$this->setOption($lastShortArg, $this->rawArgs[$i]);
				}
			// test.php test <-- test would be the match
			} else {
				$this->arguments[] = $this->rawArgs[$i];
			}
		}
		// fill up arguments array with options that are not passed, but have
		// a default value or values that have short and long names and need
		// to get syncronized (this is kind of crap shit implementation i know)
		foreach($this->config as $index => $data) {
			if (!isset($data['default'])) {
				continue;
			}
			if (isset($data['short']) && !isset($this->options[$data['short']])) {
				if (isset($data['long']) && isset($this->options[$data['long']])) {
					$this->setOption($data['short'], $this->options[$data[$long]]);
				} else {
					$this->setOption($data['short'], $data['default']);
				}
			}
			if (isset($data['long']) && !isset($this->options[$data['long']])) {
				if (isset($data['short']) && isset($this->options[$data['short']])) {
					$this->setOption($data['long'], $this->options[$data['short']]);
				} else {
					$this->setOption($data['long'], $data['default']);
				}
			}
		}
		return array($this->options, $this->arguments);
	}
	
	private function getOptionDefinition($name) {
		foreach($this->config as $i => $optionInfo) {
			if ((is_array($optionInfo['short']) && in_array($name, $optionInfo['short']))
				|| (is_array($optionInfo['long']) && in_array($name, $optionInfo['long']))
				|| (in_array($name, array($optionInfo['short'], $optionInfo['long'])))
				) {
				return $optionInfo;
			}
		}
		return false;
	}
	
	public function setOption($name, $value = null) 
	{
		// try to find the arguments name in the options array
		if ($optionInfo = $this->getOptionDefinition($name)) {
			// special type of args, defined in the {@link options}
			switch(isset($optionInfo['type']) ? $optionInfo['type'] : false) {
				// callbacks
				case self::TYPE_CALLBACK:
					if (!empty($optionInfo['callback'])) {
						$methodName = $optionInfo['callback'];
					} elseif (!empty($optionInfo['dest'])) {
						$methodName = $optionInfo['dest'];
					} elseif (!empty($optionInfo['long'])) {
						$methodName = $optionInfo['long'];
					} else {
						user_error('Undefined callback name for option.', E_USER_ERROR);
					}
					return $this->$methodName($name, $value);
				// arrays
				case self::TYPE_ARRAY:
					if (!empty($value)) {
						if (!isset($this->arguments[$name])) {
							$this->options[$name] = array($value);
						} else {
							$this->options[$name][] = $value;
						}
					}
					return $this;
				// boolean and or flags
				case self::TYPE_BOOL:
					// options with default value and no storage directive are set their default
					/*if ($value == null && !isset($optionInfo['store']) && isset($optionInfo['default'])) {
						$value = $optionInfo['default'];
					} else*/
					if (isset($optionInfo['store']) && $value == null) {
						$value = $optionInfo['store'];
					}
					if (in_array($value, array('true', 'yes', 'ja', 'y', 'j', 't', true, null), true)) {
						$value = true;
					} elseif (in_array($value, array('false', 'no', 'nein', 'n', 'f', false), true)) {
						$value = false;
					}
					break;
				// strings
				case self::TYPE_STRING:
					$value = strval($value);
					break;
				// integers
				case self::TYPE_INTEGER:
					$value = intval($value);
					break;
				// floats and doubles
				case self::TYPE_FLOAT:
					$value = floatval($value);
					break;
			}
			if (isset($optionInfo['dest'])) {
				$name = $optionInfo['dest'];
			}
		// auto convert value to matching type (int/float)
		} else {
			// possibly hit a flag
			if (strlen($value) == 0) {
				$value = true;
			// integer values and also boolean values will be the same
			} else if (preg_match('/^[-+]?(\s+)?\d+/', $value)) {
				$value = (int) $value;
			// float values (. as devider)
			} elseif (preg_match('/^[-+]?(\s+)?\d+(.\d+)?$/', $value)) {
				$value = (float) $value;
			}
		}
		$this->options[$name] = $value;
		return $this;
	}
	
	/**
	 * Flushes the info stored in the ArgParser
	 * @return ArgParser
	 */
	public function flush() 
	{
		$this->arguments = array();
		$this->rawArgs = array();
		return $this;
	}

}

/**
 * @package ephFrame
 * @subpackage ephFrame.lib.exception
 */
class OptParseException extends Exception
{}

/**
 * @package ephFrame
 * @subpackage ephFrame.lib.exception
 */
class OptParseInvalidTypeException extends OptParseException 
{
	public function __construct(OptParse $optParse, $optionType) 
	{
		$message = sprintf('Invalid type of option passed in option info array. \'%s\' is not a valid OptParse option type', $optionType);
		parent::__construct($message);
	}
}


/**
 *	Automatic Test Case for {@link OptParse}
 * 	This will only be reached if you execute this file directly, otherwise
 * 	this part of the code will be ignored
 */
if (basename($_SERVER['PHP_SELF']) == basename(__FILE__)) {
	class OptParserTest extends OptParse{
		public $config = array(
			array(
				'short' => 'h',
				'long' 	=> 'help',
				'help' 	=> 'display usage message and exit',
				'dest' 	=> 'help',
				'type' 	=> self::TYPE_BOOL,
				'store'	=> true
			),
			array(
				'short' => 'q',
				'help' => 'sets verbose mode to not-verbose',
				'dest' => 'verbose',
				'type' => OptParse::TYPE_BOOL,
				'store' => false
			),
			array(
				'short' => 'v',
				'long'	=> 'verbose',
				'dest'	=> 'verbose',
				'type'	=> OptParse::TYPE_BOOL,
				'help' => 'sets verbose mode to true',
				'store' => true
			)
		);
	}
	$parser = new OptParserTest();
	$parser->parse();
	echo '------------------------'.LF.'generated usage message:'.LF.'------------------------'.LF;
	echo $parser->usage().LF;
	echo '------------------------'.LF.'parsed options:'.LF.'------------------------'.LF;
	var_dump($parser->options);
	echo '------------------------'.LF.'parsed arguments:'.LF.'------------------------'.LF;
	var_dump($parser->arguments);
	exit;
}
?>
Return current item: PHPOptParse