dokuwiki/inc/cli.php

657 lines
20 KiB
PHP

<?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);
}
}