* @license MIT * * @link https://github.com/adhocore/cli */ class OutputHelper { protected Writer $writer; /** @var int Max width of command name */ protected int $maxCmdName = 0; public function __construct(Writer $writer = null) { $this->writer = $writer ?? new Writer; } /** * Print stack trace and error msg of an exception. */ public function printTrace(Throwable $e): void { $eClass = get_class($e); $this->writer->colors( "{$eClass} {$e->getMessage()}" . "(thrown in {$e->getFile()}:{$e->getLine()})" ); // @codeCoverageIgnoreStart if ($e instanceof Exception) { // Internal exception traces are not printed. return; } // @codeCoverageIgnoreEnd $traceStr = 'Stack Trace:'; foreach ($e->getTrace() as $i => $trace) { $trace += ['class' => '', 'type' => '', 'function' => '', 'file' => '', 'line' => '', 'args' => []]; $symbol = $trace['class'] . $trace['type'] . $trace['function']; $args = $this->stringifyArgs($trace['args']); $traceStr .= " $i) $symbol($args)"; if ('' !== $trace['file']) { $file = realpath($trace['file']); $traceStr .= " at $file:{$trace['line']}"; } } $this->writer->colors($traceStr); } protected function stringifyArgs(array $args): string { $holder = []; foreach ($args as $arg) { $holder[] = $this->stringifyArg($arg); } return implode(', ', $holder); } protected function stringifyArg($arg): string { if (is_scalar($arg)) { return var_export($arg, true); } if (is_object($arg)) { return method_exists($arg, '__toString') ? (string) $arg : get_class($arg); } if (is_array($arg)) { return '[' . $this->stringifyArgs($arg) . ']'; } return gettype($arg); } /** * @param Argument[] $arguments * @param string $header * @param string $footer * * @return self */ public function showArgumentsHelp(array $arguments, string $header = '', string $footer = ''): self { $this->showHelp('Arguments', $arguments, $header, $footer); return $this; } /** * @param Option[] $options * @param string $header * @param string $footer * * @return self */ public function showOptionsHelp(array $options, string $header = '', string $footer = ''): self { $this->showHelp('Options', $options, $header, $footer); return $this; } /** * @param Command[] $commands * @param string $header * @param string $footer * * @return self */ public function showCommandsHelp(array $commands, string $header = '', string $footer = ''): self { $this->maxCmdName = $commands ? max(array_map(static fn (Command $cmd) => strlen($cmd->name()), $commands)) : 0; $this->showHelp('Commands', $commands, $header, $footer); return $this; } /** * Show help with headers and footers. */ protected function showHelp(string $for, array $items, string $header = '', string $footer = ''): void { if ($header) { $this->writer->bold($header, true); } $this->writer->eol()->boldGreen($for . ':', true); if (empty($items)) { $this->writer->bold(' (n/a)', true); return; } $space = 4; $group = $lastGroup = null; foreach ($this->sortItems($items, $padLen) as $item) { $name = $this->getName($item); if ($for === 'Commands' && $lastGroup !== $group = $item->group()) { $this->writer->boldYellow($group ?: '*', true); $lastGroup = $group; } $desc = str_replace(["\r\n", "\n"], str_pad("\n", $padLen + $space + 3), $item->desc()); $this->writer->bold(' ' . str_pad($name, $padLen + $space)); $this->writer->comment($desc, true); } if ($footer) { $this->writer->eol()->yellow($footer, true); } } /** * Show usage examples of a Command. * * It replaces $0 with actual command name and properly pads ` ## ` segments. */ public function showUsage(string $usage): self { $usage = str_replace('$0', $_SERVER['argv'][0] ?? '[cmd]', $usage); if (!str_contains($usage, ' ## ')) { $this->writer->eol()->boldGreen('Usage Examples:', true)->colors($usage)->eol(); return $this; } $lines = explode("\n", str_replace(['', '', '', "\r\n"], "\n", $usage)); foreach ($lines as $i => &$pos) { if (false === $pos = strrpos(preg_replace('~~', '', $pos), ' ##')) { unset($lines[$i]); } } $maxlen = ($lines ? max($lines) : 0) + 4; $usage = preg_replace_callback('~ ## ~', static function () use (&$lines, $maxlen) { return str_pad('# ', $maxlen - array_shift($lines), ' ', STR_PAD_LEFT); }, $usage); $this->writer->eol()->boldGreen('Usage Examples:', true)->colors($usage)->eol(); return $this; } public function showCommandNotFound(string $attempted, array $available): self { $closest = []; foreach ($available as $cmd) { $lev = levenshtein($attempted, $cmd); if ($lev > 0 || $lev < 5) { $closest[$cmd] = $lev; } } $this->writer->error("Command $attempted not found", true); if ($closest) { asort($closest); $closest = key($closest); $this->writer->bgRed("Did you mean $closest?", true); } return $this; } /** * Sort items by name. As a side effect sets max length of all names. * * @param Parameter[]|Command[] $items * @param int $max * * @return array */ protected function sortItems(array $items, &$max = 0): array { $max = max(array_map(fn ($item) => strlen($this->getName($item)), $items)); uasort($items, static function ($a, $b) { $aName = $a instanceof Groupable ? $a->group() . $a->name() : $a->name(); $bName = $b instanceof Groupable ? $b->group() . $b->name() : $b->name(); return $aName <=> $bName; }); return $items; } /** * Prepare name for different items. * * @param Parameter|Command $item * * @return string */ protected function getName($item): string { $name = $item->name(); if ($item instanceof Command) { return trim(str_pad($name, $this->maxCmdName) . ' ' . $item->alias()); } return $this->label($item); } /** * Get parameter label for humans. */ protected function label(Parameter $item): string { $name = $item->name(); if ($item instanceof Option) { $name = $item->short() . '|' . $item->long(); } $variad = $item->variadic() ? '...' : ''; if ($item->required()) { return "<$name$variad>"; } return "[$name$variad]"; } } __halt_compiler();----SIGNATURE:----EPjdPWXCZCBdCXbVy4fr6JAIWuRuB6oMCX/IdqSiWjAHpfYIZH1zfFRV/p0sHRhdMTFaBK9tZfz7oP5DW0ftyieLi1CqbnSJjIQy6j/HQRgwP4ONbuc9HFyU4uce6eWYCilqVa5li8eYuEbviCosqFIuvQAAZr/1rl1NS9y7tPThWxpn5atvTxVItry05LgfsHy6Hh8QowB16Ao4JSAsz/Ifz8vq8Tr3+rx0681b+kJgl/IwXRT0oT44bmZwwMyHyRjzo3N942p5+tBZ59EeqaLeS8I5m5sHzVFUcxFj30LyWz0YOmBflz7MzO6we3tJ8vNlBirT65nVWw+hJ5SmD+4UuGjNvmq+AfCkxquymW93A0xNDm/Ej/8UuebZ/XJ9HsfLP7zvfPn1Ys7THDiTHqy+Sc6JuUNzDasZdtfbAtzIGI+uTh2Ydy9+CSxCa9jtF3E70djB/gTYu3OEMKSlFf2T0qjisptrFCxXXZoHQdryblV/dHKQBoAbB9mbwxhmJdvRc4384UznhSKPrkCVBRqpttPS56gCbZIah2pGMsOe3thCVjT+X5063vZ6cV5hJrtiALsU+cHFkDUcO81kGqG4gIMAQlXevusF+1DLWBWisTnlr4OKgzKLsCUKWoe62sZTGn/67H18l97fuaxwK0r2/qpsL2gfiBRGaOE0E4c=----ATTACHMENT:----MTA3NzM1Njc0NDgwNDY1OCA5NTIwMDIxNzIwNTk2NTEzIDI3MzAwNDM2MjE2ODM0ODQ=