PageRenderTime 43ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/cli.php

http://github.com/splitbrain/dokuwiki
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
  1. <?php
  2. /**
  3. * Class DokuCLI
  4. *
  5. * All DokuWiki commandline scripts should inherit from this class and implement the abstract methods.
  6. *
  7. * @deprecated 2017-11-10
  8. * @author Andreas Gohr <andi@splitbrain.org>
  9. */
  10. abstract class DokuCLI {
  11. /** @var string the executed script itself */
  12. protected $bin;
  13. /** @var DokuCLI_Options the option parser */
  14. protected $options;
  15. /** @var DokuCLI_Colors */
  16. public $colors;
  17. /**
  18. * constructor
  19. *
  20. * Initialize the arguments, set up helper classes and set up the CLI environment
  21. */
  22. public function __construct() {
  23. set_exception_handler(array($this, 'fatal'));
  24. $this->options = new DokuCLI_Options();
  25. $this->colors = new DokuCLI_Colors();
  26. dbg_deprecated('use \splitbrain\phpcli\CLI instead');
  27. $this->error('DokuCLI is deprecated, use \splitbrain\phpcli\CLI instead.');
  28. }
  29. /**
  30. * Register options and arguments on the given $options object
  31. *
  32. * @param DokuCLI_Options $options
  33. * @return void
  34. */
  35. abstract protected function setup(DokuCLI_Options $options);
  36. /**
  37. * Your main program
  38. *
  39. * Arguments and options have been parsed when this is run
  40. *
  41. * @param DokuCLI_Options $options
  42. * @return void
  43. */
  44. abstract protected function main(DokuCLI_Options $options);
  45. /**
  46. * Execute the CLI program
  47. *
  48. * Executes the setup() routine, adds default options, initiate the options parsing and argument checking
  49. * and finally executes main()
  50. */
  51. public function run() {
  52. if('cli' != php_sapi_name()) throw new DokuCLI_Exception('This has to be run from the command line');
  53. // setup
  54. $this->setup($this->options);
  55. $this->options->registerOption(
  56. 'no-colors',
  57. 'Do not use any colors in output. Useful when piping output to other tools or files.'
  58. );
  59. $this->options->registerOption(
  60. 'help',
  61. 'Display this help screen and exit immediately.',
  62. 'h'
  63. );
  64. // parse
  65. $this->options->parseOptions();
  66. // handle defaults
  67. if($this->options->getOpt('no-colors')) {
  68. $this->colors->disable();
  69. }
  70. if($this->options->getOpt('help')) {
  71. echo $this->options->help();
  72. exit(0);
  73. }
  74. // check arguments
  75. $this->options->checkArguments();
  76. // execute
  77. $this->main($this->options);
  78. exit(0);
  79. }
  80. /**
  81. * Exits the program on a fatal error
  82. *
  83. * @param Exception|string $error either an exception or an error message
  84. */
  85. public function fatal($error) {
  86. $code = 0;
  87. if(is_object($error) && is_a($error, 'Exception')) {
  88. /** @var Exception $error */
  89. $code = $error->getCode();
  90. $error = $error->getMessage();
  91. }
  92. if(!$code) $code = DokuCLI_Exception::E_ANY;
  93. $this->error($error);
  94. exit($code);
  95. }
  96. /**
  97. * Print an error message
  98. *
  99. * @param string $string
  100. */
  101. public function error($string) {
  102. $this->colors->ptln("E: $string", 'red', STDERR);
  103. }
  104. /**
  105. * Print a success message
  106. *
  107. * @param string $string
  108. */
  109. public function success($string) {
  110. $this->colors->ptln("S: $string", 'green', STDERR);
  111. }
  112. /**
  113. * Print an info message
  114. *
  115. * @param string $string
  116. */
  117. public function info($string) {
  118. $this->colors->ptln("I: $string", 'cyan', STDERR);
  119. }
  120. }
  121. /**
  122. * Class DokuCLI_Colors
  123. *
  124. * Handles color output on (Linux) terminals
  125. *
  126. * @author Andreas Gohr <andi@splitbrain.org>
  127. */
  128. class DokuCLI_Colors {
  129. /** @var array known color names */
  130. protected $colors = array(
  131. 'reset' => "\33[0m",
  132. 'black' => "\33[0;30m",
  133. 'darkgray' => "\33[1;30m",
  134. 'blue' => "\33[0;34m",
  135. 'lightblue' => "\33[1;34m",
  136. 'green' => "\33[0;32m",
  137. 'lightgreen' => "\33[1;32m",
  138. 'cyan' => "\33[0;36m",
  139. 'lightcyan' => "\33[1;36m",
  140. 'red' => "\33[0;31m",
  141. 'lightred' => "\33[1;31m",
  142. 'purple' => "\33[0;35m",
  143. 'lightpurple' => "\33[1;35m",
  144. 'brown' => "\33[0;33m",
  145. 'yellow' => "\33[1;33m",
  146. 'lightgray' => "\33[0;37m",
  147. 'white' => "\33[1;37m",
  148. );
  149. /** @var bool should colors be used? */
  150. protected $enabled = true;
  151. /**
  152. * Constructor
  153. *
  154. * Tries to disable colors for non-terminals
  155. */
  156. public function __construct() {
  157. if(function_exists('posix_isatty') && !posix_isatty(STDOUT)) {
  158. $this->enabled = false;
  159. return;
  160. }
  161. if(!getenv('TERM')) {
  162. $this->enabled = false;
  163. return;
  164. }
  165. }
  166. /**
  167. * enable color output
  168. */
  169. public function enable() {
  170. $this->enabled = true;
  171. }
  172. /**
  173. * disable color output
  174. */
  175. public function disable() {
  176. $this->enabled = false;
  177. }
  178. /**
  179. * Convenience function to print a line in a given color
  180. *
  181. * @param string $line
  182. * @param string $color
  183. * @param resource $channel
  184. */
  185. public function ptln($line, $color, $channel = STDOUT) {
  186. $this->set($color);
  187. fwrite($channel, rtrim($line)."\n");
  188. $this->reset();
  189. }
  190. /**
  191. * Set the given color for consecutive output
  192. *
  193. * @param string $color one of the supported color names
  194. * @throws DokuCLI_Exception
  195. */
  196. public function set($color) {
  197. if(!$this->enabled) return;
  198. if(!isset($this->colors[$color])) throw new DokuCLI_Exception("No such color $color");
  199. echo $this->colors[$color];
  200. }
  201. /**
  202. * reset the terminal color
  203. */
  204. public function reset() {
  205. $this->set('reset');
  206. }
  207. }
  208. /**
  209. * Class DokuCLI_Options
  210. *
  211. * Parses command line options passed to the CLI script. Allows CLI scripts to easily register all accepted options and
  212. * commands and even generates a help text from this setup.
  213. *
  214. * @author Andreas Gohr <andi@splitbrain.org>
  215. */
  216. class DokuCLI_Options {
  217. /** @var array keeps the list of options to parse */
  218. protected $setup;
  219. /** @var array store parsed options */
  220. protected $options = array();
  221. /** @var string current parsed command if any */
  222. protected $command = '';
  223. /** @var array passed non-option arguments */
  224. public $args = array();
  225. /** @var string the executed script */
  226. protected $bin;
  227. /**
  228. * Constructor
  229. */
  230. public function __construct() {
  231. $this->setup = array(
  232. '' => array(
  233. 'opts' => array(),
  234. 'args' => array(),
  235. 'help' => ''
  236. )
  237. ); // default command
  238. $this->args = $this->readPHPArgv();
  239. $this->bin = basename(array_shift($this->args));
  240. $this->options = array();
  241. }
  242. /**
  243. * Sets the help text for the tool itself
  244. *
  245. * @param string $help
  246. */
  247. public function setHelp($help) {
  248. $this->setup['']['help'] = $help;
  249. }
  250. /**
  251. * Register the names of arguments for help generation and number checking
  252. *
  253. * This has to be called in the order arguments are expected
  254. *
  255. * @param string $arg argument name (just for help)
  256. * @param string $help help text
  257. * @param bool $required is this a required argument
  258. * @param string $command if theses apply to a sub command only
  259. * @throws DokuCLI_Exception
  260. */
  261. public function registerArgument($arg, $help, $required = true, $command = '') {
  262. if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered");
  263. $this->setup[$command]['args'][] = array(
  264. 'name' => $arg,
  265. 'help' => $help,
  266. 'required' => $required
  267. );
  268. }
  269. /**
  270. * This registers a sub command
  271. *
  272. * Sub commands have their own options and use their own function (not main()).
  273. *
  274. * @param string $command
  275. * @param string $help
  276. * @throws DokuCLI_Exception
  277. */
  278. public function registerCommand($command, $help) {
  279. if(isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command already registered");
  280. $this->setup[$command] = array(
  281. 'opts' => array(),
  282. 'args' => array(),
  283. 'help' => $help
  284. );
  285. }
  286. /**
  287. * Register an option for option parsing and help generation
  288. *
  289. * @param string $long multi character option (specified with --)
  290. * @param string $help help text for this option
  291. * @param string|null $short one character option (specified with -)
  292. * @param bool|string $needsarg does this option require an argument? give it a name here
  293. * @param string $command what command does this option apply to
  294. * @throws DokuCLI_Exception
  295. */
  296. public function registerOption($long, $help, $short = null, $needsarg = false, $command = '') {
  297. if(!isset($this->setup[$command])) throw new DokuCLI_Exception("Command $command not registered");
  298. $this->setup[$command]['opts'][$long] = array(
  299. 'needsarg' => $needsarg,
  300. 'help' => $help,
  301. 'short' => $short
  302. );
  303. if($short) {
  304. if(strlen($short) > 1) throw new DokuCLI_Exception("Short options should be exactly one ASCII character");
  305. $this->setup[$command]['short'][$short] = $long;
  306. }
  307. }
  308. /**
  309. * Checks the actual number of arguments against the required number
  310. *
  311. * Throws an exception if arguments are missing. Called from parseOptions()
  312. *
  313. * @throws DokuCLI_Exception
  314. */
  315. public function checkArguments() {
  316. $argc = count($this->args);
  317. $req = 0;
  318. foreach($this->setup[$this->command]['args'] as $arg) {
  319. if(!$arg['required']) break; // last required arguments seen
  320. $req++;
  321. }
  322. if($req > $argc) throw new DokuCLI_Exception("Not enough arguments", DokuCLI_Exception::E_OPT_ARG_REQUIRED);
  323. }
  324. /**
  325. * Parses the given arguments for known options and command
  326. *
  327. * The given $args array should NOT contain the executed file as first item anymore! The $args
  328. * array is stripped from any options and possible command. All found otions can be accessed via the
  329. * getOpt() function
  330. *
  331. * Note that command options will overwrite any global options with the same name
  332. *
  333. * @throws DokuCLI_Exception
  334. */
  335. public function parseOptions() {
  336. $non_opts = array();
  337. $argc = count($this->args);
  338. for($i = 0; $i < $argc; $i++) {
  339. $arg = $this->args[$i];
  340. // The special element '--' means explicit end of options. Treat the rest of the arguments as non-options
  341. // and end the loop.
  342. if($arg == '--') {
  343. $non_opts = array_merge($non_opts, array_slice($this->args, $i + 1));
  344. break;
  345. }
  346. // '-' is stdin - a normal argument
  347. if($arg == '-') {
  348. $non_opts = array_merge($non_opts, array_slice($this->args, $i));
  349. break;
  350. }
  351. // first non-option
  352. if($arg[0] != '-') {
  353. $non_opts = array_merge($non_opts, array_slice($this->args, $i));
  354. break;
  355. }
  356. // long option
  357. if(strlen($arg) > 1 && $arg[1] == '-') {
  358. list($opt, $val) = explode('=', substr($arg, 2), 2);
  359. if(!isset($this->setup[$this->command]['opts'][$opt])) {
  360. throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT);
  361. }
  362. // argument required?
  363. if($this->setup[$this->command]['opts'][$opt]['needsarg']) {
  364. if(is_null($val) && $i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) {
  365. $val = $this->args[++$i];
  366. }
  367. if(is_null($val)) {
  368. throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED);
  369. }
  370. $this->options[$opt] = $val;
  371. } else {
  372. $this->options[$opt] = true;
  373. }
  374. continue;
  375. }
  376. // short option
  377. $opt = substr($arg, 1);
  378. if(!isset($this->setup[$this->command]['short'][$opt])) {
  379. throw new DokuCLI_Exception("No such option $arg", DokuCLI_Exception::E_UNKNOWN_OPT);
  380. } else {
  381. $opt = $this->setup[$this->command]['short'][$opt]; // store it under long name
  382. }
  383. // argument required?
  384. if($this->setup[$this->command]['opts'][$opt]['needsarg']) {
  385. $val = null;
  386. if($i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) {
  387. $val = $this->args[++$i];
  388. }
  389. if(is_null($val)) {
  390. throw new DokuCLI_Exception("Option $arg requires an argument", DokuCLI_Exception::E_OPT_ARG_REQUIRED);
  391. }
  392. $this->options[$opt] = $val;
  393. } else {
  394. $this->options[$opt] = true;
  395. }
  396. }
  397. // parsing is now done, update args array
  398. $this->args = $non_opts;
  399. // if not done yet, check if first argument is a command and reexecute argument parsing if it is
  400. if(!$this->command && $this->args && isset($this->setup[$this->args[0]])) {
  401. // it is a command!
  402. $this->command = array_shift($this->args);
  403. $this->parseOptions(); // second pass
  404. }
  405. }
  406. /**
  407. * Get the value of the given option
  408. *
  409. * Please note that all options are accessed by their long option names regardless of how they were
  410. * specified on commandline.
  411. *
  412. * Can only be used after parseOptions() has been run
  413. *
  414. * @param string $option
  415. * @param bool|string $default what to return if the option was not set
  416. * @return bool|string
  417. */
  418. public function getOpt($option, $default = false) {
  419. if(isset($this->options[$option])) return $this->options[$option];
  420. return $default;
  421. }
  422. /**
  423. * Return the found command if any
  424. *
  425. * @return string
  426. */
  427. public function getCmd() {
  428. return $this->command;
  429. }
  430. /**
  431. * Builds a help screen from the available options. You may want to call it from -h or on error
  432. *
  433. * @return string
  434. */
  435. public function help() {
  436. $text = '';
  437. $hascommands = (count($this->setup) > 1);
  438. foreach($this->setup as $command => $config) {
  439. $hasopts = (bool) $this->setup[$command]['opts'];
  440. $hasargs = (bool) $this->setup[$command]['args'];
  441. if(!$command) {
  442. $text .= 'USAGE: '.$this->bin;
  443. } else {
  444. $text .= "\n$command";
  445. }
  446. if($hasopts) $text .= ' <OPTIONS>';
  447. foreach($this->setup[$command]['args'] as $arg) {
  448. if($arg['required']) {
  449. $text .= ' <'.$arg['name'].'>';
  450. } else {
  451. $text .= ' [<'.$arg['name'].'>]';
  452. }
  453. }
  454. $text .= "\n";
  455. if($this->setup[$command]['help']) {
  456. $text .= "\n";
  457. $text .= $this->tableFormat(
  458. array(2, 72),
  459. array('', $this->setup[$command]['help']."\n")
  460. );
  461. }
  462. if($hasopts) {
  463. $text .= "\n OPTIONS\n\n";
  464. foreach($this->setup[$command]['opts'] as $long => $opt) {
  465. $name = '';
  466. if($opt['short']) {
  467. $name .= '-'.$opt['short'];
  468. if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>';
  469. $name .= ', ';
  470. }
  471. $name .= "--$long";
  472. if($opt['needsarg']) $name .= ' <'.$opt['needsarg'].'>';
  473. $text .= $this->tableFormat(
  474. array(2, 20, 52),
  475. array('', $name, $opt['help'])
  476. );
  477. $text .= "\n";
  478. }
  479. }
  480. if($hasargs) {
  481. $text .= "\n";
  482. foreach($this->setup[$command]['args'] as $arg) {
  483. $name = '<'.$arg['name'].'>';
  484. $text .= $this->tableFormat(
  485. array(2, 20, 52),
  486. array('', $name, $arg['help'])
  487. );
  488. }
  489. }
  490. if($command == '' && $hascommands) {
  491. $text .= "\nThis tool accepts a command as first parameter as outlined below:\n";
  492. }
  493. }
  494. return $text;
  495. }
  496. /**
  497. * Safely read the $argv PHP array across different PHP configurations.
  498. * Will take care on register_globals and register_argc_argv ini directives
  499. *
  500. * @throws DokuCLI_Exception
  501. * @return array the $argv PHP array or PEAR error if not registered
  502. */
  503. private function readPHPArgv() {
  504. global $argv;
  505. if(!is_array($argv)) {
  506. if(!@is_array($_SERVER['argv'])) {
  507. if(!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
  508. throw new DokuCLI_Exception(
  509. "Could not read cmd args (register_argc_argv=Off?)",
  510. DOKU_CLI_OPTS_ARG_READ
  511. );
  512. }
  513. return $GLOBALS['HTTP_SERVER_VARS']['argv'];
  514. }
  515. return $_SERVER['argv'];
  516. }
  517. return $argv;
  518. }
  519. /**
  520. * Displays text in multiple word wrapped columns
  521. *
  522. * @param int[] $widths list of column widths (in characters)
  523. * @param string[] $texts list of texts for each column
  524. * @return string
  525. */
  526. private function tableFormat($widths, $texts) {
  527. $wrapped = array();
  528. $maxlen = 0;
  529. foreach($widths as $col => $width) {
  530. $wrapped[$col] = explode("\n", wordwrap($texts[$col], $width - 1, "\n", true)); // -1 char border
  531. $len = count($wrapped[$col]);
  532. if($len > $maxlen) $maxlen = $len;
  533. }
  534. $out = '';
  535. for($i = 0; $i < $maxlen; $i++) {
  536. foreach($widths as $col => $width) {
  537. if(isset($wrapped[$col][$i])) {
  538. $val = $wrapped[$col][$i];
  539. } else {
  540. $val = '';
  541. }
  542. $out .= sprintf('%-'.$width.'s', $val);
  543. }
  544. $out .= "\n";
  545. }
  546. return $out;
  547. }
  548. }
  549. /**
  550. * Class DokuCLI_Exception
  551. *
  552. * The code is used as exit code for the CLI tool. This should probably be extended. Many cases just fall back to the
  553. * E_ANY code.
  554. *
  555. * @author Andreas Gohr <andi@splitbrain.org>
  556. */
  557. class DokuCLI_Exception extends Exception {
  558. const E_ANY = -1; // no error code specified
  559. const E_UNKNOWN_OPT = 1; //Unrecognized option
  560. const E_OPT_ARG_REQUIRED = 2; //Option requires argument
  561. const E_OPT_ARG_DENIED = 3; //Option not allowed argument
  562. const E_OPT_ABIGUOUS = 4; //Option abiguous
  563. const E_ARG_READ = 5; //Could not read argv
  564. /**
  565. * @param string $message The Exception message to throw.
  566. * @param int $code The Exception code
  567. * @param Exception $previous The previous exception used for the exception chaining.
  568. */
  569. public function __construct($message = "", $code = 0, Exception $previous = null) {
  570. if(!$code) $code = DokuCLI_Exception::E_ANY;
  571. parent::__construct($message, $code, $previous);
  572. }
  573. }