*/ abstract class DokuCLI { /** @var string the executed script itself */ protected $bin; /** @var DokuCLI_Options the option parser */ protected $options; /** @var DokuCLI_Colors */ public $colors; /** * constructor * * Initialize the arguments, set up helper classes and set up the CLI environment */ public function __construct() { set_exception_handler(array($this, 'fatal')); $this->options = new DokuCLI_Options(); $this->colors = new DokuCLI_Colors(); dbg_deprecated('use \splitbrain\phpcli\CLI instead'); $this->error('DokuCLI is deprecated, use \splitbrain\phpcli\CLI instead.'); } /** * Register options and arguments on the given $options object * * @param DokuCLI_Options $options * @return void */ abstract protected function setup(DokuCLI_Options $options); /** * Your main program * * Arguments and options have been parsed when this is run * * @param DokuCLI_Options $options * @return void */ abstract protected function main(DokuCLI_Options $options); /** * Execute the CLI program * * Executes the setup() routine, adds default options, initiate the options parsing and argument checking * and finally executes main() */ public function run() { if('cli' != php_sapi_name()) throw new DokuCLI_Exception('This has to be run from the command line'); // setup $this->setup($this->options); $this->options->registerOption( 'no-colors', 'Do not use any colors in output. Useful when piping output to other tools or files.' ); $this->options->registerOption( 'help', 'Display this help screen and exit immediately.', 'h' ); // parse $this->options->parseOptions(); // handle defaults if($this->options->getOpt('no-colors')) { $this->colors->disable(); } if($this->options->getOpt('help')) { echo $this->options->help(); exit(0); } // check arguments $this->options->checkArguments(); // execute $this->main($this->options); exit(0); } /** * Exits the program on a fatal error * * @param Exception|string $error either an exception or an error message */ public function fatal($error) { $code = 0; if(is_object($error) && is_a($error, 'Exception')) { /** @var Exception $error */ $code = $error->getCode(); $error = $error->getMessage(); } if(!$code) $code = DokuCLI_Exception::E_ANY; $this->error($error); exit($code); } /** * Print an error message * * @param string $string */ public function error($string) { $this->colors->ptln("E: $string", 'red', STDERR); } /** * Print a success message * * @param string $string */ public function success($string) { $this->colors->ptln("S: $string", 'green', STDERR); } /** * Print an info message * * @param string $string */ public function info($string) { $this->colors->ptln("I: $string", 'cyan', STDERR); } } /** * Class DokuCLI_Colors * * Handles color output on (Linux) terminals * * @author Andreas Gohr */ class DokuCLI_Colors { /** @var array known color names */ protected $colors = array( 'reset' => "\33[0m", 'black' => "\33[0;30m", 'darkgray' => "\33[1;30m", 'blue' => "\33[0;34m", 'lightblue' => "\33[1;34m", 'green' => "\33[0;32m", 'lightgreen' => "\33[1;32m", 'cyan' => "\33[0;36m", 'lightcyan' => "\33[1;36m", 'red' => "\33[0;31m", 'lightred' => "\33[1;31m", 'purple' => "\33[0;35m", 'lightpurple' => "\33[1;35m", 'brown' => "\33[0;33m", 'yellow' => "\33[1;33m", 'lightgray' => "\33[0;37m", 'white' => "\33[1;37m", ); /** @var bool should colors be used? */ protected $enabled = true; /** * Constructor * * Tries to disable colors for non-terminals */ public function __construct() { if(function_exists('posix_isatty') && !posix_isatty(STDOUT)) { $this->enabled = false; return; } if(!getenv('TERM')) { $this->enabled = false; return; } } /** * enable color output */ public function enable() { $this->enabled = true; } /** * disable color output */ public function disable() { $this->enabled = false; } /** * Convenience function to print a line in a given color * * @param string $line * @param string $color * @param resource $channel */ public function ptln($line, $color, $channel = STDOUT) { $this->set($color); fwrite($channel, rtrim($line)."\n"); $this->reset(); } /** * Set the given color for consecutive output * * @param string $color one of the supported color names * @throws DokuCLI_Exception */ public function set($color) { if(!$this->enabled) return; if(!isset($this->colors[$color])) throw new DokuCLI_Exception("No such color $color"); echo $this->colors[$color]; } /** * reset the terminal color */ public function reset() { $this->set('reset'); } } /** * Class DokuCLI_Options * * Parses command line options passed to the CLI script. Allows CLI scripts to easily register all accepted options and * commands and even generates a help text from this setup. * * @author Andreas Gohr */ class DokuCLI_Options { /** @var array keeps the list of options to parse */ protected $setup; /** @var array store parsed options */ protected $options = array(); /** @var string current parsed command if any */ protected $command = ''; /** @var array passed non-option arguments */ public $args = array(); /** @var string the executed script */ protected $bin; /** * Constructor */ public function __construct() { $this->setup = array( '' => array( 'opts' => array(), 'args' => array(), 'help' => '' ) ); // default command $this->args = $this->readPHPArgv(); $this->bin = basename(array_shift($this->args)); $this->options = array(); } /** * Sets the help text for the tool itself * * @param string $help */ public function setHelp($help) { $this->setup['']['help'] = $help; } /** * Register the names of arguments for help generation and number checking * * This has to be called in the order arguments are expected * * @param string $arg argument name (just for help) * @param string $help help text * @param bool $required is this a required argument * @param string $command if theses apply to a sub command only * @throws DokuCLI_Exception */ public function registerArgument($arg, $help, $required = true, $command = '') { if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered"); $this->setup[$command]['args'][] = array( 'name' => $arg, 'help' => $help, 'required' => $required ); } /** * This registers a sub command * * Sub commands have their own options and use their own function (not main()). * * @param string $command * @param string $help * @throws DokuCLI_Exception */ public function registerCommand($command, $help) { if(isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command already registered"); $this->setup[$command] = array( 'opts' => array(), 'args' => array(), 'help' => $help ); } /** * Register an option for option parsing and help generation * * @param string $long multi character option (specified with --) * @param string $help help text for this option * @param string|null $short one character option (specified with -) * @param bool|string $needsarg does this option require an argument? give it a name here * @param string $command what command does this option apply to * @throws DokuCLI_Exception */ public function registerOption($long, $help, $short = null, $needsarg = false, $command = '') { if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered"); $this->setup[$command]['opts'][$long] = array( 'needsarg' => $needsarg, 'help' => $help, 'short' => $short ); if($short) { if(strlen($short) > 1) throw new DokuCLI_Exception("Short options should be exactly one ASCII character"); $this->setup[$command]['short'][$short] = $long; } } /** * Checks the actual number of arguments against the required number * * Throws an exception if arguments are missing. Called from parseOptions() * * @throws DokuCLI_Exception */ public function checkArguments() { $argc = count($this->args); $req = 0; foreach($this->setup[$this->command]['args'] as $arg) { if(!$arg['required']) break; // last required arguments seen $req++; } if($req > $argc) throw new DokuCLI_Exception("Not enough arguments", DokuCLI_Exception::E_OPT_ARG_REQUIRED); } /** * Parses the given arguments for known options and command * * The given $args array should NOT contain the executed file as first item anymore! The $args * array is stripped from any options and possible command. All found otions can be accessed via the * getOpt() function * * Note that command options will overwrite any global options with the same name * * @throws DokuCLI_Exception */ public function parseOptions() { $non_opts = array(); $argc = count($this->args); for($i = 0; $i < $argc; $i++) { $arg = $this->args[$i]; // The special element '--' means explicit end of options. Treat the rest of the arguments as non-options // and end the loop. if($arg == '--') { $non_opts = array_merge($non_opts, array_slice($this->args, $i + 1)); break; } // '-' is stdin - a normal argument if($arg == '-') { $non_opts = array_merge($non_opts, array_slice($this->args, $i)); break; } // first non-option if($arg[0] != '-') { $non_opts = array_merge($non_opts, array_slice($this->args, $i)); break; } // long option if(strlen($arg) > 1 && $arg[1] == '-') { list($opt, $val) = explode('=', substr($arg, 2), 2); if(!isset($this->setup[$this->command]['opts'][$opt])) { throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT); } // argument required? if($this->setup[$this->command]['opts'][$opt]['needsarg']) { if(is_null($val) && $i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { $val = $this->args[++$i]; } if(is_null($val)) { throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED); } $this->options[$opt] = $val; } else { $this->options[$opt] = true; } continue; } // short option $opt = substr($arg, 1); if(!isset($this->setup[$this->command]['short'][$opt])) { throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT); } else { $opt = $this->setup[$this->command]['short'][$opt]; // store it under long name } // argument required? if($this->setup[$this->command]['opts'][$opt]['needsarg']) { $val = null; if($i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { $val = $this->args[++$i]; } if(is_null($val)) { throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED); } $this->options[$opt] = $val; } else { $this->options[$opt] = true; } } // parsing is now done, update args array $this->args = $non_opts; // if not done yet, check if first argument is a command and reexecute argument parsing if it is if(!$this->command && $this->args && isset($this->setup[$this->args[0]])) { // it is a command! $this->command = array_shift($this->args); $this->parseOptions(); // second pass } } /** * Get the value of the given option * * Please note that all options are accessed by their long option names regardless of how they were * specified on commandline. * * Can only be used after parseOptions() has been run * * @param string $option * @param bool|string $default what to return if the option was not set * @return bool|string */ public function getOpt($option, $default = false) { if(isset($this->options[$option])) return $this->options[$option]; return $default; } /** * Return the found command if any * * @return string */ public function getCmd() { return $this->command; } /** * Builds a help screen from the available options. You may want to call it from -h or on error * * @return string */ public function help() { $text = ''; $hascommands = (count($this->setup) > 1); foreach($this->setup as $command => $config) { $hasopts = (bool) $this->setup[$command]['opts']; $hasargs = (bool) $this->setup[$command]['args']; if(!$command) { $text .= 'USAGE: '.$this->bin; } else { $text .= "\n$command"; } if($hasopts) $text .= ' '; foreach($this->setup[$command]['args'] as $arg) { if($arg['required']) { $text .= ' <'.$arg['name'].'>'; } else { $text .= ' [<'.$arg['name'].'>]'; } } $text .= "\n"; if($this->setup[$command]['help']) { $text .= "\n"; $text .= $this->tableFormat( array(2, 72), array('', $this->setup[$command]['help']."\n") ); } if($hasopts) { $text .= "\n OPTIONS\n\n"; foreach($this->setup[$command]['opts'] as $long => $opt) { $name = ''; if($opt['short']) { $name .= '-'.$opt['short']; if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>'; $name .= ', '; } $name .= "--$long"; if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>'; $text .= $this->tableFormat( array(2, 20, 52), array('', $name, $opt['help']) ); $text .= "\n"; } } if($hasargs) { $text .= "\n"; foreach($this->setup[$command]['args'] as $arg) { $name = '<'.$arg['name'].'>'; $text .= $this->tableFormat( array(2, 20, 52), array('', $name, $arg['help']) ); } } if($command == '' && $hascommands) { $text .= "\nThis tool accepts a command as first parameter as outlined below:\n"; } } return $text; } /** * Safely read the $argv PHP array across different PHP configurations. * Will take care on register_globals and register_argc_argv ini directives * * @throws DokuCLI_Exception * @return array the $argv PHP array or PEAR error if not registered */ private function readPHPArgv() { global $argv; if(!is_array($argv)) { if(!@is_array($_SERVER['argv'])) { if(!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { throw new DokuCLI_Exception( "Could not read cmd args (register_argc_argv=Off?)", DOKU_CLI_OPTS_ARG_READ ); } return $GLOBALS['HTTP_SERVER_VARS']['argv']; } return $_SERVER['argv']; } return $argv; } /** * Displays text in multiple word wrapped columns * * @param int[] $widths list of column widths (in characters) * @param string[] $texts list of texts for each column * @return string */ private function tableFormat($widths, $texts) { $wrapped = array(); $maxlen = 0; foreach($widths as $col => $width) { $wrapped[$col] = explode("\n", wordwrap($texts[$col], $width - 1, "\n", true)); // -1 char border $len = count($wrapped[$col]); if($len > $maxlen) $maxlen = $len; } $out = ''; for($i = 0; $i < $maxlen; $i++) { foreach($widths as $col => $width) { if(isset($wrapped[$col][$i])) { $val = $wrapped[$col][$i]; } else { $val = ''; } $out .= sprintf('%-'.$width.'s', $val); } $out .= "\n"; } return $out; } } /** * Class DokuCLI_Exception * * The code is used as exit code for the CLI tool. This should probably be extended. Many cases just fall back to the * E_ANY code. * * @author Andreas Gohr */ class DokuCLI_Exception extends Exception { const E_ANY = -1; // no error code specified const E_UNKNOWN_OPT = 1; //Unrecognized option const E_OPT_ARG_REQUIRED = 2; //Option requires argument const E_OPT_ARG_DENIED = 3; //Option not allowed argument const E_OPT_ABIGUOUS = 4; //Option abiguous const E_ARG_READ = 5; //Could not read argv /** * @param string $message The Exception message to throw. * @param int $code The Exception code * @param Exception $previous The previous exception used for the exception chaining. */ public function __construct($message = "", $code = 0, Exception $previous = null) { if(!$code) $code = DokuCLI_Exception::E_ANY; parent::__construct($message, $code, $previous); } }