/inc/cli.php
PHP | 656 lines | 346 code | 83 blank | 227 comment | 69 complexity | 5171cef9b33796367041e898bac800a3 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1, GPL-2.0
- <?php
- /**
- * Class DokuCLI
- *
- * All DokuWiki commandline scripts should inherit from this class and implement the abstract methods.
- *
- * @deprecated 2017-11-10
- * @author Andreas Gohr <andi@splitbrain.org>
- */
- 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 <andi@splitbrain.org>
- */
- 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 <andi@splitbrain.org>
- */
- 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 .= ' <OPTIONS>';
- 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 <andi@splitbrain.org>
- */
- 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);
- }
- }