php/env.php

<?php
/* php helper functions
        err: error handler
        out*: output for html or terminal with headers, lists, tables
              ideas: outTDrc ==> with row col index (store old stack! and move storing logic to outWork)
                     outLiL, output several LiItems (not words as outLi), similar: outTDD, outTRD etc.  
        a2str: anything to string
        i2str: a2str for each argument
        i2strEC: a2str for each argument with $ = $, ... syntax    

*/

###----------- err, dbg and global settings -----------
function err () {
    $t = "error: " . i2str(func_get_args());
    outErr($t);
       throw new Error(strlen($t) < 520 ? $t : substr($t, 0, 500) . '...'); # avoid truncation of stacktrace if message is too long
}

if (phpversion() < '7') 
    err('php version ', phpversion(), 'not supported must be >= 7...');

define('EnvCL', ! isset($_SERVER['REQUEST_METHOD']));
# define('EnvDE', EnvCL or ini_get('display_errors'));
if ( ! defined('OutHtml'))
    define('OutHtml', ! EnvCL);
$outNL = OutHtml ? "\n<br>" : "\n";
$dbg ??= 0;
$dbgPre ??= 'dbg: ';

function dbg($lf, $a, ...$b) {
    global $dbg;
    if ((int) $lf <= $dbg) 
        dbgOut(trim($lf, '1- 0123456789'), $a, $b);
}

function dbQ(...$a) { # unconditional dbg output, to be removed when problem is analysed
    out('dbQ:', ...$a);
}

function dbgOut($ff, $a, $b) {
    global $dbgPre;
    $f = "out$ff";
    $f($dbgPre . a2str($a), ...$b);
}


function dbg1($a, ...$b) {
    global $dbg;
    if (1 <= $dbg)
        dbgOut('', $a, $b);
}

#--- try to activate assertions
function envAssert($al=null) {
    $o = ini_set('zend.assertions', $al);
    if ($al === null)
        return $o;
    $n = ini_get('zend.assertions');
    return "zend.assertions=$n " . ($n == $al ? "changed from $o" : "could not change to $al");
}

$errHHCat = $errHHCat ?? null; 
$errHHRet = $errHHRet ?? false; 
$errHHMax = $errHHMax ?? 20; 
function errHH(int $errno , string $errstr , string $errfile , int $errline) {
    static $cnt = 0;
    global $errHHCat, $errHHRet, $errHHMax;
    $cnt++;
    $b = substr($errstr, 0,  max(3, min(12, strcspn($errstr, ' ()[],.'))));
    $errHHCat[$b] = 1 + ($errHHCat[$b] ?? 0); 
    out("errHH $cnt no=$errno, msg=$errstr, file=$errfile, line=$errline", $errHHCat);
    if ($cnt > $errHHMax)
        err("errHH $cnt errors are too much:", $errHHCat);
    return $errHHRet;
}
 
function errHHAct($actDeact = true) { # activate or deactivate errorHandler errHH
    $res = set_error_handler ($actDeact ? 'errHH' : null, E_ALL);
    out("errorhandler set res $res");
}

###----------- arguments (url or commandline) -----------
if ( EnvCL) { # we are in terminal (CommandLine) mode

    function envScript() {
        global $argv;
        return $argv[0];
    }

    function envArgs($def=null) {
        static $old = null;
        if ($old === null) {
            global $argv;       
            $old = $argv;
            unset($old[0]);    # get rid of script
        }
        return ($def === null or count($old) > 0) ? $old : preg_split('/\s+/', $def, -1, PREG_SPLIT_NO_EMPTY);
    }
} else {
    function envScript() {
        return $_SERVER['SCRIPT_FILENAME'];
    }
    function envArgs($def=null) {
        static $old = null;
        if ($old === null) {
            $a = $_SERVER['QUERY_STRING'];
            if (strpos($a, '&') !== false)
                $old = array_map(fn($v) => trim(urldecode($v)), explode('&', $a));
            else
                $old = preg_split('/\s+/', urldecode($a), -1, PREG_SPLIT_NO_EMPTY);
        }
        return ($def === null or count($old) > 0) ? $old : preg_split('/\s+/', $def, -1, PREG_SPLIT_NO_EMPTY);
    }
}


class EnvArg {
    public $type = [];
    public $val = [];
    public $itr1 = null;
    public $opr = null;

    function __get($nm) {
        return $this->val[$nm === 'ops' ? '' : $nm];    
    }
    
    public function __construct($tps) {
    /* $tps types space separated words in syntax
        natd
        
        n name (0, 1 or more characters)
        
        a array info
            =         not an array
            empty   not an array, name is first character
            [        array (multiple values allowed)
            >        array, reroute operands to this name
            <       reroute operands to '' (reset >) 

        t type
            b boolean (no data, following opt or data allowed in same argument)
            i integer
            s string
            h help

        d default (for arrays d='' means [], d='-' means [''] d='-xyz' means ['xyz'])
    */
        foreach (explode(' ', $tps) as $o) {
            if ( $o === '' ) 
                continue;
            if ( 0 === $r = preg_match('/([^[<>=]*)([[<>=])(.?)(.*)/', $o, $mm))
                $mm = [$o, substr($o, 0 , 1), '=', substr($o, 1 , 1), substr($o, 2)];
            else if ($r !== 1)
                err("EnvArg regex failure on $o");
            if (isset($this->type[$mm[1]]))
                err('already set', $o, $this->type);
            if ($mm[3] === '')
                $mm[3] = 's';
            if ($mm[3] === 'b' or $mm[3] === 'h') 
                $mm[4] = $mm[4] === '' ? false : (bool) $mm[3];
            else if ($mm[3] === 'i') 
                $mm[4] = $mm[4] === '' ? 0 : (int) $mm[4];
            else if ($mm[3] !== 's' ) 
                err ("bad type {$mm[3]} in opt", $mm);
            $this->type[$mm[1]] = $mm[2] . $mm[3] . $mm[4]; 
            if ($mm[2] === '<') 
                $this->type[''] = $this->type[$mm[1]];
            else if ($mm[3] !== 'h')
                $this->val[$mm[1]] = $mm[2] === '=' ? $mm[4] : [];
        }
        $this->type[''] =  '[' . substr($this->type[''] ?? '?s', 1);
        $this->val[''] = [];
        $this->val0 = $this->val;
    }

    # set arg to array of strings, with syntax -opt operand or key = value syntax
    function arg($args, $syn = null) {
        $this->arg = $args;
        if ($syn === null) 
            $syn = (strpos($a = implode(' ', $args), '=') === false or strpos($a, '-') !== false and strpos($a, '-') < strpos($a, '=')) ? '-' : '=';
        $this->itr1 = $syn === '-' ? 'iterMinus' : 'iterEqual';
        foreach ($this->iterate() as $k => $v)
            continue;
        return $this->val[''];
    }
    
    # iterate over array of strings, with the key=value syntax
    function iterEqual() {
        foreach ($this->arg as $a) {
            if (false !== $x = strpos($a, '=')) 
                yield trim(substr($a, 0, $x)) => substr($a, $x+1);
            else 
                yield '' => $a;                 
        } 
    }

    # iterate over array of strings, with the minus option syntax (unix style)
    function iterMinus() { 
        $lOp = null;
        foreach ($this->arg as $a) {
            $x = substr($a, 0, 2) === '--' ? 2 : (int) (substr($a, 0, 1) === '-' or substr($a, 0, 1) === '+');
            $pr = substr($a, 0, $x);
            if ($x <= 0) {
                yield $lOp ?? '' => $a;    
                $lOp = null;
            } else {
                if ($lOp !== null) {
                    yield $lOp => '';
                    $lOp = null;
                }
                while (true) {
                    for ($y=strlen($a) - $x; $y > 0 and ! isset($this->type[substr($a, $x, $y)]); $y--) 
                        continue;
                    if ($y < 1)
                        err('option unknown', substr($a, $x), 'in',  $a, 'in',  $this->arg);
                    $nm = substr($a, $x, $y);
                    $t = $this->type[$nm][1];
                    $x += $y;
                    if ($t === 'b' or $t === 'h') {
                        yield $nm => $pr != '+';
                        if ($x >= strlen($a))
                            break;
                    } else {
                        if ($x >= strlen($a)) 
                            $lOp = $nm;
                         else 
                            yield $nm => substr($a, $x);
                        break;
                    }
                }
            }
        }
        if ($lOp !== null)
            err('option expected after', $lOp);
    }
    
    function iterate() {
        $this->val = $this->val0;
        $this->opr = null;
        $redir = '';
        foreach ($this->{$this->itr1}() as $k => $v) {
            $t = $this->type[$k];
            if ($t[0] === '>') 
                $redir = $k;
            else if ($t[0] === '<')
                $t = $this->type[$redir = $k = ''];
            if ($t[1] === 'b') {
                $v = (bool) $v;
            } else if ($t[1] === 'h') {
                $help = true;
                continue;
            } else if ($t[1] === 'i') {
                $v = (int) $v;
            } else if ($t[1] !== 's') {
                err("bad type $t for key $k, val $v");
            }
            if ($k === '') 
                $k = $redir;
            if (is_array($this->val[$k]))
                $this->val[$k][] = $v;
            else
                $this->val[$k] = $v;
            if ($k === '') 
                yield $this->opr = $v;
        }
        foreach ($this->type as $k => $v) {
            if ($v[0] !== '=' and $v[0] !== '<' and strlen($v) > 2 and count($this->val[$k]) < 1) {
                $this->val[$k][] = substr($v, $v[2] === '-' ? 3 : 2);
                if ($k === '')
                    yield $this->opr = $this->val[''][0];
            }
        }
        if (isset($help))
            exit(help('EnvArg got', $this->val));        

    }
    
    # parse args an call callback $cbt
    function do($cbt, $undef = null) {
        if (! isset($undef))
            $undef = function($k, $v) use($cbt) {err("bad arg $k => $v. supported are", implode(', ', array_keys($cbt))); };
        foreach ($this->{$this->itr1}() as $k => $v) {
            if (isset($cbt[$k]))
                $cbt[$k]($v);
            else 
                $undef($k, $v);                 
        } 
    }
}

###----------- out: output formatting, for commandline or html -----------
require_once 'envFiber.php' ;

#--- send output to mai
function out2mail($main, $email, $sub, $from) {
    ob_start();
    dbg1('out2mail after ob_start');
    try {
        $main();
    } catch(Throwable $t) {
        outThr($t, "out2mail caught");
        error_log("out2mail calling main catch: $t");
    }
    dbg1('before ob_end, last_error:', error_get_last());
    $c = ob_get_contents();
    ob_end_clean();
    dbg1("got contents from main: $c");

    error_clear_last();
    $r = mail($email, $sub, $c, "From: $from\r\nContent-Type: text/html\r\nMIME-Version: 1.0\r\n\r\n");
    dbg1('after mail returns',  $r);
    if (! $r) {
        error_log("could not mail: " . error_get_last());
        err("could not mail:", error_get_last());
        $c .= "<br>\nerror: could not mail: " . error_get_last();
    }
    return $c;
} 

#--- catch errors and write them to out
function envCatch2out($main) {
    #if (EnvDE) 
    #    return $main();
        
    try {
        $main();
    } catch (Throwable $e) { 
        outThr($e, "error: caught");
    }
}

#--- out a Throwable and format stacktrace
function outThr($e, $m) {
    out($m . ' ' . get_class($e) . ': ' . $e->getMessage());
    outOL("{$e->getFile()}: {$e->getLine()}");
    foreach($e->getTrace() as $a)
        outLi("{$a['file']}: {$a['line']} {$a['function']}(", isset($a['args']) ? i2str($a['args'], ', ') : '', ')');
    outOLEnd("2str< $e >2str");         
}

#--- out help
function help(...$m) {
    out();
    if (count($m) > 0)
        out(...$m);
    $f = fopen(envScript(), 'r');
    while (false !== ($li = fgets($f)) and strpos($li, '/*') === false) {}
    if ($li === false) {
        out('no help in ' . envScript());
    } else {
        out('help of', envScript());
        for ( ; $li !== false; $li = fgets($f)) {
            out((strlen($li) >= strlen(PHP_EOL) and substr($li, $ll = strlen($li)-strlen(PHP_EOL)) == PHP_EOL)
                 ? substr($li, 0, $ll): $li);
            if (strpos($li, '*/') !== false)
                break;
            }
    }
    fclose($f);
}

#--- err plus help
function helpErr(...$m) {
    help("error", ...$m);
    err(...$m);
}

###----------- a2str, i2str, etc.: format any type -----------
if (PHP_VERSION_ID < 80100) {
    function array_is_list($a) { # this is a function in inbuilt from php 8.1 and laster
        return array_keys($a) === range(0, count($a) - 1);
    }
    function str_starts_with(string $haystack, string $needle) {
        return substr($haystack, 0, strlen($needle)) === $needle;
    }
    function str_starts_with(string $haystack, string $needle) {
        return substr($haystack, - strlen($needle)) === $needle;
    }
}
    
$a2strLevel=2;
function a2str($a, $st=0) {
    global $a2strLevel;
    if ($a === null)
        return 'null';
    elseif ($a === 0 or $a === 1)
        return "$a";
    elseif ($a === false)
        return 'false';
    elseif ($a === true)
        return 'true';
    elseif (is_object($a)) {
        if (method_exists($a, 'a2str')) 
            return $a->a2str($st);
        if ($st > $a2strLevel) 
            return method_exists($a, '__tostring') ? ((string) $a) : (gettype($a) . ':' . get_class($a));
        $r = '';
        foreach (get_object_vars($a) as $k => $v) # attention, foreach $a as also works, however if $a is an iterator, it will iterate ....
            $r .= ", $k -> " . a2str($v, $st+1);
        return '{' . gettype($a) . ':' . get_class($a) . ': ' . substr($r, 2) . '}';
    } elseif (! is_array($a)) 
        return (string) $a; 
    elseif (count($a) <= 0)
        return '[]';
    elseif ($st > $a2strLevel)
        return '[...' . count($a) . '...]';
    $r = '';
    if (array_is_list($a))
        foreach ($a as $v)
            $r .= ", " . a2str($v, $st+1);
    else    
        foreach ($a as $k => $v)
            $r .= ", $k => " . a2str($v, $st+1);
            
    return '[' . substr($r, 2) . ']';
}

function i2str($a, $sep=' ') { # implode a2str of each element of $a with separator $sep
    if (1 > $l = count($a))
        return '';
    $o = a2str($a[0]);
    for ($i=1; $i < $l; $i++)
        $o .= $sep . a2str($a[$i]);
    return $o;
}

function i2strEC($a, $s1= '=', $s2=', ') { # implode a2str of each element of $a with 2 separators 
    if (1 > ($l = count($a) - 1))
        return  $l < 0 ? '' : a2str($a[0]);
    $o = a2str($a[0]) . $s1 . a2str($a[1]);
    for($i=2; $i < $l; $i+=2)
        $o .= $s2 . a2str($a[$i]) . $s1 . a2str($a[$i+1]);
    return $i > $l ? $o : $o . $s2 . a2str($a[$i]);
}

#--- format a number with decimal prefixes (k, M, G, ...)
function fmtG($n, $w=8, $si='') {
    static $fmtG2n = ['k' => 1e3, 'M' => 1e6, 'G' => 1e9, 'T' => 1e12, 'P' => 1e15, 'E' => 1e18];
    $a = abs($n);
    $u = null;
    foreach ($fmtG2n as $k => $v) {
        if ($a < $v)
            break;
        $u = $k;
    }
    $s = $n < 0 ? '-' : $si;
    if ($u === null)
        return str_pad($s . round($a),  $w-1, ' ', STR_PAD_LEFT) . ' ';
    else if ($u === $k)
        return fmtE($n, $w-1, $si) . ' ';
    $l = $w - 1;
    $p = $w - 5 - ($s !== '');
    $m = $n/$fmtG2n[$u];
    $l = $w - 1;
    $p = $w - 5 - ($si !== '' or $s !== '' and abs($m) >= 100);
    $r = sprintf("%{$si}{$l}.{$p}f", $m);
    if (strlen($r) > $l) {
        $p--;
        $r = sprintf("%{$si}{$l}.{$p}f", $m);
    }
    return $r . $u;
}

#--- format a number ibn scientific notation economise necessary space
function fmtE($n, $w=6, $si='') {
    $a = abs($n);
    $p = max(0, $w + ($a >= 1 or $a == 0) - 5 - ($n < 0 or $si !== '') - ($a >= 1e10 or $a < 1e-9 and $a != 0));
    $r = str_replace('e+', 'e', sprintf("%{$si}1.{$p}e", $n));
    if (strlen($r) == $w)
        return $r;
    if (0 <= $p += strlen($r) < $w ? 1 : -1) { # retry with correct
        $r2 = str_replace('e+', 'e', sprintf("%{$si}1.{$p}e", $n));
        if (strlen($r2) == $w)
            return $r2;
        if (strlen($r2) < $w)
            $r = $r2;
    }
    if (strlen($r) < $w) 
        return str_pad($r, $w, ' ', STR_PAD_LEFT);
    if ($a >= 1e10) 
        return str_repeat($n < 0 ? '-' : '+', $w);
    else if ($a >= 1e-8)
        return str_repeat('?', $w);
    $s = $n < 0 ? '-' : $si;
    $r = sprintf("{$s}%1de-9", round($a*1e9));
    #echo "\n e9 {$r}\n";
    return strlen($r) <= $w ? str_pad($r, $w, ' ', STR_PAD_LEFT) : str_repeat('?', $w); 
}