Created
September 19, 2018 18:45
-
-
Save ghassani/8079e5669f0ce5c842e02257da848dfb to your computer and use it in GitHub Desktop.
Simple PHP Argument Parser and Console wrapper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* ArgumentParser | |
* | |
* Usage: | |
* | |
* $args = new ArgumentParser(new Console(), array( | |
* new Argument('vara', 'a', Argument::ARG_TYPE_STRING, 'Help text for vara', null, Argument::ARG_OPTIONAL), | |
* new Argument('varb', 'b', Argument::ARG_TYPE_INT, 'Help text for varb', false, Argument::ARG_OPTIONAL), | |
* new Argument('varc', 'c', Argument::ARG_TYPE_BOOL, 'Help text for varc', false, Argument::ARG_OPTIONAL) | |
* )); | |
* | |
* $args->parse(); | |
* | |
* $argA = $args->getValue('vara'); | |
* $argB = $args->getValue($args->getArgumentByShortName('b')->getName()); | |
* $argC = $args->getValue('varc'); | |
* | |
* | |
* You can then pass values for these arguments and it will be validated: | |
* | |
* php <script_file> -aTEST -b2 -c | |
*/ | |
class ArgumentParser | |
{ | |
const STATE_PARSED = 0; | |
const STATE_DIRTY = 1; | |
/** @var array */ | |
protected $arguments = array(); | |
/** @var array */ | |
protected $parsedArguments = array(); | |
/** @var int */ | |
protected $state = self::STATE_DIRTY; | |
/** @var Console */ | |
protected $cli; | |
/** | |
* Constructor | |
* | |
* @param Console|null $cli | |
* @param array $arguments | |
*/ | |
public function __construct(Console $cli = null, array $arguments = array()) | |
{ | |
if (is_null($cli)) { | |
$this->cli = new Console(); | |
} else { | |
$this->cli = $cli; | |
} | |
if (count($arguments)) { | |
foreach ($arguments as $arg) { | |
if (!$arg instanceof Argument) { | |
throw \InvalidArgumentException('ArgumentParser constructor must contain an array of Argument if provided'); | |
} | |
$this->arguments[] = $arg; | |
} | |
} | |
// add in default "help" | |
$this->addArgument('help', 'h', Argument::ARG_TYPE_NO_VALUE, 'Displays help', false, Argument::ARG_OPTIONAL); | |
if (count($this->arguments)) { | |
$this->parse(); | |
} | |
} | |
/** | |
* addArgument | |
* | |
* Add an argument to be parsed | |
*/ | |
public function addArgument($name, $shortName, $type, $help = null, $default = null, $requirements = Argument::ARG_OPTIONAL) | |
{ | |
if ($this->hasArgument($name)) { | |
throw new \Exception(sprintf('Argument with name %s is already defined', $name)); | |
} else if ($this->hasArgumentByShortname($shortName)) { | |
throw new \Exception(sprintf('Argument with short name %s is already defined', $shortName)); | |
} | |
$this->setState(static::STATE_DIRTY); | |
$this->arguments[] = new Argument($name, $shortName, $type, $help, $default, $requirements); | |
return $this; | |
} | |
/** | |
* hasArgument | |
* | |
* Check if an argument exists by its name | |
*/ | |
public function hasArgument($name) | |
{ | |
foreach ($this->arguments as $arg) { | |
if ($arg->getName() == $name) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* hasArgumentByShortName | |
* | |
* Check if an argument exists by its short name | |
* | |
* @param string $name | |
* | |
* @return bool | |
*/ | |
public function hasArgumentByShortName($name) | |
{ | |
foreach ($this->arguments as $arg) { | |
if ($arg->getShortName() == $name) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* getArgument | |
* | |
* Get an argument by name | |
* | |
* @param string $name | |
* | |
* @throws \Exception when argument does not exist | |
* @return mixed | |
*/ | |
public function getArgument($name) | |
{ | |
foreach ($this->arguments as $arg) { | |
if ($arg->getName() == $name) { | |
return $arg; | |
} | |
} | |
throw new \Exception(sprintf('Argument `%s` does not exist', $name)); | |
} | |
/** | |
* getArgument | |
* | |
* Get an argument by name | |
* | |
* @param string $name | |
* | |
* @throws \Exception when argument does not exist | |
* @return Argument | |
*/ | |
public function getArgumentByShortName($name) | |
{ | |
foreach ($this->arguments as $arg) { | |
if ($arg->getShortName() == $name) { | |
return $arg; | |
} | |
} | |
throw new \Exception(sprintf('Argument short name `%s` does not exist', $name)); | |
} | |
/** | |
* getShortName | |
* | |
* Get an arguments short name by full name | |
* | |
* @param string $name | |
* | |
* @return string | |
*/ | |
public function getShortName($name) | |
{ | |
if (!$this->hasArgument($name)) { | |
return false; | |
} | |
return $this->getArgument($name)->getShortName(); | |
} | |
/** | |
* getValue | |
* | |
* Get an arguments value by full name | |
* | |
* @param string $name | |
* | |
* @return mixed | |
*/ | |
public function getValue($name) | |
{ | |
if (!$this->hasArgument($name)) { | |
return false; | |
} | |
return $this->getArgument($name)->getValue(); | |
} | |
/** | |
* getValueByShortname | |
* | |
* Get an arguments value by short name | |
* | |
* @param string $shortName | |
* | |
* @return mixed | |
*/ | |
public function getValueByShortname($shortName) | |
{ | |
if (!$this->hasArgumentByShortName($shortName)) { | |
return false; | |
} | |
return $this->getArgumentByShortName($shortName)->getValue(); | |
} | |
/** | |
* parse | |
* | |
* Parses the passed arguments from the command line with the specified arguments | |
* set. Called after arguments have been set | |
* | |
* @return array - Array of parsed arguments | |
*/ | |
public function parse() | |
{ | |
global $argv, $argc; | |
if ($this->isDirty() && count($this->arguments)) { | |
$shortopts = null; | |
$longopts = array(); | |
foreach ($this->arguments as $arg) { | |
if ($arg->getType() != Argument::ARG_TYPE_BOOL) { | |
if ($arg->getRequirements() == Argument::ARG_OPTIONAL) { | |
$shortopts .= $arg->getShortName() . '::'; | |
$longopts[] = $arg->getName() . '::'; | |
} else if ($arg->getRequirements() == Argument::ARG_REQUIRED) { | |
$shortopts .= $arg->getShortName() . ':'; | |
$longopts[] = $arg->getName() . ':'; | |
} else { | |
$shortopts .= $arg->getShortName(); | |
$longopts[] = $arg->getName(); | |
} | |
} else { | |
$shortopts .= $arg->getShortName(); | |
$longopts[] = $arg->getName(); | |
} | |
} | |
$opts = getopt($shortopts, $longopts); | |
$parsedOpts = array(); | |
foreach ($this->arguments as $arg) { | |
$valueFound = false; | |
if (isset($opts[$arg->getName()])) { | |
if ($arg->getType() == Argument::ARG_TYPE_BOOL) { | |
$arg->setValue(true); | |
} else { | |
$arg->setValue($opts[$arg->getName()]); | |
} | |
$valueFound = true; | |
} else if (isset($opts[$arg->getShortName()])) { | |
if ($arg->getType() == Argument::ARG_TYPE_BOOL) { | |
$arg->setValue(true); | |
} else { | |
$arg->setValue($opts[$arg->getShortName()]); | |
} | |
$valueFound = true; | |
} | |
if (!$valueFound && $arg->getRequirements() == Argument::ARG_REQUIRED && !$arg->getDefault()) { | |
$this->cli->writeLine(PHP_EOL . 'Error: Invalid or missing arguments' . PHP_EOL); | |
$this->usage(); | |
} | |
if ($arg->getType() == Argument::ARG_TYPE_INT) { | |
$parsedOpts[$arg->getName()] = (int) $arg->getValue(); | |
} else if ($arg->getType() == Argument::ARG_TYPE_DATETIME) { | |
if ($arg->getValue()) { | |
try { | |
if (is_numeric($arg->getValue())) { | |
// assume timestamp | |
$parsedOpts[$arg->getName()] = new \DateTime(); | |
$parsedOpts[$arg->getName()]->setTimestamp($arg->getValue()); | |
} else { | |
$parsedOpts[$arg->getName()] = new \DateTime($arg->getValue()); | |
} | |
} catch (\Exception $e) { | |
$this->cli->writeLine(PHP_EOL . 'Error: Option %s must be a valid date string' . PHP_EOL); | |
$this->usage(); | |
} | |
} else { | |
$parsedOpts[$arg->getName()] = null; | |
} | |
} else { | |
$parsedOpts[$arg->getName()] = $arg->getValue(); | |
} | |
$arg->setValue($parsedOpts[$arg->getName()]); | |
} | |
$this->parsedArguments = $parsedOpts; | |
} | |
$this->setState(static::STATE_PARSED); | |
if ($this->hasArgumentByShortName('h')) { | |
if ($this->getValue('help')) { | |
$this->usage(); | |
} | |
} | |
return $this->parsedArguments; | |
} | |
/** | |
* usage | |
* Displays argument usage and terminates the program | |
*/ | |
public function usage() | |
{ | |
foreach ($this->getHelp() as $line) { | |
$this->cli->writeLine($line); | |
} | |
die(); | |
} | |
/** | |
* getHelp | |
* Gets help text as an array of lines | |
* | |
* @return array | |
*/ | |
public function getHelp() | |
{ | |
$ret = array(); | |
$ret[] = 'Program Usage:'; | |
$ret[] = null; | |
$ret[] = "\t" . 'php <script> <options>'; | |
$ret[] = null; | |
$ret[] = 'Program Options:'; | |
$ret[] = null; | |
foreach ($this->arguments as $arg) { | |
$default = $arg->getDefault(); | |
if ($default === false) { | |
$default = 'False'; | |
} else if ($default === true) { | |
$default = 'True'; | |
} | |
if ($arg->getName() == 'help') { | |
$ret[] = "\t" . sprintf('--%s [-%s] | %s', | |
$arg->getName(), | |
$arg->getShortName(), | |
$arg->getHelp() | |
); | |
} else { | |
$ret[] = "\t" . sprintf('--%s [-%s] | %s - %s Default: %s', | |
$arg->getName(), | |
$arg->getShortName(), | |
$arg->getHelp(), | |
$arg->getRequirements() == Argument::ARG_REQUIRED ? 'REQUIRED' : 'OPTIONAL', | |
$default | |
); | |
$ret[] = "\t\t" . sprintf('Example: php script.php -%s%s', $arg->getShortName(), $arg->getType() == Argument::ARG_TYPE_BOOL ? '' : '<VALUE>'); | |
$ret[] = "\t\t" . sprintf('Example: php script.php --%s%s', $arg->getName(), $arg->getType() == Argument::ARG_TYPE_BOOL ? '' : '=<VALUE>'); | |
$ret[] = ''; | |
} | |
} | |
return $ret; | |
} | |
/** | |
* getConsole | |
* | |
* @return Console | |
*/ | |
public function getConsole() | |
{ | |
return $this->cli; | |
} | |
/** | |
* isDirty | |
* | |
* @return bool | |
*/ | |
private function isDirty() | |
{ | |
return $this->state == static::STATE_DIRTY; | |
} | |
/** | |
* setState | |
* | |
* @param int $state - one of STATE_ constants | |
* | |
* @return ArgumentParser | |
*/ | |
private function setState($state) | |
{ | |
$this->state = $state; | |
return $this; | |
} | |
} | |
/** | |
* Argument - Represents a single argument | |
*/ | |
class Argument | |
{ | |
const ARG_REQUIRED = 0x01; | |
const ARG_OPTIONAL = 0x02; | |
const ARG_TYPE_INT = 1; | |
const ARG_TYPE_FLOAT = 2; | |
const ARG_TYPE_STRING = 3; | |
const ARG_TYPE_BOOL = 4; | |
const ARG_TYPE_NO_VALUE = self::ARG_TYPE_BOOL; | |
const ARG_TYPE_DATETIME = 5; | |
protected $longName; | |
protected $shortName; | |
protected $help; | |
protected $default; | |
protected $requirements; | |
public function __construct($name, $shortName, $type = self::ARG_STRING, $help = null, $default = null, $requirements = self::ARG_OPTIONAL) | |
{ | |
$this->name = $name; | |
$this->shortName = $shortName; | |
$this->help = $help; | |
$this->default = $default; | |
$this->type = $type; | |
$this->requirements = $requirements; | |
} | |
public function getName() | |
{ | |
return $this->name; | |
} | |
public function setName($name) | |
{ | |
$this->name = $name; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getShortName() | |
{ | |
return $this->shortName; | |
} | |
/** | |
* @param mixed $shortName | |
*/ | |
public function setShortName($shortName) | |
{ | |
$this->shortName = $shortName; | |
return $this; | |
} | |
/** | |
* @return null | |
*/ | |
public function getHelp() | |
{ | |
return $this->help; | |
} | |
/** | |
* @param null $help | |
*/ | |
public function setHelp($help) | |
{ | |
$this->help = $help; | |
return $this; | |
} | |
/** | |
* @return null | |
*/ | |
public function getDefault() | |
{ | |
return $this->default; | |
} | |
/** | |
* @param null $default | |
*/ | |
public function setDefault($default) | |
{ | |
$this->default = $default; | |
return $this; | |
} | |
/** | |
* @return int | |
*/ | |
public function getRequirements() | |
{ | |
return $this->requirements; | |
} | |
/** | |
* @param int $requirements | |
*/ | |
public function setRequirements($requirements) | |
{ | |
$this->requirements = $requirements; | |
return $this; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getType() | |
{ | |
return $this->type; | |
} | |
/** | |
* @param mixed $type | |
*/ | |
public function setType($type) | |
{ | |
$this->type = $type; | |
} | |
public function getValue() | |
{ | |
return isset($this->value) ? $this->value : $this->getDefault(); | |
} | |
public function setValue($value) | |
{ | |
$this->value = $value; | |
return $this; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Console - Helper for reading and writing STDin/out or other stream | |
* | |
* Usage: | |
* $console = new Console(); | |
* $console->write('Hi!'); | |
* $console->write('Hello %s', 'World'); | |
* $console->writeLine('Hello!!'); | |
* $console->writeLine('Hello There %s', 'World'); | |
*/ | |
class Console | |
{ | |
/** @var resource */ | |
protected $stdout; | |
/** @var resource */ | |
protected $stdin; | |
/** | |
* Constructor | |
*/ | |
public function __construct($in = 'php://stdin', $out = 'php://stdout') | |
{ | |
$this->stdout = fopen($out, 'w'); | |
$this->stdin = fopen($in, 'r'); | |
} | |
/** | |
* Destructor | |
*/ | |
public function __destruct() | |
{ | |
if (is_resource($this->stdin)) { | |
fclose($this->stdin); | |
} | |
if (is_resource($this->stdout)) { | |
fclose($this->stdout); | |
} | |
} | |
/** | |
* write - Write to output stream without a terminating line character. | |
* | |
* @param string $msg | |
* @param additional string format arguments | |
*/ | |
public function write($msg) | |
{ | |
fwrite($this->stdout, vsprintf($msg, array_slice(func_get_args(), 1))); | |
} | |
/** | |
* writeLine - Write to output stream with a terminating lnie character. | |
* | |
* @param string $msg Formatted string, pass additional arguments to replace | |
*/ | |
public function writeLine($msg) | |
{ | |
fwrite($this->stdout, vsprintf($msg, array_slice(func_get_args(), 1)).PHP_EOL); | |
} | |
/** | |
* getUserInput | |
* | |
* @param string $prompt a message to display to the user. Formatted string, pass additional arguments to replace | |
* | |
* @return bool | |
*/ | |
public function getUserInput($prompt) | |
{ | |
if (function_exists('readline')) { | |
$data = readline(vsprintf($prompt, array_slice(func_get_args(), 1))); | |
} else { | |
$this->write(vsprintf($prompt, array_slice(func_get_args(), 1))); | |
$data = trim(fgets($this->stdin)); | |
} | |
return $data; | |
} | |
/** | |
* confirm | |
* | |
* @param string $prompt a message to display to the user | |
* | |
* @return bool | |
*/ | |
public function confirm($msg) { | |
$userResponse = strtolower(trim($this->getUserInput(vsprintf($msg, array_slice(func_get_args(), 1))))); | |
if (!$userResponse || !in_array($userResponse, array('y','n'))) { | |
$this->writeLine('Confirm with Y or N'); | |
return $this->confirm($msg); | |
} | |
return $userResponse == 'y' ? true : false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment