diff options
Diffstat (limited to 'library/Icinga/Cli')
-rw-r--r-- | library/Icinga/Cli/AnsiScreen.php | 122 | ||||
-rw-r--r-- | library/Icinga/Cli/Command.php | 208 | ||||
-rw-r--r-- | library/Icinga/Cli/Documentation.php | 162 | ||||
-rw-r--r-- | library/Icinga/Cli/Documentation/CommentParser.php | 85 | ||||
-rw-r--r-- | library/Icinga/Cli/Loader.php | 499 | ||||
-rw-r--r-- | library/Icinga/Cli/Params.php | 320 | ||||
-rw-r--r-- | library/Icinga/Cli/Screen.php | 106 |
7 files changed, 1502 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..d1a1986 --- /dev/null +++ b/library/Icinga/Cli/Command.php @@ -0,0 +1,208 @@ +<?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; + + 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; + } + + 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..111745e --- /dev/null +++ b/library/Icinga/Cli/Documentation.php @@ -0,0 +1,162 @@ +<?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; + + 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 = ''; + 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..732c3bc --- /dev/null +++ b/library/Icinga/Cli/Loader.php @@ -0,0 +1,499 @@ +<?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(); + } + + 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; + } + + 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 && $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]; + } +} |