$row) { if (!is_array($out[0])) { unset($out[$i]); continue; } $nCols = max($nCols, count($row)); for ($j = 0; $j < $nCols; $j++) $pads[$j] = max($pads[$j] ?? 0, mb_strlen(self::purgeEscapes($row[$j] ?? ''))); } foreach ($out as $i => $row) { for ($j = 0; $j < $nCols; $j++) { if (!isset($row[$j])) break; $len = ($pads[$j] - mb_strlen(self::purgeEscapes($row[$j]))); for ($k = 0; $k < $len; $k++) // can't use str_pad(). it counts invisible chars. $row[$j] .= ' '; } if ($i || $headless) self::write(' '.implode(' ' . self::tblDelim(' ') . ' ', $row), self::LOG_NONE, $timestamp); else self::write(self::tblHead(' '.implode(' ', $row)), self::LOG_NONE, $timestamp); } if (!$headless) self::write(self::tblHead(str_pad('', array_sum($pads) + count($pads) * 3 - 2)), self::LOG_NONE, $timestamp); self::write(); } /***********/ /* logging */ /***********/ public static function initLogFile(string $file = '') : void { if (!$file) return; $file = self::nicePath($file); if (!file_exists($file)) self::$logHandle = fopen($file, 'w'); else { $logFileParts = pathinfo($file); $i = 1; while (file_exists($logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : ''))) $i++; $file = $logFileParts['dirname'].'/'.$logFileParts['filename'].$i.(isset($logFileParts['extension']) ? '.'.$logFileParts['extension'] : ''); self::$logHandle = fopen($file, 'w'); } } private static function tblHead(string $str) : string { return CLI_HAS_E ? "\e[1;48;5;236m".$str."\e[0m" : $str; } private static function tblDelim(string $str) : string { return CLI_HAS_E ? "\e[48;5;236m".$str."\e[0m" : $str; } public static function grey(string $str) : string { return CLI_HAS_E ? "\e[90m".$str."\e[0m" : $str; } public static function red(string $str) : string { return CLI_HAS_E ? "\e[31m".$str."\e[0m" : $str; } public static function green(string $str) : string { return CLI_HAS_E ? "\e[32m".$str."\e[0m" : $str; } public static function yellow(string $str) : string { return CLI_HAS_E ? "\e[33m".$str."\e[0m" : $str; } public static function blue(string $str) : string { return CLI_HAS_E ? "\e[36m".$str."\e[0m" : $str; } public static function bold(string $str) : string { return CLI_HAS_E ? "\e[1m".$str."\e[0m" : $str; } public static function write(string $txt = '', int $lvl = self::LOG_BLANK, bool $timestamp = true, bool $tmpRow = false) : void { $msg = ''; if ($txt) { if ($timestamp) $msg = str_pad(date('H:i:s'), 10); switch ($lvl) { case self::LOG_ERROR: // red critical error $msg .= '['.self::red('ERR').'] '; break; case self::LOG_WARN: // yellow notice $msg .= '['.self::yellow('WARN').'] '; break; case self::LOG_OK: // green success $msg .= '['.self::green('OK').'] '; break; case self::LOG_INFO: // blue info $msg .= '['.self::blue('INFO').'] '; break; case self::LOG_BLANK: $msg .= ' '; break; } $msg .= $txt; } // https://shiroyasha.svbtle.com/escape-sequences-a-quick-guide-1#movement_1 $msg = (self::$overwriteLast && CLI_HAS_E ? "\e[1G\e[0K" : "\n") . $msg; self::$overwriteLast = $tmpRow; fwrite($lvl == self::LOG_ERROR ? STDERR : STDOUT, $msg); if (self::$logHandle) // remove control sequences from log fwrite(self::$logHandle, self::purgeEscapes($msg)); flush(); } private static function purgeEscapes(string $msg) : string { return preg_replace(["/\e\[[\d;]+[mK]/", "/\e\[\d+G/"], ['', "\n"], $msg); } public static function nicePath(string $fileOrPath, string ...$pathParts) : string { $path = ''; if ($pathParts) { foreach ($pathParts as &$pp) $pp = trim($pp); $path .= implode(DIRECTORY_SEPARATOR, $pathParts); } $path .= ($path ? DIRECTORY_SEPARATOR : '').trim($fileOrPath); // remove double quotes (from erronous user input), single quotes are // valid chars for filenames and removing those mutilates several wow icons $path = str_replace('"', '', $path); if (!$path) // empty strings given. (faulty dbc data?) return ''; if (DIRECTORY_SEPARATOR == '/') // *nix { $path = str_replace('\\', '/', $path); $path = preg_replace('/\/+/i', '/', $path); } else if (DIRECTORY_SEPARATOR == '\\') // win { $path = str_replace('/', '\\', $path); $path = preg_replace('/\\\\+/i', '\\', $path); } else self::write('Dafuq! Your directory separator is "'.DIRECTORY_SEPARATOR.'". Please report this!', self::LOG_ERROR); // resolve *nix home shorthand if (!OS_WIN) { if (preg_match('/^~(\w+)\/.*/i', $path, $m)) $path = '/home/'.substr($path, 1); else if (substr($path, 0, 2) == '~/') $path = getenv('HOME').substr($path, 1); else if ($path[0] == DIRECTORY_SEPARATOR && substr($path, 0, 6) != '/home/') $path = substr($path, 1); } return $path; } /**************/ /* read input */ /**************/ /* since the CLI on WIN ist not interactive, the following things have to be considered you do not receive keystrokes but whole strings upon pressing (wich also appends a \r) as such and probably other control chars can not be registered this also means, you can't hide input at all, least process it */ public static function read(array $fields, ?array &$userInput = []) : bool { // first time set if (self::$hasReadline === null) self::$hasReadline = function_exists('readline_callback_handler_install'); // prevent default output if able if (self::$hasReadline) readline_callback_handler_install('', function() { }); if (!STDIN) return false; stream_set_blocking(STDIN, false); // pad default values onto $fields array_walk($fields, function(&$val, $_, $pad) { $val += $pad; }, ['', false, false, '']); foreach ($fields as $name => [$desc, $isHidden, $singleChar, $validPattern]) { $charBuff = ''; if ($desc) fwrite(STDOUT, "\n".$desc.": "); while (true) { if (feof(STDIN)) return false; $r = [STDIN]; $w = $e = null; $n = stream_select($r, $w, $e, 200000); if (!$n || !in_array(STDIN, $r)) continue; // stream_get_contents is always blocking under WIN - fgets should work similary as php always receives a terminated line of text $chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN)); $ordinals = array_map('ord', $chars); if ($ordinals[0] == self::CHR_ESC) { if (count($ordinals) == 1) { fwrite(STDOUT, chr(self::CHR_BELL)); return false; } else continue; } foreach ($chars as $idx => $char) { $keyId = $ordinals[$idx]; // skip char if horizontal tab or \r if followed by \n if ($keyId == self::CHR_TAB || ($keyId == self::CHR_CR && ($ordinals[$idx + 1] ?? '') == self::CHR_LF)) continue; if ($keyId == self::CHR_BACKSPACE) { if (!$charBuff) continue 2; $charBuff = mb_substr($charBuff, 0, -1); if (!$isHidden && self::$hasReadline) fwrite(STDOUT, chr(self::CHR_BACK)." ".chr(self::CHR_BACK)); } // standalone \n or \r else if ($keyId == self::CHR_LF || $keyId == self::CHR_CR) { $userInput[$name] = $charBuff; break 2; } else if (!$validPattern || preg_match($validPattern, $char)) { $charBuff .= $char; if (!$isHidden && self::$hasReadline) fwrite(STDOUT, $char); if ($singleChar && self::$hasReadline) { $userInput[$name] = $charBuff; break 2; } } } } } fwrite(STDOUT, chr(self::CHR_BELL)); foreach ($userInput as $ui) if (strlen($ui)) return true; $userInput = null; return true; } } ?>