"; public function __construct() { $this->configure(); } protected function configure() { } /** * Push a new primary context, saving the previous contexts on a stack. * * @param Context $context */ public function pushContext(Context $context) { if ($this->context) { $context->setParent($this->context); array_unshift($this->contextStack, $this->context); } $context->setShell($this); $this->context = $context; $this->dispatchEvent("context.update", [ "context"=>$this->context ]); } /** * Pop the current context. * * @return Context */ public function popContext() { $previous = $this->context; if (count($this->contextStack)>0) { $this->context = array_shift($this->contextStack); } else { $this->context = null; } $this->dispatchEvent("context.update", [ "context"=>$this->context ]); return $previous; } public function getContextPath($separator=":") { // Return null if we don't have a current context if (!$this->context) return null; // Assemble the contexts to walk $stack = [ $this->context->getName() ]; foreach ($this->contextStack as $context) { $stack[] = $context->getName(); } // Reverse the order to make it more logical $stack = array_reverse($stack); return join($separator,$stack); } public function createContext() { $context = new Context(); $this->pushContext($context); return $context; } /** * Add a handler to be called when an event fires. * * @param string $event * @param callable $handler */ public function addListener($event, callable $handler) { if (!array_key_exists($event,$this->listeners)) { $this->listeners[$event] = []; } $this->listeners[$event][] = $handler; } /** * Invoke event handlers for event. * * @param string $event * @param callable $handler */ protected function dispatchEvent($event, array $data=[]) { if (!array_key_exists($event,$this->listeners)) { return; } foreach ($this->listeners[$event] as $handler) { call_user_func($handler, (object)$data, $this); } } /** * Add a callback to be called at a regular interval during the update phase. * Adding timers with a low interval (less than 200 i.e. 0.2s) may have an * impact on performance. * * @param int $interval Interval in ms * @param callable $handler The handler * @param array $userdata Data to be passed to the handler * @return Timer */ public function addTimer($interval, callable $handler, array $userdata=[]) { $timer = new class($interval, $handler, $userdata) { private $next; private $interval; private $handler; private $userdata; public function __construct($interval, callable $handler, array $userdata) { $this->interval = $interval / 1000; $this->handler = $handler; $this->userdata = $userdata; } public function update() { $now = microtime(true); if ($now > $this->next) { $this->next = $now + $this->interval; call_user_func($this->handler, $this->userdata); } } }; $this->timers[] = $timer; return $timer; } /** * Remove a created timer. * * @param Timer $timer */ public function removeTimer(Timer $timer) { } public function setPrompt($text) { $this->prompt = $text; if ($this->lineReader) { $this->lineReader->setPromptText($text); } } /** * Find a command and return a closure. * * @return callable The command */ private function findCommand($command) { // Go over current context and walk through stack until finding command if ($this->context->hasCommand($command)) { $handler = $this->context->getCommand($command); } else { foreach($this->contextStack as $context) { if ($context->hasCommand($command) && $context->isCommandGlobal($command)) { $handler = $context->getCommand($command); break; } } } // No handler... if (empty($handler)) { return false; } // Return closure return function (...$args) use ($handler) { return call_user_func($handler, ...$args); }; } /** * Execute a command with arguments. * * @param string $command The command name to execute * @param string $args Arguments * @return mixed * @throws Exception\BadCommandExcception */ public function executeCommand($command, ...$args) { if ($this->executeBuiltin($command, ...$args)) { return; } // Call the handler if the command was found if (($target = $this->findCommand($command))) { $ret = $target(...$args); if ($ret instanceof Context) { $this->pushContext($ret); } return; } // Call 'execute' on the current context if ($this->context->execute($command, ...$args)) { return; } // Throw error if the command could not be found throw new Exception\BadCommandException("Command {$command} not found"); } public function executeBuiltin($command, ...$args) { switch ($command) { case '.': $type = basename(strtr(get_class($this->context), "\\", "/")); printf("%s<%s>: %s\n", $type, $this->context->getName(), $this->context->getContextInfo()); $level = 0; foreach ($this->contextStack as $context) { $type = basename(strtr(get_class($context), "\\", "/")); printf(" %sā””ā”€%s<%s>: %s\n", str_repeat(" ",$level++), $type, $context->getName(), $context->getContextInfo()); } break; case '..': if (count($this->contextStack)>0) $this->popContext(); break; case 'help': $help = $this->context->getCommandHelp(); $ghelp = []; foreach ($this->contextStack as $context) { $commands = $context->getCommandHelp(); foreach ($commands as $command=>$info) { if (strpos(" ",$command)!==false) { list ($cmd,$arg)=explode(" ",$command,2); } else { $cmd = $command; } if ($context->isCommandGlobal($cmd)) { $ghelp[$command] = $info; } } } ksort($ghelp); printf("Commands in current context:\n"); foreach ($help as $command=>$info) { printf(" %-20s %s\n", $command, $info); } if (count($ghelp)) { printf("\nImported from parent contexts:\n"); foreach ($ghelp as $command=>$info) { printf(" %-20s %s\n", $command, $info); } } printf("\nGlobal commands:\n"); printf(" %-20s %s\n", "exit", "Leave the shell"); printf(" %-20s %s\n", "..", "Discard the current context and go to parent"); break; case 'exit': $this->stop(); break; default: return false; } return true; } /** * Parse a string and execute the resulting command. * * @param string $command The string to parse * @return mixed */ public function executeBuffer(string $command) { $args = str_getcsv($command, " ", "\"", "\\"); $command = array_shift($args); try { $this->executeCommand($command, ...$args); } catch (Exception\ShellException $e) { echo "\e[31;91;1m{$e->getMessage()}\e[0m\n"; } /* if (array_key_exists($commandName, $this->commands)) { $command = $this->commands[$commandName]; if ($command instanceof Command) { $command->run($buffer); } else { call_user_func_array($command,$buffer); } return; } $this->writeln("Bad command: ".$commandName); */ } public function run() { try { $this->lineReader = new LineRead(); $this->lineReader->setPromptText($this->prompt); $this->lineReader->setPromptStyle(new Style(Style::BR_GREEN)); $this->lineReader->setCommandStyle(new Style(Style::GREEN)); $this->running = true; $this->dispatchEvent("prompt", [ "context"=>$this->context ]); while ($this->running) { // Update the input stuff, sleep if nothing to do. if (!($buffer = $this->lineReader->update())) { usleep(10000); } // Escape is handy too... if ($buffer == "\e") { $this->dispatchEvent("shell.abort"); continue; } // we get a ^C on ^C, so deal with the ^C. if ($buffer == "\x03") { $this->dispatchEvent("shell.stop"); $this->stop(); continue; } // Execute the buffer ob_start(); $this->dispatchEvent("update"); foreach ($this->timers as $timer) { $timer->update(); } if ($buffer) { $this->executeBuffer($buffer); } $output = ob_get_contents(); ob_end_clean(); if (trim($output)) { $this->lineReader->erase(); echo rtrim($output)."\n"; $this->lineReader->redraw(); } if (!$this->context) { $this->stop(); } if ($buffer) { $this->dispatchEvent("prompt", [ "context"=>$this->context ]); } } } catch (\Exception $e) { fprintf(STDERR, "\e[31;1mFatal: Unhandled exception\e[0m\n\n%s\n", $e); } $this->lineReader = null; } public function stop() { $this->running = false; } }