|
<?php |
|
|
|
class Commando |
|
{ |
|
|
|
protected $filename; |
|
protected $command; |
|
protected $arguments = []; |
|
protected $options = []; |
|
protected $optionsAlias = []; |
|
protected $commands = []; |
|
protected $resolvedOptions = []; |
|
|
|
protected $foregroundColors = [ |
|
'black' => '0;30', |
|
'dark_gray' => '1;30', |
|
'blue' => '0;34', |
|
'light_blue' => '1;34', |
|
'green' => '0;32', |
|
'light_green' => '1;32', |
|
'cyan' => '0;36', |
|
'light_cyan' => '1;36', |
|
'red' => '0;31', |
|
'light_red' => '1;31', |
|
'purple' => '0;35', |
|
'light_purple' => '1;35', |
|
'brown' => '0;33', |
|
'yellow' => '1;33', |
|
'light_gray' => '0;37', |
|
'white' => '1;37', |
|
]; |
|
|
|
protected $backgroundColors = [ |
|
'black' => '40', |
|
'red' => '41', |
|
'green' => '42', |
|
'yellow' => '43', |
|
'blue' => '44', |
|
'magenta' => '45', |
|
'cyan' => '46', |
|
'light_gray' => '47', |
|
]; |
|
|
|
public function __construct(array $argv = null) |
|
{ |
|
if (is_null($argv)) { |
|
$argv = $GLOBALS['argv']; |
|
} |
|
|
|
list( |
|
$this->filename, |
|
$this->command, |
|
$this->arguments, |
|
$this->options, |
|
$this->optionsAlias |
|
) = $this->parseArgv($argv); |
|
|
|
$this->command('list', "Show available commands", [$this, 'showAvailableCommands']); |
|
} |
|
|
|
public function command($command, $description, callable $handler) |
|
{ |
|
list($command, $args, $options) = $this->parseCommand($command); |
|
|
|
$this->commands[$command] = [ |
|
'handler' => $handler, |
|
'description' => $description, |
|
'args' => $args, |
|
'options' => $options |
|
]; |
|
} |
|
|
|
public function run() |
|
{ |
|
return $this->execute($this->command); |
|
} |
|
|
|
public function execute($command) |
|
{ |
|
if (!isset($this->commands[$command])) { |
|
return $this->showAvailableCommands(); |
|
} |
|
|
|
if (array_key_exists('help', $this->options) OR array_key_exists('h', $this->optionsAlias)) { |
|
return $this->showHelp($command); |
|
} |
|
|
|
$handler = $this->commands[$command]['handler']; |
|
$arguments = $this->validateAndResolveArguments($command); |
|
$this->validateAndResolveOptions($command); |
|
|
|
if ($handler instanceof \Closure) { |
|
$handler = $handler->bindTo($this); |
|
} |
|
|
|
call_user_func_array($handler, $arguments); |
|
} |
|
|
|
public function option($key) |
|
{ |
|
return isset($this->resolvedOptions[$key])? $this->resolvedOptions[$key] : null; |
|
} |
|
|
|
protected function showAvailableCommands() |
|
{ |
|
$count = 0; |
|
$maxLen = 0; |
|
$this->writeln(PHP_EOL.$this->color(" Available Commands: ", 'blue').PHP_EOL); |
|
foreach(array_keys($this->commands) as $name) { |
|
if (strlen($name ) > $maxLen) $maxLen = strlen($name); |
|
} |
|
$pad = $maxLen + 3; |
|
|
|
foreach ($this->commands as $name => $command) { |
|
$no = ++$count.') '; |
|
$this->write(str_repeat(' ', 4 - strlen($no)).$this->color($no, 'dark_gray')); |
|
$this->write($this->color($name, 'green').str_repeat(' ', $pad - strlen($name))); |
|
$this->writeln($command['description']); |
|
$this->writeln(''); |
|
} |
|
|
|
$this->writeln(" Type '".$this->color("php ".$this->filename." <command> --help", 'blue')."' for usage information".PHP_EOL); |
|
} |
|
|
|
protected function showHelp($commandName) |
|
{ |
|
$command = $this->commands[$commandName]; |
|
$maxLen = 0; |
|
$args = $command['args']; |
|
$opts = $command['options']; |
|
$usageArgs = [$commandName]; |
|
$displayArgs = []; |
|
$displayOpts = []; |
|
foreach($args as $argName => $argSetting) { |
|
$usageArgs[] = "<".$argName.">"; |
|
$displayArg = $argName; |
|
if ($argSetting['is_optional']) { |
|
$displayArg .= " (optional)"; |
|
} |
|
if (strlen($displayArg) > $maxLen) { |
|
$maxLen = strlen($displayArg); |
|
} |
|
$displayArgs[$displayArg] = $argSetting['description']; |
|
} |
|
$usageArgs[] = "[options]"; |
|
|
|
foreach($opts as $optName => $optSetting) { |
|
$displayOpt = $optSetting['alias']? str_pad('-'.$optSetting['alias'].',', 4) : str_repeat(' ', 4); |
|
$displayOpt .= "--".$optName; |
|
if (strlen($displayOpt) > $maxLen) { |
|
$maxLen = strlen($displayOpt); |
|
} |
|
$displayOpts[$displayOpt] = $optSetting['description']; |
|
} |
|
|
|
$pad = $maxLen + 3; |
|
$this->writeln(PHP_EOL." ".$command['description'].PHP_EOL); |
|
$this->writeln($this->color(" Usage:", 'blue')); |
|
$this->writeln(''); |
|
$this->writeln(" ".implode(" ", $usageArgs)); |
|
$this->writeln(""); |
|
$this->writeln($this->color(" Arguments: ", 'blue').PHP_EOL); |
|
foreach($displayArgs as $argName => $argDesc) { |
|
$this->writeln(" ".$this->color($argName, 'green').str_repeat(' ', $pad - strlen($argName)).$argDesc); |
|
} |
|
$this->writeln(''); |
|
$this->writeln($this->color(" Options: ", 'blue').PHP_EOL); |
|
foreach($displayOpts as $optName => $optDesc) { |
|
$this->writeln(" ".$this->color($optName, 'green').str_repeat(' ', $pad - strlen($optName)).$optDesc); |
|
} |
|
$this->writeln(''); |
|
} |
|
|
|
public function write($message) |
|
{ |
|
print($message); |
|
} |
|
|
|
public function writeln($message) |
|
{ |
|
return $this->write($message.PHP_EOL); |
|
} |
|
|
|
public function error($message, $exit = true) |
|
{ |
|
$this->writeln(PHP_EOL." WHOOPS! ".$message.PHP_EOL); |
|
if ($exit) exit(); |
|
} |
|
|
|
public function color($text, $fg, $bg = null) |
|
{ |
|
$coloredString = ""; |
|
$colored = false; |
|
|
|
// Check if given foreground color found |
|
if (isset($this->foregroundColors[$fg])) { |
|
$colored = true; |
|
$coloredString .= "\033[" . $this->foregroundColors[$fg] . "m"; |
|
} |
|
// Check if given background color found |
|
if (isset($this->backgroundColors[$bg])) { |
|
$colored = true; |
|
$coloredString .= "\033[" . $this->backgroundColors[$bg] . "m"; |
|
} |
|
|
|
// Add string and end coloring |
|
$coloredString .= $text . ($colored? "\033[0m" : ""); |
|
|
|
return $coloredString; |
|
} |
|
|
|
protected function validateAndResolveArguments($command) |
|
{ |
|
$args = $this->arguments; |
|
$commandArgs = $this->commands[$command]['args']; |
|
$resolvedArgs = []; |
|
foreach($commandArgs as $argName => $argOption) { |
|
if (!$argOption['is_optional'] AND empty($args)) { |
|
return $this->error("Argument {$argName} is required"); |
|
} |
|
if ($argOption['is_array']) { |
|
$value = $args; |
|
} else { |
|
$value = array_shift($args) ?: $argOption['default']; |
|
} |
|
|
|
$resolvedArgs[$argName] = $value; |
|
} |
|
|
|
return $resolvedArgs; |
|
} |
|
|
|
protected function validateAndResolveOptions($command) |
|
{ |
|
$options = $this->options; |
|
$optionsAlias = $this->optionsAlias; |
|
$commandOptions = $this->commands[$command]['options']; |
|
$resolvedOptions = []; |
|
|
|
foreach ($commandOptions as $optName => $optionSetting) { |
|
$alias = $optionSetting['alias']; |
|
if ($alias AND isset($optionsAlias[$alias])) { |
|
$value = isset($optionsAlias[$alias])? $optionsAlias[$alias] : $optionSetting['default']; |
|
} else { |
|
$value = isset($options[$optName])? $options[$optName] : $optionSetting['default']; |
|
} |
|
|
|
if (!$optionSetting['is_valuable']) { |
|
$resolvedOptions[$optName] = !empty($value); |
|
} else { |
|
$resolvedOptions[$optName] = $value; |
|
} |
|
} |
|
|
|
$this->resolvedOptions = $resolvedOptions; |
|
} |
|
|
|
protected function getHandlerParams($handler) |
|
{ |
|
if ($handler instanceof \Closure) { |
|
$reflection = new ReflectionFunction($handler); |
|
} elseif(is_array($handler)) { |
|
$reflection = new ReflectionMethod($handler[0], $handler[1]); |
|
} elseif(is_string($handler)) { |
|
$reflection = new ReflectionFunction($handler); |
|
} |
|
|
|
return $reflection->getParameters(); |
|
} |
|
|
|
protected function parseCommand($command) |
|
{ |
|
$exp = explode(" ", trim($command), 2); |
|
$command = trim($exp[0]); |
|
$args = []; |
|
$options = []; |
|
|
|
if (isset($exp[1])) { |
|
preg_match_all("/\{(?<name>\w+)(?<arr>\*)?((=(?<default>[^\}]+))|(?<optional>\?))?(::(?<desc>[^}]+))?\}/i", $exp[1], $matchArgs); |
|
preg_match_all("/\{--((?<alias>[a-zA-Z])\|)?(?<name>\w+)((?<valuable>=)(?<default>[^\}]+)?)?(::(?<desc>[^}]+))?\}/i", $exp[1], $matchOptions); |
|
foreach($matchArgs['name'] as $i => $argName) { |
|
$default = $matchArgs['default'][$i]; |
|
$expDefault = explode('::', $default, 2); |
|
if (count($expDefault) > 1) { |
|
$default = $expDefault[0]; |
|
$description = $expDefault[1]; |
|
} else { |
|
$default = $expDefault[0]; |
|
$description = $matchArgs['desc'][$i]; |
|
} |
|
|
|
$args[$argName] = [ |
|
'is_array' => !empty($matchArgs['arr'][$i]), |
|
'is_optional' => !empty($matchArgs['optional'][$i]) || !empty($default), |
|
'default' => $default ?: null, |
|
'description' => $description, |
|
]; |
|
} |
|
|
|
foreach($matchOptions['name'] as $i => $optName) { |
|
$default = $matchOptions['default'][$i]; |
|
$expDefault = explode('::', $default, 2); |
|
if (count($expDefault) > 1) { |
|
$default = $expDefault[0]; |
|
$description = $expDefault[1]; |
|
} else { |
|
$default = $expDefault[0]; |
|
$description = $matchOptions['desc'][$i]; |
|
} |
|
$options[$optName] = [ |
|
'is_valuable' => !empty($matchOptions['valuable'][$i]), |
|
'default' => $default ?: null, |
|
'description' => $description, |
|
'alias' => $matchOptions['alias'][$i] ?: null, |
|
]; |
|
} |
|
} |
|
|
|
return [$command, $args, $options]; |
|
} |
|
|
|
protected function parseArgv(array $argv) |
|
{ |
|
$filename = array_shift($argv); |
|
$command = array_shift($argv); |
|
$arguments = []; |
|
$options = []; |
|
$optionsAlias = []; |
|
|
|
while (count($argv)) { |
|
$arg = array_shift($argv); |
|
if ($this->isOption($arg)) { |
|
$optName = ltrim($arg, "-"); |
|
if ($this->isOptionWithValue($arg)) { |
|
list($optName, $optvalue) = explode("=", $arg); |
|
} else { |
|
$optvalue = array_shift($argv); |
|
} |
|
|
|
$options[$optName] = $optvalue; |
|
} elseif ($this->isOptionAlias($arg)) { |
|
$alias = ltrim($arg, "-"); |
|
$exp = explode("=", $alias); |
|
$aliases = str_split($exp[0]); |
|
if (count($aliases) > 1) { |
|
foreach($aliases as $aliasName) { |
|
$optionsAlias[$aliasName] = null; |
|
} |
|
} else { |
|
$aliasName = $aliases[0]; |
|
if (count($exp) > 1) { |
|
list($aliasName, $aliasValue) = $exp; |
|
} else { |
|
$aliasValue = array_shift($argv); |
|
} |
|
|
|
$optionsAlias[$aliasName] = $aliasValue; |
|
} |
|
} else { |
|
$arguments[] = $arg; |
|
} |
|
} |
|
|
|
return [$filename, $command, $arguments, $options, $optionsAlias]; |
|
} |
|
|
|
protected function isOption($arg) |
|
{ |
|
return (bool) preg_match("/^--\w+/", $arg); |
|
} |
|
|
|
protected function isOptionAlias($arg) |
|
{ |
|
return (bool) preg_match("/^-[a-z]+/i", $arg); |
|
} |
|
|
|
protected function isOptionWithValue($arg) |
|
{ |
|
return strpos($arg, "=") !== false; |
|
} |
|
|
|
} |