summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Cli
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Cli')
-rw-r--r--library/Icinga/Cli/AnsiScreen.php122
-rw-r--r--library/Icinga/Cli/Command.php216
-rw-r--r--library/Icinga/Cli/Documentation.php167
-rw-r--r--library/Icinga/Cli/Documentation/CommentParser.php85
-rw-r--r--library/Icinga/Cli/Loader.php501
-rw-r--r--library/Icinga/Cli/Params.php320
-rw-r--r--library/Icinga/Cli/Screen.php106
7 files changed, 1517 insertions, 0 deletions
diff --git a/library/Icinga/Cli/AnsiScreen.php b/library/Icinga/Cli/AnsiScreen.php
new file mode 100644
index 0000000..2780f08
--- /dev/null
+++ b/library/Icinga/Cli/AnsiScreen.php
@@ -0,0 +1,122 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Cli\Screen;
+use Icinga\Exception\IcingaException;
+
+// @see http://en.wikipedia.org/wiki/ANSI_escape_code
+
+class AnsiScreen extends Screen
+{
+ protected $fgColors = array(
+ 'black' => '30',
+ 'darkgray' => '1;30',
+ 'red' => '31',
+ 'lightred' => '1;31',
+ 'green' => '32',
+ 'lightgreen' => '1;32',
+ 'brown' => '33',
+ 'yellow' => '1;33',
+ 'blue' => '34',
+ 'lightblue' => '1;34',
+ 'purple' => '35',
+ 'lightpurple' => '1;35',
+ 'cyan' => '36',
+ 'lightcyan' => '1;36',
+ 'lightgray' => '37',
+ 'white' => '1;37',
+ );
+
+ protected $bgColors = array(
+ 'black' => '40',
+ 'red' => '41',
+ 'green' => '42',
+ 'brown' => '43',
+ 'blue' => '44',
+ 'purple' => '45',
+ 'cyan' => '46',
+ 'lightgray' => '47',
+ );
+
+ public function strlen($string)
+ {
+ return strlen($this->stripAnsiCodes($string));
+ }
+
+ public function stripAnsiCodes($string)
+ {
+ return preg_replace('/\e\[?.*?[\@-~]/', '', $string);
+ }
+
+ public function clear()
+ {
+ return "\033[2J" // Clear the whole screen
+ . "\033[1;1H" // Move the cursor to row 1, column 1
+ . "\033[1S"; // Scroll whole page up by 1 line (why?)
+ }
+
+ public function underline($text)
+ {
+ return "\033[4m"
+ . $text
+ . "\033[0m"; // Reset color codes
+ }
+
+ public function colorize($text, $fgColor = null, $bgColor = null)
+ {
+ return $this->startColor($fgColor, $bgColor)
+ . $text
+ . "\033[0m"; // Reset color codes
+ }
+
+ protected function fgColor($color)
+ {
+ if (! array_key_exists($color, $this->fgColors)) {
+ throw new IcingaException(
+ 'There is no such foreground color: %s',
+ $color
+ );
+ }
+ return $this->fgColors[$color];
+ }
+
+ protected function bgColor($color)
+ {
+ if (! array_key_exists($color, $this->bgColors)) {
+ throw new IcingaException(
+ 'There is no such background color: %s',
+ $color
+ );
+ }
+ return $this->bgColors[$color];
+ }
+
+ protected function startColor($fgColor = null, $bgColor = null)
+ {
+ $escape = "ESC[";
+ $parts = array();
+ if ($fgColor !== null
+ && $bgColor !== null
+ && ! array_key_exists($bgColor, $this->bgColors)
+ && array_key_exists($bgColor, $this->fgColors)
+ && array_key_exists($fgColor, $this->bgColors)
+ ) {
+ $parts[] = '7'; // reverse video, negative image
+ $parts[] = $this->bgColor($fgColor);
+ $parts[] = $this->fgColor($bgColor);
+ } else {
+ if ($fgColor !== null) {
+ $parts[] = $this->fgColor($fgColor);
+ }
+ if ($bgColor !== null) {
+ $parts[] = $this->bgColor($bgColor);
+ }
+ }
+ if (empty($parts)) {
+ return '';
+ }
+ return "\033[" . implode(';', $parts) . 'm';
+ }
+}
diff --git a/library/Icinga/Cli/Command.php b/library/Icinga/Cli/Command.php
new file mode 100644
index 0000000..7fd5f87
--- /dev/null
+++ b/library/Icinga/Cli/Command.php
@@ -0,0 +1,216 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Application\ApplicationBootstrap as App;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotReadableError;
+use ipl\I18n\Translation;
+
+abstract class Command
+{
+ use Translation;
+
+ protected $app;
+ protected $docs;
+
+ /**
+ * @var Params
+ */
+ protected $params;
+ protected $screen;
+
+ /**
+ * Whether the --verbose switch is given and thus the set log level INFO is
+ *
+ * @var bool
+ */
+ protected $isVerbose;
+
+ /**
+ * Whether the --debug switch is given and thus the set log level DEBUG is
+ *
+ * @var bool
+ */
+ protected $isDebugging;
+
+ protected $moduleName;
+ protected $commandName;
+ protected $actionName;
+
+ protected $config;
+
+ protected $configs;
+
+ protected $defaultActionName = 'default';
+
+ /** @var bool Whether to automatically load enabled modules */
+ protected $loadEnabledModules = true;
+
+ /** @var bool Whether to enable trace for the CLI commands */
+ protected $trace = false;
+
+ public function __construct(App $app, $moduleName, $commandName, $actionName, $initialize = true)
+ {
+ $this->app = $app;
+ $this->moduleName = $moduleName;
+ $this->commandName = $commandName;
+ $this->actionName = $actionName;
+ $this->params = $app->getParams();
+ $this->screen = Screen::instance();
+ $this->trace = $this->params->shift('trace', false);
+ $this->isVerbose = $this->params->shift('verbose', false);
+ $this->isDebugging = $this->params->shift('debug', false);
+ $this->configs = [];
+
+ $this->translationDomain = $moduleName ?: 'icinga';
+
+ if ($this->loadEnabledModules) {
+ try {
+ $app->getModuleManager()->loadEnabledModules();
+ } catch (NotReadableError $e) {
+ Logger::error(new IcingaException('Cannot load enabled modules. An exception was thrown:', $e));
+ }
+ }
+
+ if ($initialize) {
+ $this->init();
+ }
+ }
+
+ public function Config($file = null)
+ {
+ if ($this->isModule()) {
+ return $this->getModuleConfig($file);
+ } else {
+ return $this->getMainConfig($file);
+ }
+ }
+
+ private function getModuleConfig($file = null)
+ {
+ if ($file === null) {
+ if ($this->config === null) {
+ $this->config = Config::module($this->moduleName);
+ }
+ return $this->config;
+ } else {
+ if (! array_key_exists($file, $this->configs)) {
+ $this->configs[$file] = Config::module($this->moduleName, $file);
+ }
+ return $this->configs[$file];
+ }
+ }
+
+ private function getMainConfig($file = null)
+ {
+ if ($file === null) {
+ if ($this->config === null) {
+ $this->config = Config::app();
+ }
+ return $this->config;
+ } else {
+ if (! array_key_exists($file, $this->configs)) {
+ $this->configs[$file] = Config::app($file);
+ }
+ return $this->configs[$file];
+ }
+ return $this->config;
+ }
+
+ public function isModule()
+ {
+ return substr(get_class($this), 0, 14) === 'Icinga\\Module\\';
+ }
+
+ public function setParams(Params $params)
+ {
+ $this->params = $params;
+ }
+
+ public function hasRemainingParams()
+ {
+ return $this->params->count() > 0;
+ }
+
+ public function showTrace()
+ {
+ return $this->trace;
+ }
+
+ /**
+ * @param $msg
+ *
+ * @throws IcingaException
+ */
+ public function fail($msg)
+ {
+ throw new IcingaException('%s', $msg);
+ }
+
+ public function getDefaultActionName()
+ {
+ return $this->defaultActionName;
+ }
+
+ /**
+ * Get {@link moduleName}
+ *
+ * @return string
+ */
+ public function getModuleName()
+ {
+ return $this->moduleName;
+ }
+
+ public function hasDefaultActionName()
+ {
+ return $this->hasActionName($this->defaultActionName);
+ }
+
+ public function hasActionName($name)
+ {
+ $actions = $this->listActions();
+ return in_array($name, $actions);
+ }
+
+ public function listActions()
+ {
+ $actions = array();
+ foreach (get_class_methods($this) as $method) {
+ if (preg_match('~^([A-Za-z0-9]+)Action$~', $method, $m)) {
+ $actions[] = $m[1];
+ }
+ }
+ sort($actions);
+ return $actions;
+ }
+
+ public function docs()
+ {
+ if ($this->docs === null) {
+ $this->docs = new Documentation($this->app);
+ }
+ return $this->docs;
+ }
+
+ public function showUsage($action = null)
+ {
+ if ($action === null) {
+ $action = $this->actionName;
+ }
+ echo $this->docs()->usage(
+ $this->moduleName,
+ $this->commandName,
+ $action
+ );
+ return false;
+ }
+
+ public function init()
+ {
+ }
+}
diff --git a/library/Icinga/Cli/Documentation.php b/library/Icinga/Cli/Documentation.php
new file mode 100644
index 0000000..6881467
--- /dev/null
+++ b/library/Icinga/Cli/Documentation.php
@@ -0,0 +1,167 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Application\ApplicationBootstrap as App;
+use Icinga\Cli\Documentation\CommentParser;
+use ReflectionClass;
+use ReflectionMethod;
+
+class Documentation
+{
+ protected $icinga;
+
+ protected $app;
+
+ protected $loader;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ $this->loader = $app->cliLoader();
+ }
+
+ public function usage($module = null, $command = null, $action = null)
+ {
+ if ($module) {
+ $module = $this->loader->resolveModuleName($module);
+ return $this->moduleUsage($module, $command, $action);
+ }
+ if ($command) {
+ $command = $this->loader->resolveCommandName($command);
+ return $this->commandUsage($command, $action);
+ }
+ return $this->globalUsage();
+ }
+
+ public function globalUsage()
+ {
+ $d = "USAGE: icingacli [module] <command> [action] [options]\n\n"
+ . "Available commands:\n\n";
+ foreach ($this->loader->listCommands() as $command) {
+ if ($command !== 'autocomplete') {
+ $obj = $this->loader->getCommandInstance($command);
+ $d .= sprintf(
+ " %-14s %s\n",
+ $command,
+ $this->getClassTitle($obj)
+ );
+ }
+ }
+ $d .= "\nAvailable modules:\n\n";
+ foreach ($this->loader->listModules() as $module) {
+ $d .= ' ' . $module . "\n";
+ }
+ $d .= "\nGlobal options:\n\n"
+ . " --log [t] Log to <t>, either stderr, file or syslog (default: stderr)\n"
+ . " --log-path <f> Which file to log into in case of --log file\n"
+ . " --verbose Be verbose\n"
+ . " --debug Show debug output\n"
+ . " --help Show help\n"
+ . " --benchmark Show benchmark summary\n"
+ . " --watch [s] Refresh output every <s> seconds (default: 5)\n"
+ . " --version Shows version of Icinga Web 2, loaded modules and PHP\n"
+ ;
+ $d .= "\nShow help on a specific command : icingacli help <command>"
+ . "\nShow help on a specific module : icingacli help <module>"
+ . "\n";
+ return $d;
+ }
+
+ public function moduleUsage($module, $command = null, $action = null)
+ {
+ $commands = $this->loader->listModuleCommands($module);
+
+ if (empty($commands)) {
+ return "The '$module' module does not provide any CLI commands\n";
+ }
+ $d = '';
+ $obj = null;
+ if ($command) {
+ $obj = $this->loader->getModuleCommandInstance($module, $command);
+ }
+ if ($command === null) {
+ $d = "USAGE: icingacli $module <command> [<action>] [options]\n\n"
+ . "Available commands:\n\n";
+ foreach ($commands as $command) {
+ $d .= ' ' . $command . "\n";
+ }
+ $d .= "\nShow help on a specific command: icingacli help $module <command>\n";
+ } elseif ($action === null) {
+ $d .= $this->showCommandActions($obj, $command);
+ } else {
+ $action = $this->loader->resolveObjectActionName($obj, $action);
+ $d .= $this->getMethodDocumentation($obj, $action);
+ }
+ return $d;
+ }
+
+ /**
+ * @param Command $command
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function showCommandActions($command, $name)
+ {
+ $actions = $command->listActions();
+ $d = $this->getClassDocumentation($command)
+ . "Available actions:\n\n";
+ foreach ($actions as $action) {
+ $d .= sprintf(
+ " %-14s %s\n",
+ $action,
+ $this->getMethodTitle($command, $action)
+ );
+ }
+ $d .= "\nShow help on a specific action: icingacli help ";
+ if ($command->isModule()) {
+ $d .= $command->getModuleName() . ' ';
+ }
+ $d .= "$name <action>\n";
+ return $d;
+ }
+
+ public function commandUsage($command, $action = null)
+ {
+ $obj = $this->loader->getCommandInstance($command);
+ $action = $this->loader->resolveObjectActionName($obj, $action);
+
+ $d = "\n";
+ if ($action) {
+ $d .= $this->getMethodDocumentation($obj, $action);
+ } else {
+ $d .= $this->showCommandActions($obj, $command);
+ }
+ return $d;
+ }
+
+ protected function getClassTitle($class)
+ {
+ $ref = new ReflectionClass($class);
+ $comment = new CommentParser($ref->getDocComment());
+ return $comment->getTitle();
+ }
+
+ protected function getClassDocumentation($class)
+ {
+ $ref = new ReflectionClass($class);
+ $comment = new CommentParser($ref->getDocComment());
+ return $comment->dump();
+ }
+
+ protected function getMethodTitle($class, $method)
+ {
+ $ref = new ReflectionMethod($class, $method . 'Action');
+ $comment = new CommentParser($ref->getDocComment());
+ return $comment->getTitle();
+ }
+
+ protected function getMethodDocumentation($class, $method)
+ {
+ $ref = new ReflectionMethod($class, $method . 'Action');
+ $comment = new CommentParser($ref->getDocComment());
+ return $comment->dump();
+ }
+}
diff --git a/library/Icinga/Cli/Documentation/CommentParser.php b/library/Icinga/Cli/Documentation/CommentParser.php
new file mode 100644
index 0000000..4104848
--- /dev/null
+++ b/library/Icinga/Cli/Documentation/CommentParser.php
@@ -0,0 +1,85 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli\Documentation;
+
+use Icinga\Cli\Screen;
+
+class CommentParser
+{
+ protected $raw;
+ protected $plain;
+ protected $title;
+ protected $paragraphs = array();
+
+ public function __construct($raw)
+ {
+ $this->raw = $raw;
+ if ($raw) {
+ $this->parse();
+ }
+ }
+
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ protected function parse()
+ {
+ $plain = $this->raw;
+
+ // Strip comment start /**
+ $plain = preg_replace('~^/\s*\*\*\n~s', '', $plain);
+
+ // Strip comment end */
+ $plain = preg_replace('~\n\s*\*/\s*~s', "\n", $plain);
+ $p = null;
+ foreach (preg_split('~\n~', $plain) as $line) {
+ // Strip * at line start
+ $line = preg_replace('~^\s*\*\s?~', '', $line);
+ $line = rtrim($line);
+ if ($this->title === null) {
+ $this->title = $line;
+ continue;
+ }
+ if ($p === null && empty($this->paragraphs)) {
+ $p = & $this->paragraphs[];
+ }
+
+ if ($line === '') {
+ if ($p !== null) {
+ $p = & $this->paragraphs[];
+ }
+ continue;
+ }
+ if ($p === null) {
+ $p = $line;
+ } else {
+ if (substr($line, 0, 2) === ' ') {
+ $p .= "\n" . $line;
+ } else {
+ $p .= ' ' . $line;
+ }
+ }
+ }
+ if ($p === null) {
+ array_pop($this->paragraphs);
+ }
+ }
+
+ public function dump()
+ {
+ if ($this->title) {
+ $res = $this->title . "\n" . str_repeat('=', strlen($this->title)) . "\n\n";
+ } else {
+ $res = '';
+ }
+
+ foreach ($this->paragraphs as $p) {
+ $res .= wordwrap($p, Screen::instance()->getColumns()) . "\n\n";
+ }
+
+ return $res;
+ }
+}
diff --git a/library/Icinga/Cli/Loader.php b/library/Icinga/Cli/Loader.php
new file mode 100644
index 0000000..5e63f3f
--- /dev/null
+++ b/library/Icinga/Cli/Loader.php
@@ -0,0 +1,501 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Application\ApplicationBootstrap as App;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Cli\Params;
+use Icinga\Cli\Screen;
+use Icinga\Cli\Command;
+use Icinga\Cli\Documentation;
+use Exception;
+
+/**
+ *
+ */
+class Loader
+{
+ protected $app;
+
+ protected $docs;
+
+ protected $commands;
+
+ protected $modules;
+
+ protected $moduleCommands = array();
+
+ protected $coreAppDir;
+
+ protected $screen;
+
+ protected $moduleName;
+
+ protected $commandName;
+
+ protected $actionName; // Should this better be moved to the Command?
+
+ /**
+ * [$command] = $class;
+ */
+ protected $commandClassMap = array();
+
+ /**
+ * [$command] = $file;
+ */
+ protected $commandFileMap = array();
+
+ /**
+ * [$module][$command] = $class;
+ */
+ protected $moduleClassMap = array();
+
+ /**
+ * [$module][$command] = $file;
+ */
+ protected $moduleFileMap = array();
+
+ protected $commandInstances = array();
+
+ protected $moduleInstances = array();
+
+ protected $lastSuggestions = array();
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ $this->coreAppDir = $app->getApplicationDir('clicommands');
+ }
+
+ /**
+ * Screen shortcut
+ *
+ * @return Screen
+ */
+ protected function screen()
+ {
+ if ($this->screen === null) {
+ $this->screen = Screen::instance(STDERR);
+ }
+
+ return $this->screen;
+ }
+
+ /**
+ * Documentation shortcut
+ *
+ * @return Documentation
+ */
+ protected function docs()
+ {
+ if ($this->docs === null) {
+ $this->docs = new Documentation($this->app);
+ }
+ return $this->docs;
+ }
+
+ /**
+ * Show given message and exit
+ *
+ * @param string $msg message to show
+ */
+ public function fail($msg)
+ {
+ fprintf(STDERR, "%s: %s\n", $this->screen()->colorize('ERROR', 'red'), $msg);
+ exit(1);
+ }
+
+ public function getModuleName()
+ {
+ return $this->moduleName;
+ }
+
+ public function setModuleName($name)
+ {
+ $this->moduleName = $name;
+ return $this;
+ }
+
+ public function getCommandName()
+ {
+ return $this->commandName;
+ }
+
+ public function getActionName()
+ {
+ return $this->actionName;
+ }
+
+ public function getCommandInstance($command)
+ {
+ if (! array_key_exists($command, $this->commandInstances)) {
+ $this->assertCommandExists($command);
+ require_once $this->commandFileMap[$command];
+ $className = $this->commandClassMap[$command];
+ $this->commandInstances[$command] = new $className(
+ $this->app,
+ null,
+ $command,
+ null,
+ false
+ );
+ }
+ return $this->commandInstances[$command];
+ }
+
+ public function getModuleCommandInstance($module, $command)
+ {
+ if (! array_key_exists($command, $this->moduleInstances[$module])) {
+ $this->assertModuleCommandExists($module, $command);
+ require_once $this->moduleFileMap[$module][$command];
+ $className = $this->moduleClassMap[$module][$command];
+ $this->moduleInstances[$module][$command] = new $className(
+ $this->app,
+ $module,
+ $command,
+ null,
+ false
+ );
+ }
+ return $this->moduleInstances[$module][$command];
+ }
+
+ public function getLastSuggestions()
+ {
+ return $this->lastSuggestions;
+ }
+
+ public function showLastSuggestions()
+ {
+ if (! empty($this->lastSuggestions)) {
+ foreach ($this->lastSuggestions as & $s) {
+ $s = $this->screen()->colorize($s, 'lightblue');
+ }
+ fprintf(
+ STDERR,
+ "Did you mean %s?\n",
+ implode(" or ", $this->lastSuggestions)
+ );
+ }
+ }
+
+ public function parseParams(Params $params = null)
+ {
+ if ($params === null) {
+ $params = $this->app->getParams();
+ }
+
+ $first = null;
+ if ($this->moduleName === null) {
+ $first = $params->shift();
+ if (! $first) {
+ return;
+ }
+ $found = $this->resolveName($first);
+ } else {
+ $found = $this->moduleName;
+ }
+ if (! $found) {
+ $msg = "There is no such module or command: '$first'";
+ fprintf(STDERR, "%s: %s\n", $this->screen()->colorize('ERROR', 'red'), $msg);
+ $this->showLastSuggestions();
+ fwrite(STDERR, "\n");
+ }
+
+ $obj = null;
+ if ($this->hasCommand($found)) {
+ $this->commandName = $found;
+ $obj = $this->getCommandInstance($this->commandName);
+ } elseif ($this->hasModule($found)) {
+ $this->moduleName = $found;
+ $command = $this->resolveModuleCommandName($found, $params->shift());
+ if ($command) {
+ $this->commandName = $command;
+ $obj = $this->getModuleCommandInstance(
+ $this->moduleName,
+ $this->commandName
+ );
+ }
+ }
+ if ($obj !== null) {
+ $action = $this->resolveObjectActionName(
+ $obj,
+ $params->getStandalone()
+ );
+ if ($obj->hasActionName($action)) {
+ $this->actionName = $action;
+ $params->shift();
+ } elseif ($obj->hasDefaultActionName()) {
+ $this->actionName = $obj->getDefaultActionName();
+ }
+ }
+ return $this;
+ }
+
+ public function handleParams(Params $params = null)
+ {
+ $this->parseParams($params);
+ $this->dispatch();
+ }
+
+ public function dispatch(Params $overrideParams = null)
+ {
+ if ($this->commandName === null) {
+ fwrite(STDERR, $this->docs()->usage($this->moduleName));
+ return false;
+ } elseif ($this->actionName === null) {
+ fwrite(STDERR, $this->docs()->usage($this->moduleName, $this->commandName));
+ return false;
+ }
+
+ $obj = null;
+ try {
+ if ($this->moduleName) {
+ $this->app->getModuleManager()->loadModule($this->moduleName);
+ $obj = $this->getModuleCommandInstance(
+ $this->moduleName,
+ $this->commandName
+ );
+ } else {
+ $obj = $this->getCommandInstance($this->commandName);
+ }
+ if ($overrideParams !== null) {
+ $obj->setParams($overrideParams);
+ }
+ $obj->init();
+ return $obj->{$this->actionName . 'Action'}();
+ } catch (Exception $e) {
+ if ($obj instanceof Command && $obj->showTrace()) {
+ fwrite(STDERR, $this->formatTrace($e->getTrace()));
+ }
+
+ $this->fail(IcingaException::describe($e));
+ }
+ }
+
+ protected function searchMatch($needle, $haystack)
+ {
+ if ($needle === null) {
+ $needle = '';
+ }
+
+ $this->lastSuggestions = preg_grep(sprintf('/^%s.*$/', preg_quote($needle, '/')), $haystack);
+ $match = array_search($needle, $haystack, true);
+ if (false !== $match) {
+ return $haystack[$match];
+ }
+ if (count($this->lastSuggestions) === 1) {
+ $lastSuggestions = array_values($this->lastSuggestions);
+ return $lastSuggestions[0];
+ }
+ return false;
+ }
+
+ public function resolveName($name)
+ {
+ return $this->searchMatch(
+ $name,
+ array_merge($this->listCommands(), $this->listModules())
+ );
+ }
+
+ public function resolveCommandName($name)
+ {
+ return $this->searchMatch($name, $this->listCommands());
+ }
+
+ public function resolveModuleName($name)
+ {
+ return $this->searchMatch($name, $this->listModules());
+ }
+
+ public function resolveModuleCommandName($module, $name)
+ {
+ return $this->searchMatch($name, $this->listModuleCommands($module));
+ }
+
+ public function resolveObjectActionName($obj, $name)
+ {
+ return $this->searchMatch($name, $obj->listActions());
+ }
+
+ protected function assertModuleExists($module)
+ {
+ if (! $this->hasModule($module)) {
+ throw new ProgrammingError(
+ 'There is no such module: %s',
+ $module
+ );
+ }
+ }
+
+ protected function assertCommandExists($command)
+ {
+ if (! $this->hasCommand($command)) {
+ throw new ProgrammingError(
+ 'There is no such command: %s',
+ $command
+ );
+ }
+ }
+
+ protected function assertModuleCommandExists($module, $command)
+ {
+ $this->assertModuleExists($module);
+ if (! $this->hasModuleCommand($module, $command)) {
+ throw new ProgrammingError(
+ 'The module \'%s\' has no such command: %s',
+ $module,
+ $command
+ );
+ }
+ }
+
+ protected function formatTrace($trace)
+ {
+ $output = array();
+ foreach ($trace as $i => $step) {
+ $object = '';
+ if (isset($step['object']) && is_object($step['object'])) {
+ $object = sprintf('[%s]', get_class($step['object'])) . $step['type'];
+ } elseif (! empty($step['object'])) {
+ $object = (string) $step['object'] . $step['type'];
+ }
+ if (isset($step['args']) && is_array($step['args'])) {
+ foreach ($step['args'] as & $arg) {
+ if (is_object($arg)) {
+ $arg = sprintf('[%s]', get_class($arg));
+ }
+ if (is_string($arg)) {
+ $arg = preg_replace('~\n~', '\n', $arg);
+ if (strlen($arg) > 50) {
+ $arg = substr($arg, 0, 47) . '...';
+ }
+ $arg = "'" . $arg . "'";
+ }
+ if ($arg === null) {
+ $arg = 'NULL';
+ }
+ if (is_bool($arg)) {
+ $arg = $arg ? 'TRUE' : 'FALSE';
+ }
+ }
+ } else {
+ $step['args'] = array();
+ }
+ $args = $step['args'];
+ foreach ($args as & $v) {
+ if (is_array($v)) {
+ $v = var_export($v, 1);
+ } else {
+ $v = (string) $v;
+ }
+ }
+ $output[$i] = sprintf(
+ '#%d %s:%d %s%s(%s)',
+ $i,
+ isset($step['file']) ? preg_replace(
+ '~.+/library/~',
+ 'library/',
+ $step['file']
+ ) : '[unknown file]',
+ isset($step['line']) ? $step['line'] : '0',
+ $object,
+ $step['function'],
+ implode(', ', $args)
+ );
+ }
+ return implode(PHP_EOL, $output) . PHP_EOL;
+ }
+
+ public function hasCommand($name)
+ {
+ return in_array($name, $this->listCommands());
+ }
+
+ public function hasModule($name)
+ {
+ return in_array($name, $this->listModules());
+ }
+
+ public function hasModuleCommand($module, $name)
+ {
+ return in_array($name, $this->listModuleCommands($module));
+ }
+
+ public function listModules()
+ {
+ if ($this->modules === null) {
+ $this->modules = array();
+ try {
+ $this->modules = array_unique(array_merge(
+ $this->app->getModuleManager()->listEnabledModules(),
+ $this->app->getModuleManager()->listLoadedModules()
+ ));
+ } catch (NotReadableError $e) {
+ $this->fail($e->getMessage());
+ }
+ }
+ return $this->modules;
+ }
+
+ protected function retrieveCommandsFromDir($dirname)
+ {
+ $commands = array();
+ if (! @file_exists($dirname) || ! is_readable($dirname)) {
+ return $commands;
+ }
+
+ $base = opendir($dirname);
+ if ($base === false) {
+ return $commands;
+ }
+ while (false !== ($dir = readdir($base))) {
+ if ($dir[0] === '.') {
+ continue;
+ }
+ if (preg_match('~^([A-Za-z0-9]+)Command\.php$~', $dir, $m)) {
+ $cmd = strtolower($m[1]);
+ $commands[] = $cmd;
+ }
+ }
+ closedir($base);
+ sort($commands);
+ return $commands;
+ }
+
+ public function listCommands()
+ {
+ if ($this->commands === null) {
+ $this->commands = array();
+ $ns = 'Icinga\\Clicommands\\';
+ $this->commands = $this->retrieveCommandsFromDir($this->coreAppDir);
+ foreach ($this->commands as $cmd) {
+ $this->commandClassMap[$cmd] = $ns . ucfirst($cmd) . 'Command';
+ $this->commandFileMap[$cmd] = $this->coreAppDir . '/' . ucfirst($cmd) . 'Command.php';
+ }
+ }
+ return $this->commands;
+ }
+
+ public function listModuleCommands($module)
+ {
+ if (! array_key_exists($module, $this->moduleCommands)) {
+ $ns = 'Icinga\\Module\\' . ucfirst($module) . '\\Clicommands\\';
+ $this->assertModuleExists($module);
+ $manager = $this->app->getModuleManager();
+ $manager->loadModule($module);
+ $dir = $manager->getModuleDir($module) . '/application/clicommands';
+ $this->moduleCommands[$module] = $this->retrieveCommandsFromDir($dir);
+ $this->moduleInstances[$module] = array();
+ foreach ($this->moduleCommands[$module] as $cmd) {
+ $this->moduleClassMap[$module][$cmd] = $ns . ucfirst($cmd) . 'Command';
+ $this->moduleFileMap[$module][$cmd] = $dir . '/' . ucfirst($cmd) . 'Command.php';
+ }
+ }
+ return $this->moduleCommands[$module];
+ }
+}
diff --git a/library/Icinga/Cli/Params.php b/library/Icinga/Cli/Params.php
new file mode 100644
index 0000000..463d4ae
--- /dev/null
+++ b/library/Icinga/Cli/Params.php
@@ -0,0 +1,320 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Exception\MissingParameterException;
+
+/**
+ * Params
+ *
+ * A class to ease commandline-option and -argument handling.
+ */
+class Params
+{
+ /**
+ * The name and path of the executable
+ *
+ * @var string
+ */
+ protected $program;
+
+ /**
+ * The arguments
+ *
+ * @var array
+ */
+ protected $standalone = array();
+
+ /**
+ * The options
+ *
+ * @var array
+ */
+ protected $params = array();
+
+ /**
+ * Parse the given commandline and create a new Params object
+ *
+ * @param array $argv The commandline
+ */
+ public function __construct($argv)
+ {
+ $noOptionFlag = false;
+ $this->program = array_shift($argv);
+ for ($i = 0; $i < count($argv); $i++) {
+ if ($argv[$i] === '--') {
+ $noOptionFlag = true;
+ } elseif (!$noOptionFlag && substr($argv[$i], 0, 2) === '--') {
+ $key = substr($argv[$i], 2);
+ $matches = array();
+ if (1 === preg_match(
+ '/(?<!.)([^=]+)=(.*)(?!.)/ms',
+ $key,
+ $matches
+ )) {
+ $this->params[$matches[1]] = $matches[2];
+ } elseif (! isset($argv[$i + 1]) || substr($argv[$i + 1], 0, 2) === '--') {
+ $this->params[$key] = true;
+ } elseif (array_key_exists($key, $this->params)) {
+ if (!is_array($this->params[$key])) {
+ $this->params[$key] = array($this->params[$key]);
+ }
+ $this->params[$key][] = $argv[++$i];
+ } else {
+ $this->params[$key] = $argv[++$i];
+ }
+ } else {
+ $this->standalone[] = $argv[$i];
+ }
+ }
+ }
+
+ /**
+ * Return the value for an argument by position
+ *
+ * @param int $pos The position of the argument
+ * @param mixed $default The default value to return
+ *
+ * @return mixed
+ */
+ public function getStandalone($pos = 0, $default = null)
+ {
+ if (isset($this->standalone[$pos])) {
+ return $this->standalone[$pos];
+ }
+ return $default;
+ }
+
+ /**
+ * Count and return the number of arguments and options
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return count($this->standalone) + count($this->params);
+ }
+
+ /**
+ * Return the options
+ *
+ * @return array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Return the arguments
+ *
+ * @return array
+ */
+ public function getAllStandalone()
+ {
+ return $this->standalone;
+ }
+
+ /**
+ * Support isset() and empty() checks on options
+ *
+ * @param $name
+ *
+ * @return bool
+ */
+ public function __isset($name)
+ {
+ return isset($this->params[$name]);
+ }
+
+ /**
+ * @see Params::get()
+ */
+ public function __get($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * Return whether the given option exists
+ *
+ * @param string $key The option name to check
+ *
+ * @return bool
+ */
+ public function has($key)
+ {
+ return array_key_exists($key, $this->params);
+ }
+
+ /**
+ * Return the value of the given option
+ *
+ * @param string $key The option name
+ * @param mixed $default The default value to return
+ *
+ * @return mixed
+ */
+ public function get($key, $default = null)
+ {
+ if ($this->has($key)) {
+ return $this->params[$key];
+ }
+ return $default;
+ }
+
+ /**
+ * Require a parameter
+ *
+ * @param string $name Name of the parameter
+ * @param bool $strict Whether the parameter's value must not be the empty string
+ *
+ * @return mixed
+ *
+ * @throws MissingParameterException If the parameter was not given
+ */
+ public function getRequired($name, $strict = true)
+ {
+ if ($this->has($name)) {
+ $value = $this->get($name);
+ if (! $strict || strlen($value) > 0) {
+ return $value;
+ }
+ }
+ $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name);
+ $e->setParameter($name);
+ throw $e;
+ }
+
+ /**
+ * Set a value for the given option
+ *
+ * @param string $key The option name
+ * @param mixed $value The value to set
+ *
+ * @return $this
+ */
+ public function set($key, $value)
+ {
+ $this->params[$key] = $value;
+ return $this;
+ }
+
+ /**
+ * Remove a single option or multiple options
+ *
+ * @param string|array $keys The option or options to remove
+ *
+ * @return $this
+ */
+ public function remove($keys = array())
+ {
+ if (! is_array($keys)) {
+ $keys = array($keys);
+ }
+ foreach ($keys as $key) {
+ if (array_key_exists($key, $this->params)) {
+ unset($this->params[$key]);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Return a copy of this object with the given options being removed
+ *
+ * @param string|array $keys The option or options to remove
+ *
+ * @return Params
+ */
+ public function without($keys = array())
+ {
+ $params = clone($this);
+ return $params->remove($keys);
+ }
+
+ /**
+ * Remove and return the value of the given option
+ *
+ * Called multiple times for an option with multiple values returns
+ * them one by one in case the default is not an array.
+ *
+ * @param string $key The option name
+ * @param mixed $default The default value to return
+ *
+ * @return mixed
+ */
+ public function shift($key = null, $default = null)
+ {
+ if ($key === null) {
+ if (count($this->standalone) > 0) {
+ return array_shift($this->standalone);
+ }
+ return $default;
+ }
+ $result = $this->get($key, $default);
+ if (is_array($result) && !is_array($default)) {
+ $result = array_shift($result) || $default;
+ if ($result === $default) {
+ $this->remove($key);
+ }
+ } else {
+ $this->remove($key);
+ }
+ return $result;
+ }
+
+ /**
+ * Require and remove a parameter
+ *
+ * @param string $name Name of the parameter
+ * @param bool $strict Whether the parameter's value must not be the empty string
+ *
+ * @return mixed
+ *
+ * @throws MissingParameterException If the parameter was not given
+ */
+ public function shiftRequired($name, $strict = true)
+ {
+ if ($this->has($name)) {
+ $value = $this->get($name);
+ if (! $strict || strlen($value) > 0) {
+ $this->shift($name);
+ return $value;
+ }
+ }
+ $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name);
+ $e->setParameter($name);
+ throw $e;
+ }
+
+ /**
+ * Put the given value onto the argument stack
+ *
+ * @param mixed $key The argument
+ *
+ * @return $this
+ */
+ public function unshift($key)
+ {
+ array_unshift($this->standalone, $key);
+ return $this;
+ }
+
+ /**
+ * Parse the given commandline
+ *
+ * @param array $argv The commandline to parse
+ *
+ * @return Params
+ */
+ public static function parse($argv = null)
+ {
+ if ($argv === null) {
+ $argv = $GLOBALS['argv'];
+ }
+ $params = new self($argv);
+ return $params;
+ }
+}
diff --git a/library/Icinga/Cli/Screen.php b/library/Icinga/Cli/Screen.php
new file mode 100644
index 0000000..4ffad72
--- /dev/null
+++ b/library/Icinga/Cli/Screen.php
@@ -0,0 +1,106 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Cli\AnsiScreen;
+
+class Screen
+{
+ protected static $instances = [];
+
+ protected $isUtf8;
+
+ public function getColumns()
+ {
+ $cols = (int) getenv('COLUMNS');
+ if (! $cols) {
+ // stty -a ?
+ $cols = (int) exec('tput cols');
+ }
+ if (! $cols) {
+ $cols = 80;
+ }
+ return $cols;
+ }
+
+ public function getRows()
+ {
+ $rows = (int) getenv('ROWS');
+ if (! $rows) {
+ // stty -a ?
+ $rows = (int) exec('tput lines');
+ }
+ if (! $rows) {
+ $rows = 25;
+ }
+ return $rows;
+ }
+
+ public function strlen($string)
+ {
+ return strlen($string);
+ }
+
+ public function newlines($count = 1)
+ {
+ return str_repeat("\n", $count);
+ }
+
+ public function center($txt)
+ {
+ $len = $this->strlen($txt);
+ $width = floor(($this->getColumns() + $len) / 2) - $len;
+ return str_repeat(' ', $width) . $txt;
+ }
+
+ public function hasUtf8()
+ {
+ if ($this->isUtf8 === null) {
+ // null should equal 0 here, however seems to equal '' on some systems:
+ $current = setlocale(LC_ALL, 0);
+
+ $parts = preg_split('/;/', $current);
+ $lc_parts = array();
+ foreach ($parts as $part) {
+ if (strpos($part, '=') === false) {
+ continue;
+ }
+ list($key, $val) = preg_split('/=/', $part, 2);
+ $lc_parts[$key] = $val;
+ }
+
+ $this->isUtf8 = array_key_exists('LC_CTYPE', $lc_parts)
+ && preg_match('~\.UTF-8$~i', $lc_parts['LC_CTYPE']);
+ }
+ return $this->isUtf8;
+ }
+
+ public function clear()
+ {
+ return "\n";
+ }
+
+ public function underline($text)
+ {
+ return $text;
+ }
+
+ public function colorize($text, $fgColor = null, $bgColor = null)
+ {
+ return $text;
+ }
+
+ public static function instance($output = STDOUT)
+ {
+ if (! isset(self::$instances[(int) $output])) {
+ if (function_exists('posix_isatty') && posix_isatty($output)) {
+ self::$instances[(int) $output] = new AnsiScreen();
+ } else {
+ self::$instances[(int) $output] = new Screen();
+ }
+ }
+
+ return self::$instances[(int) $output];
+ }
+}