273 lines
7.7 KiB
PHP
273 lines
7.7 KiB
PHP
<?php
|
|
namespace nulib\output\std;
|
|
|
|
use Exception;
|
|
use nulib\cl;
|
|
use nulib\output\IContent;
|
|
use nulib\output\IPrintable;
|
|
|
|
/**
|
|
* Class StdOutput: affichage sur STDOUT, STDERR ou dans un fichier quelconque
|
|
*
|
|
* si la destination est un fichier, aucune gestion d'erreur n'est faite, sauf
|
|
* lors de l'ouverture.
|
|
*/
|
|
class StdOutput {
|
|
const COLORS = [
|
|
"reset" => "0",
|
|
"bold" => "1",
|
|
"faint" => "2",
|
|
"underlined" => "4",
|
|
"reverse" => "7",
|
|
"normal" => "22",
|
|
"black" => "30",
|
|
"red" => "31",
|
|
"green" => "32",
|
|
"yellow" => "33",
|
|
"blue" => "34",
|
|
"magenta" => "35",
|
|
"cyan" => "36",
|
|
"white" => "37",
|
|
"default" => "39",
|
|
"black-bg" => "40",
|
|
"red-bg" => "41",
|
|
"green-bg" => "42",
|
|
"yellow-bg" => "43",
|
|
"blue-bg" => "44",
|
|
"magenta-bg" => "45",
|
|
"cyan-bg" => "46",
|
|
"white-bg" => "47",
|
|
"default-bg" => "49",
|
|
];
|
|
|
|
const COLOR_MAP = [
|
|
"z" => "reset",
|
|
"o" => "black",
|
|
"r" => "red",
|
|
"g" => "green",
|
|
"y" => "yellow",
|
|
"b" => "blue",
|
|
"m" => "magenta",
|
|
"c" => "cyan",
|
|
"w" => "white",
|
|
"O" => "black_bg",
|
|
"R" => "red_bg",
|
|
"G" => "green_bg",
|
|
"Y" => "yellow_bg",
|
|
"B" => "blue_bg",
|
|
"M" => "magenta_bg",
|
|
"C" => "cyan_bg",
|
|
"W" => "white_bg",
|
|
"@" => "bold",
|
|
"-" => "faint",
|
|
"_" => "underlined",
|
|
"~" => "reverse",
|
|
"n" => "normal",
|
|
];
|
|
|
|
/**
|
|
* @param resource|null $outf
|
|
* @throws Exception si la destination est un fichier et que son ouverture a
|
|
* échoué
|
|
*/
|
|
function __construct($output=null, ?array $params=null) {
|
|
if ($output !== null) $params["output"] = $output;
|
|
elseif (!isset($params["output"])) $params["output"] = STDOUT;
|
|
if (!isset($params["filter_tags"])) $params["filter_tags"] = true;
|
|
if (!isset($params["indent"])) $params["indent"] = " ";
|
|
$this->resetParams($params);
|
|
}
|
|
|
|
function resetParams(?array $params=null): void {
|
|
$output = cl::get($params, "output");
|
|
$maskErrors = null;
|
|
$color = cl::get($params, "color");
|
|
$filterTags = cl::get($params, "filter_tags");
|
|
$indent = cl::get($params, "indent");
|
|
$flush = cl::get($params, "flush");
|
|
|
|
if ($output !== null) {
|
|
if ($output === "php://stdout") {
|
|
$outf = STDOUT;
|
|
} elseif ($output === "php://stderr") {
|
|
$outf = STDERR;
|
|
} elseif (!is_resource($output)) {
|
|
# si $outf est un nom de fichier, vérifier que l'ouverture se fait sans
|
|
# erreur. à partir de là, plus aucune gestion d'erreur n'est faite, à
|
|
# part afficher les erreurs d'écriture la première fois qu'elles se
|
|
# produisent
|
|
$maskErrors = false;
|
|
$outf = @fopen($output, "ab");
|
|
if ($outf === false) {
|
|
$error = error_get_last();
|
|
if ($error !== null) $message = $error["message"];
|
|
else $message = "$output: open error";
|
|
throw new Exception($message);
|
|
}
|
|
if ($flush === null) $flush = true;
|
|
} else {
|
|
$outf = $output;
|
|
}
|
|
$this->outf = $outf;
|
|
$this->maskErrors = $maskErrors;
|
|
if ($color === null) $color = stream_isatty($outf);
|
|
if ($flush === null) $flush = false;
|
|
}
|
|
if ($color !== null) $this->color = boolval($color);
|
|
if ($filterTags !== null) $this->filterTags = boolval($filterTags);
|
|
if ($indent !== null) $this->indent = strval($indent);
|
|
if ($flush !== null) $this->flush = boolval($flush);
|
|
}
|
|
|
|
/** @var resource */
|
|
protected $outf;
|
|
|
|
/** @var bool faut-il masquer les erreurs d'écriture? */
|
|
protected $maskErrors;
|
|
|
|
/** @var bool faut-il autoriser la sortie en couleur? */
|
|
protected $color;
|
|
|
|
function isColor(): bool {
|
|
return $this->color;
|
|
}
|
|
|
|
/** @var bool faut-il enlever les tags dans la sortie? */
|
|
protected $filterTags;
|
|
|
|
/** @var string indentation unitaire */
|
|
protected $indent;
|
|
|
|
/** @var bool faut-il flush le fichier après l'écriture de chaque ligne */
|
|
protected $flush;
|
|
|
|
function isatty(): bool {
|
|
return stream_isatty($this->outf);
|
|
}
|
|
|
|
private static function replace_colors(array $ms): string {
|
|
$colors = [];
|
|
foreach (preg_split('/\s+/', $ms[1]) as $color) {
|
|
while ($color && !cl::has(self::COLORS, $color)) {
|
|
$alias = substr($color, 0, 1);
|
|
$colors[] = self::COLOR_MAP[$alias];
|
|
$color = substr($color, 1);
|
|
}
|
|
if ($color) $colors[] = $color;
|
|
}
|
|
$text = "\x1B[";
|
|
$first = true;
|
|
foreach ($colors as $color) {
|
|
if (!$color) continue;
|
|
if ($first) $first = false;
|
|
else $text .= ";";
|
|
$text .= self::COLORS[$color];
|
|
}
|
|
$text .= "m";
|
|
return $text;
|
|
}
|
|
function filterContent(string $text): string {
|
|
# couleur au début
|
|
$text = preg_replace_callback('/<color([^>]*)>/', [self::class, "replace_colors"], $text);
|
|
# reset à la fin
|
|
$text = preg_replace('/<\/color>/', "\x1B[0m", $text);
|
|
# enlever les tags classiques
|
|
if ($this->filterTags) {
|
|
$text = preg_replace('/<[^>]*>/', "", $text);
|
|
}
|
|
return $text;
|
|
}
|
|
function filterColors(string $text): string {
|
|
return preg_replace('/\x1B\[.*?m/', "", $text);
|
|
}
|
|
|
|
static function flatten($values, ?array &$dest=null): array {
|
|
if ($dest === null) $dest = [];
|
|
if ($values === null) return $dest;
|
|
if (is_string($values)) {
|
|
$dest[] = $values;
|
|
return $dest;
|
|
} elseif (!is_array($values)) {
|
|
if ($values instanceof IContent) {
|
|
$values = $values->getContent();
|
|
} elseif ($values instanceof IPrintable) {
|
|
ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE);
|
|
$values->print();
|
|
$dest[] = ob_get_clean();
|
|
return $dest;
|
|
} elseif (!is_iterable($values)) {
|
|
$dest[] = strval($values);
|
|
return $dest;
|
|
}
|
|
}
|
|
foreach ($values as $value) {
|
|
self::flatten($value, $dest);
|
|
}
|
|
return $dest;
|
|
}
|
|
|
|
function getIndent(int $indentLevel): string {
|
|
return str_repeat($this->indent, $indentLevel);
|
|
}
|
|
|
|
function getLines(bool $withNl, ...$values): array {
|
|
$values = self::flatten($values);
|
|
if (!$values) return [];
|
|
$text = implode("", $values);
|
|
if ($text === "") return [""];
|
|
$text = $this->filterContent($text);
|
|
if (!$this->color) $text = $this->filterColors($text);
|
|
$lines = explode("\n", $text);
|
|
$max = count($lines) - 1;
|
|
if ($withNl) {
|
|
for ($i = 0; $i < $max; $i++) {
|
|
$lines[$i] .= "\n";
|
|
}
|
|
}
|
|
if ($lines[$max] === "") unset($lines[$max]);
|
|
return $lines;
|
|
}
|
|
|
|
private function _fwrite($outf, string $data): void {
|
|
if ($this->maskErrors === null) {
|
|
# masquer les erreurs d'écriture en permanence
|
|
@fwrite($outf, $data);
|
|
return;
|
|
}
|
|
# masquer uniquement la première erreur, jusqu'à ce que l'erreur disparaisse
|
|
if ($this->maskErrors) $r = @fwrite($outf, $data);
|
|
else $r = fwrite($outf, $data);
|
|
$this->maskErrors = $r === false;
|
|
}
|
|
|
|
function writeLines($indent, array $lines, bool $addNl=false): void {
|
|
$outf = $this->outf;
|
|
foreach ($lines as $line) {
|
|
if ($indent !== null) $this->_fwrite($outf, $indent);
|
|
$this->_fwrite($outf, $line);
|
|
if ($addNl) $this->_fwrite($outf, "\n");
|
|
}
|
|
if ($this->flush) @fflush($outf);
|
|
}
|
|
|
|
function write(...$values): void {
|
|
$this->writeLines(null, $this->getLines(true, ...$values));
|
|
}
|
|
|
|
function print(...$values): void {
|
|
$values[] = "\n";
|
|
$this->writeLines(null, $this->getLines(true, ...$values));
|
|
}
|
|
|
|
function iwrite(int $indentLevel, ...$values): void {
|
|
$indent = $this->getIndent($indentLevel);
|
|
$this->writeLines($indent, $this->getLines(true, ...$values));
|
|
}
|
|
|
|
function iprint(int $indentLevel, ...$values): void {
|
|
$values[] = "\n";
|
|
$indent = $this->getIndent($indentLevel);
|
|
$this->writeLines($indent, $this->getLines(true, ...$values));
|
|
}
|
|
}
|