about summary refs log tree commit diff stats
path: root/wiki/inc/cli.php
diff options
context:
space:
mode:
authorahriman <ahriman@falte.red>2018-12-03 19:22:25 -0500
committerahriman <ahriman@falte.red>2018-12-03 19:22:25 -0500
commit0ae8cbf5c0b1a198b963490985b7738392ebcb97 (patch)
treeb2c77ae72c6b717e2b97492065196ac5ffb2d9e2 /wiki/inc/cli.php
parentf57f6cc5a2d159f90168d292437dc4bd8cd7f934 (diff)
downloadsite-0ae8cbf5c0b1a198b963490985b7738392ebcb97.tar.gz
installed dokuwiki, added to navbar, updated news
Diffstat (limited to 'wiki/inc/cli.php')
-rw-r--r--wiki/inc/cli.php655
1 files changed, 655 insertions, 0 deletions
diff --git a/wiki/inc/cli.php b/wiki/inc/cli.php
new file mode 100644
index 0000000..cb4fc58
--- /dev/null
+++ b/wiki/inc/cli.php
@@ -0,0 +1,655 @@
+<?php
+
+/**
+ * Class DokuCLI
+ *
+ * All DokuWiki commandline scripts should inherit from this class and implement the abstract methods.
+ *
+ * @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);
+    }
+}