<?php if (!defined('PmWiki')) exit();
# vim: set ts=4 sw=4 et:
##
##        File: WikiSh.php
##     Version: 2015-06-05
##      SVN ID: $Id: WikiSh.php 418 2010-06-09 05:55:02Z pbowers $
##      Status: beta
##      Author: Peter Bowers
## Create Date: January 31, 2008
##   Copyright: 2008-2010, Peter Bowers
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License, Version 2, as
## published by the Free Software Foundation.
## http://www.gnu.org/copyleft/gpl.html
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##

# WikiSh depends on SecLayer.php for the final layer of security.  WikiSh
# uses 8 different possible authorizations:
#   read      - allow reading of page/file
#   insert    - allow lines to be inserted between existing lines of text
#   append    - allow lines to be added at the end, after existing text
#   prepend   - allow lines to be added at the beginning of existing text
#   overwrite - allow an entire page (or part of a page) to be overwritten
#   create    - allow new pages to be created
#   delete    - allow deletion of a page
#   attr      - allow changing of attributes (chmod)
#   forceread - bypass pmwiki auth when editing this page
#   forceedit - bypass pmwiki auth when editing this page
# These permissions will be placed in $wshAuthPage (for wiki pages) and
# $wshAuthText (for textfile access).  Typically slParsePage will get these
# from Site.WikiShAuth#page and Site.WikiShAuth#text although an administrator
# could place them elsewhere.

$RecipeInfo['WikiSh']['Version'] = '2015-06-05';

define(WikiSh, true);
define(WIKIPAGEID, 'WIKIFILE--');
define(TEXTFILEID, 'TEXTFILE--');
define(SESSFILEID, 'SESSION--');
define(BADFILEID, 'BADFILE--');

SDV($EnableWikiShWritePage,     false);
SDV($EnableWikiShCreatePage,    false); // requires WritePage as well
SDV($EnableWikiShOverwritePage, false); // requires WritePage as well
SDV($EnableWikiShRemove,        false); // requires WritePage AND OverwritePage
SDV($EnableWikiShRemoveFully,   false); // requires Remove
SDV($EnableWikiShTextRead,      false);
SDV($EnableWikiShTextWrite,     false);
SDV($EnableWikiShPlayNicely,    false); // This cannot be set after inclusion
SDV($EnableWikiShRawEcho,       false); // Security risk - only for debugging
SDV($EnableWikiShChmod,         false);
SDV($EnableWikiShMailx,         false);
SDV($EnableWikiShDebug,         false);
SDV($EnableWikiShFetchmail,     true);
$WikiShMXPrefix = ($enableWikiShPlayNicely ? "wikish_" : "");
SDV($WikiShVars['ACTIVE'],      true);
SDV($WikiShVars['HIST_PREFIX'],  "(:linebreaks:)\n");
SDV($WikiShVars['FAVSFILE'],    "Favorites"); // default to current group
SDV($WikiShVars['HISTFILE'],    "History");   // default to current group
SDV($WikiShVars['HISTSIZE'],    100);
SDV($WikiShVars['NOHIST'],      false);
SDV($WikiShVars['AUTHOR'],      "WikiSh");        // default author
SDV($WikiShControlPage,         "WikiShControl"); // default is in current group
SDV($WikiShVars['DEFAULT_DEBUG'],  5); // Print debug statements of this level
                                       // and below. 1=detailed, 5=nothing
SDV($WikiShVars['DEBUGLEVEL'],     $WikiShVars['DEFAULT_DEBUG']);
SDV($WikiShVars['DEBUG_OD'],       false); // utilize wshDbgOd()
SDV($WikiShVars['RC_DEBUG'],       5);    // normally no debug during RC
SDV($WikiShVars['RANDOM_MIN'],     0);
SDV($WikiShVars['RANDOM_MAX'],     32768);
SDV($WikiShVars['PAGEVARS'],       'post'); // "pre" "post" "prepost" "postpre"
SDV($WikiShVars['LIST'],           '');     // like pagelist's list=X
SDV($WikiShVars['MAILXVARS'],      array('include', 'comment', 'if')); // rules
                                            // to run on mailx pages
SDV($WikiShRCPages, array("WikiSh.Profile", "WikiSh.WikiShRC", 'WikiSh.{$Group}-GroupRC', 'WikiSh.{$Group}-{$Name}'));
SDV($WikiShRCRules, array('include', 'comment', 'if'));
$WikiShVars['SECONDS_START'] = microtime(true);
$WikiShVars['SECONDSLEFT_START'] = microtime(true);
$WikiShPipeBucket = '';
$WikiShWriting = false; // this will tell us if we are in process of an 
                        // UpdatePage() call
SDV($WikiShSessionPagePat, array("/^session\./i", "/^virtual\./i", "/^te?mp\./i"));
#$InputAttrs[] = 'style';
#$InputAttrs[] = 'width';

Markup_e('${var}', '>{(',
  '/(\\$\\{[^{}]*\\})/',
  "wshExpand1Var(\$pagename, \$m[1])");

# This alternate markup to invoke MXes allows interaction with $FmtPV *before*
# the variables are interpolated.  Use as {earlymx(set --pv ...)}.
Markup_e('{earlymx(', '<{$var}',
  '/\\{earlymx(\\(\\w+\\b.*?\\))\\}/',
  "MarkupExpression(\$pagename, \$m[1])");
Markup_e('{latemx(', '>if',
  '/\\{latemx(\\(\\w+\\b.*?\\))\\}/',
  "MarkupExpression(\$pagename, \$m[1])");

# This function allows non-WikiSh MXes (or other functions) to take advantage
# of some of the WikiSh utility functions for variable substitution, option
# setting, wildcard expansion, etc.  Piping is implemented through the option
# setting (usually with --xargs), some of the other generic options.
#
# Basically if this is *not* done then people have to use opt=val syntax instead
# of --opt:val, they will not have variables expanded, and they won't have
# wildcard expansion.
#
# The $action is bit-wise controlled using the constants defined below.  The
# default has wshInitOpts() being called and wshExpandVars() being called within that
# but *not* wildcards being expanded.
define(WIKISH_ACTION_OPTIONS,  0x01); // call wshInitOpts()
define(WIKISH_ACTION_VARIABLE, 0x02); // call wshExpandVars()
define(WIKISH_ACTION_WILDCARD, 0x04); // call wshExpandWildCards()
define(WIKISH_ACTION_DEFAULT, WIKISH_ACTION_VARIABLE|WIKISH_ACTION_OPTIONS);
function WikiShCompatible($pagename,&$opt,&$args,$action=WIKISH_ACTION_DEFAULT,$optlist='')
{
    if ($action & WIKISH_ACTION_OPTIONS) 
        wshInitOpts($pagename,$optlist, $opt, $args, 
            ($action & WIKISH_ACTION_VARIABLE));
    if ($action & WIKISH_ACTION_WILDCARD) 
        wshExpandWildCards($pagename, $opt, $args);
}

# This markup allows forms to automatically update $InputVals (to keep the same
# values displayed on the form between submissions) and $WikiShVars[] (to 
# allow use of those values in the rest of the page).  
# OPTIONS:
#   method=GET
#   method=POST
#   target="Group.Page"
#   formname=<somename>
# ARGUMENTS:
#   QUICKFORM - automatically output the "(:input form ...:)" and 
#               "(:input hidden name=n ...:)" directives for a quick form
#   PROCESS   - process the fields upon submission to maintain the values
#               in the fields and make them available as WikiShVars
#   REDIRECT  - redirect to another page (specified by target option above)
$MarkupExpr["wikish_form"] = 'wshForm($pagename, @$argp, @$args)'; 
function wshForm($pagename, $opt, $args) 
{
    global $InputValues, $FmtPV, $WikiShVars, $PageTextVarPatterns, 
        $PageExistsCache;
    $func = 'wshForm(' . implode(" ", $args) . ')';
    wdbg(4,"$func: Entering");
    SDV($opt['method'], 'GET');
    $rtn = '';
    foreach ($args as $arg) {
        $arg = strtoupper($arg);
        switch ($arg) {
        case 'QUICKFORM':
            $rtn  = "(:input form method=$opt[method]";
            if ($opt['formname']) $rtn .= " name=$opt[formname]";
            $rtn .= ":)\n";
            $rtn .= "(:input hidden name=n value={\$FullName}:)\n";
            break;
        case 'REDIRECT':
            $target = $opt['target'];
            if (!$target) {
                $WikiShVars['STATUS'] = 2;
                wshStdErr($pagename, $opt, "ERROR: No target specified");
                return(false);
            }
            wshExpandVars($pagename, $opt, $target);
            if (substr($target,0,4) == 'http') {
                Redirect($pagename, PSS($target));
            } else {
                preg_match("/\?action=\S+/", $target, $m);
                $tgt_pg = wshMakePageName($pagename, $opt, $target);
                $target = $tgt_pg . $m[0];
                unset($PageExistsCache[$tgt_pg]);
                if (!$opt['newpage'] && !PageExists($tgt_pg)) {
                    $WikiShVars['STATUS'] = 1;
                    wshStdErr($pagename, $opt, "ERROR: Page $tgt_pg does not exist.  Cannot redirect.");
                    return(false);
                }
                Redirect($target);
            }
            // Actually we never get here because Redirect does exit()
            return;
            break;
        case 'PROCESS':
            foreach ($_REQUEST as $k=>$v) {
               if (is_array($v)) {
                   $foo = '';
                   foreach ($v as $i=>$val) {
                       $tmp = htmlspecialchars($val,ENT_NOQUOTES);
                       $InputValues[$k][$i] = stripmagic($tmp);
                       $foo .= (($foo)?',':'') . $tmp;
                   }
               } else {
                   $foo = htmlspecialchars($v,ENT_NOQUOTES);
                   # This keeps the field values current with the form from 
                   # submission to submission, but has nothing to do with PTV
                   $InputValues[$k] = stripmagic($foo);
               }
               $WikiShVars[$k] = stripmagic($foo);
               #echo "InputValue[$k]=$InputValues[$k], WikiShVars[$k]=$WikiShVars[$k]<br>\n";
            }
            if ($opt['source']) {
                $source = WIKIPAGEID.wshMakePageName($pagename, $opt, $opt['source']);
                $page = wshReadPage($pagename, $opt, $source);
                foreach ((array)$PageTextVarPatterns as $ptvpat) {
                    if (preg_match_all($ptvpat, $page['text'], $match, PREG_SET_ORDER)) {
                        foreach ($match as $m) {
                            if (!isset($_REQUEST[$m[2]])) {
                                $InputValues[$m[2]] = 
                                    $WikiShVars[$m[2]] = stripmagic($m[3]);
                            }
                        }
                    }
                }
            }
            break;
        default:
            break;
        }
    }
    return($rtn);
}

Markup_e('wshFunction', '<{(',
  '/\\{\\(function\\b\\s*(.*?)\\)\\}/i',
  "wshStoreFunction(\$pagename, \$m[1])");
function wshStoreFunction($pagename, $expr)
{
    global $wshFunctionList, $MarkupExpr;
    $func = 'wshStoreFunction()';
    wdbg(4,"$func: Entering expr=$expr");
    $pieces = preg_split("/\s+/", $expr);
    wdbg(1,"$func: pieces=".print_r($pieces,true));
    $funcname = strtolower(ltrim(array_shift($pieces)));
    $wshFunctionList[$funcname] = implode(" ", $pieces);
    wdbg(3,"$func: Storing $funcname() as \"$wshFunctionList[$funcname]\"");
    $MarkupExpr[$funcname] = "wshDoFunction(\$pagename, '$funcname', \$params, \$exiting)";
    return('');
}

function wshDoFunction($pagename, $funcname, $expr, &$exiting)
{
    global $wshFunctionList, $MarkupExpr, $WikiShVars;
    $func = 'wshDoFunction()';
    $opt = array();
    if ($expr{0} == ' ')
        $expr = substr($expr, 1); // get rid of leading space
    wdbg(4,"$func: Entering with funcname=$funcname, expr=$expr");
    # Need to check for OUTSPEC and get set for that...
    # If you want flags to get through to your function then you need to precede
    # them with a -- (or the first one as \-)
    # foreach (preg_split("/\\s+/", $args, -1, PREG_SPLIT_NO_EMPTY) as $arg) {
    $args = explode(" ", $expr);
    wshInitOpts($pagename, '', $opt, $args);
    # Set up ${0}, ${1}, etc as positional paramaters
    $WikiShVars[0] = $funcname;
    $i = 1;
    if ($args) {
        foreach ($args as $arg) {
            wdbg(2,"$func: arg#${i}=$arg");
            $WikiShVars[$i++] = $arg;
        }
    }
    wdbg(0,$WikiShVars);
    $WikiShVars['#'] = $i - 1;
    for ( ; $i <= 9; $i++) unset($WikiShVars[$i]);
    $newexpr = $wshFunctionList[$funcname];
    wdbg(2,"$func: Attempting to run expr=>>$newexpr<<");
    $tmp = MarkupExpression($pagename, '(' . $newexpr . ')');
    wdbg(1,"$func: Return value was >>$tmp<<");
    return(wshPostProcess($pagename, $opt, explode("\n", $tmp)));
}

# WikiSh MX implements overall control including 
#    if BOOL-EXPR; then EXPR; [EXPR; ...] [else EXPR; [EXPR; ...]] fi;
#    while BOOL-EXPR; do EXPR; [EXPR; ...] done;
#    for VAR in FILE... do ... done
# Note that BOOL-EXPR is any normal expression which sets $WikiShVars['STATUS']
# to 0 for success or non-zero for failure.  Typically this would be the WikiSh
# functions: 
#    true
#    false
#    grep (true=found something, false=found nothing)
#    test (see documentation of that function)
#    [ ... ] (synonym for test - see documentation of that function)
#    {any other WikiSh function will return success unless there is an attempt
#    to write to a file (via stdout=, etc.) and that write fails or unless 
#    there is an attempt to read a file (for instance a text-file) and the
#    read attempt fails due to lack of permissions, etc.}
#       rm (true=success, false=failure for any reason)
#       sed (true=success, false=failure to write via stdout= or -i)
#       cat (true=success, false=failure to write via stdout=)
#       etc.
#
# while ($x = one expression (anchored at start, semi-colon terminated)) {
#    $cmd = first word of $x
#    switch ($cmd) {
#    if) $x = whole if statement, anchored at start, "fi\s*;" terminated
#        get testexpr, trueexpr, elseexpr
#        $negate = (!?) on testexpr
#        $rtn = testexpr
#        if (!$negate && $rtn)
#           $runexpr=$trueexpr
#        else
#           $runexpr=$elseexpr
#        process through $runexpr and run each
#    for)
#        $x = whole for statement, anchored at start, "done\s*;" terminated
#        wshExpandWildCards for $x (also variable substitution)
#        get VAR, FILELIST, EXPR-LIST
#        foreach (FILELIST as $WikiShVars[$VAR]) {
#           process through $EXPR-LIST and run each
#        }
#    unknown)
#        run the $x
#    }
#    get rid of $x off the front of the expression
# }
$MarkupExpr['wikish'] = 'WikiSh($pagename, $params)'; 
function WikiSh($pagename, $expr)
{
    global $exiting, $returning;

    if (wshNotNow($pagename)) return(Keep('', 'P'));
    SDV($exiting, false);
    SDV($returning, false);
    wdbg(4,"WikiSh(): Entering");
    wshUnHTMLSpecialChars($expr);
    $exiting = false;
    $continue = $break = 0;
    # Check for the option --expandvars
    if (($pos = strpos($expr, '--expandvars')) !== FALSE && $pos <= 1) {
        $expr = wshExpand1Var($pagename, $expr);
        $expr = substr($expr, $pos+strlen('--expandvars'));
    }
    // this needs to be split out like this because WikiShRunExpr calls itself
    // recursively and the Keep() at the end of THIS function messes things up
    wdbg(1,"WikiSh(): Calling WikiShRunExpr($expr)");
    $rtn = WikiShRunExpr($pagename, $expr, $exiting, $break, $continue);
    if ($exiting && $returning) $exiting = $returning = false;
    wdbg(4,"WikiSh(): Exiting");
    return(Keep($rtn, 'P'));
}

function WikiShRunExpr($pagename, $expr, &$exiting, &$break, &$continue)
{
    global $WikiShVars;
    static $Level=-1;
    $Level++;
    $func = "WikiShRunExpr($Level)";
    wdbg(4,"$func: Entering");
    wdbg(3,"$func: expr=$expr");
    # if EXPR; then; EXPR; [...] [else; EXPR; [...]] fi
    $iffi = '(?>if\s+)([^;]+)(?>;\s*)' .
                '(?>then\s*;?\s*)' .
                  '((?:(?:(?:(?!\bif\b)(?!\belse\b)(?!\bfi\b).)+)|(?R)+)+)\s*' .
             '(?:' .
                '(?>else\s*;?\s*)' .
                  '((?:(?:(?:(?!\bif\b)(?!\belse\b)(?!\bfi\b).)+)|(?R)+)+)' .
             ')?\s*' .
             '(?>fi;?\s*)';
    $dodonere= '(?>(?:while|for))\s+([^;]+);\s*'.
                   '(?>do\s*;?\s*)'.
                     '('.
                       '(?:'.
                         '(?:(?!\bfor\b)(?!\bwhile\b)(?!\bdone\b).)+'.
                         '|'.
                         '(?R)'.
                       ')+'.
                     ')\s*(?>done\s*;?\s*)';
    # source FILE;
    $sourceit  = "^source\s+([-$}{\w.#:\/]+)\s*([^;]*);?\s*";

    $rtn = '';
    $expr = preg_replace("/^\s*/", "", $expr); // get rid of initial spaces
    $lastexpr = 'x' . $expr;
    while (!$exiting && !$break && !$continue && $expr && $lastexpr != $expr) {
        wdbg(3,"$func: Current wikish chunk: >>$expr<<");
        $lastexpr = $expr;
        if (preg_match("/^if\b/", $expr) && preg_match("/$iffi/", $expr, $match)) {
            list($foo, $testexpr, $ifexpr, $elseexpr) = $match;
            wdbg(3,"$func: IF-THEN-ELSE MATCHED: $foo");
            wdbg(1, $match);
            wdbg(2,"$func: testexpr=$testexpr");
            wdbg(2,"$func: ifexpr=$ifexpr");
            wdbg(2,"$func: elseexpr=$elseexpr");
            $rtn = wshCS($rtn, WikiShRunExpr($pagename, $testexpr, $exiting, $break, $continue));
            wdbg(2,"$func: STATUS=$WikiShVars[STATUS]");
            if (!$exiting && !$break && !$continue) {
                if ($WikiShVars['STATUS'] == 0) {
                    wdbg(2,"$func: running ifexpr=$ifexpr");
                    $rtn = wshCS($rtn, WikiShRunExpr($pagename, $ifexpr, $exiting, $break, $continue));
                } else {
                    wdbg(2,"$func: running elseexpr=$elseexpr");
                    $rtn = wshCS($rtn, WikiShRunExpr($pagename, $elseexpr, $exiting, $break, $continue));
                }
            }
        } elseif (preg_match("/^for\b/", $expr) && preg_match("/$dodonere/", $expr, $match)) {
            list($foo, $tmp, $doexpr) = $match;
            # $tmp = "VAR in LIST"
            if (!preg_match("/(\w+)\s+in\s+(\S.*)$/", $tmp, $match)) {
                wdbg(4,"$func: ERROR: preg_match should not have failed >>$tmp<<");
                $Level--;
                return ('');
            }
            list($tmp, $varname, $list) = $match;
            wdbg(3,"$func: FOR-IN-DO-DONE MATCHED: $foo");
            wdbg(2,"$func: varname=$varname");
            wdbg(2,"$func: list=$list");
            wdbg(2,"$func: doexpr=$doexpr");
            $list = array($list);
            wshExpandVars($pagename, $opt, $list);
            $list = explode(" ", implode(" ", $list)); // yes, I mean that.
            wshExpandWildCards($pagename, array(), $list, true, true, true);
            foreach ($list as $l) {
                $WikiShVars[$varname] = $l;
                $rtn = wshCS($rtn, WikiShRunExpr($pagename, $doexpr, $exiting, $break, $continue));
                if ($continue) {
                    $continue--;
                    if ($continue) break;
                }
                if ($exiting) break;
                if ($break) { $break--; break; }
            }
        } elseif (preg_match("/^while\b/", $expr) && preg_match("/$dodonere/", $expr, $match)) {
            list($foo, $testexpr, $doexpr) = $match;
            wdbg(3,"$func: WHILE-DO MATCHED: $foo");
            wdbg(2,"$func: testexpr=$testexpr");
            wdbg(2,"$func: doexpr=$doexpr");
            $rtn = wshCS($rtn, WikiShRunExpr($pagename, $testexpr, $exiting, $break, $continue));
            while (!$exiting && $WikiShVars['STATUS'] == 0 && !$continue && !$break) {
                $rtn=wshCS($rtn, WikiShRunExpr($pagename, $doexpr, $exiting, $break, $continue));
                if ($break) { $break--; break; }
                if ($continue) {
                    $continue--;
                    if ($continue) break;
                }
                if (!$exiting)
                    $rtn=wshCS($rtn, WikiShRunExpr($pagename, $testexpr, $exiting, $break, $continue));
            }
        } elseif (preg_match("/$sourceit/", $expr, $match)) {
            list($foo, $sourcefile, $args) = $match;
            wdbg(3,"$func: SOURCE MATCHED: $foo");
            wdbg(2,"$func: sourcefile=$sourcefile");
            $sourcefile = wshExpand1Var($pagename, $sourcefile);
            $WikiShVars[0] = $sourcefile;
            $i = 1;
            if ($args) {
                foreach (explode(" ", $args) as $arg) {
                    wdbg(2,"$func: arg#${i}=$arg");
                    $WikiShVars[$i++] = $arg;
                }
            }
            $WikiShVars['#'] = $i - 1;
            wdbg(1,$WikiShVars);
            $page = wshReadPage($pagename, array(), WIKIPAGEID . $sourcefile, true);
            $code = wshParseCode(wshUnHTMLSpecialChars($page['text']));
            wdbg(2,"$func: Got this code: $code");
            $expr = substr($expr, 0, strlen($foo)) . $code . 
                substr($expr, strlen($foo));
            wdbg(1,"$func: Expr became this: $expr");
        } elseif (preg_match("/^([^;]*)\s*\|\s*(?=while)/", $expr, $match)) {
            list($foo, $doexpr) = $match;
            wdbg(3,"$func: PIPE TO WHILE EXPR MATCHED: $foo");
            wdbg(2,"$func: doexpr=$doexpr");
            $looprtn = '';
            WikiShRunExprNode($pagename, $doexpr, $looprtn, $exiting, $break, $continue, true);
            $rtn .= $looprtn;
        } elseif (preg_match("/^([^;]*)\s*;?\s*/", $expr, $match)) {
            list($foo, $doexpr) = $match;
            wdbg(3,"$func: SIMPLE EXPR MATCHED: $foo");
            wdbg(2,"$func: doexpr=$doexpr");
            WikiShRunExprNode($pagename, $doexpr, $rtn, $exiting, $break, $continue);
        }
        wdbg(1,"$func: Before stripping off just-processed chunk=>>$expr<<");
        wdbg(1,"$func: Stripping off >>$foo<< (len=" . strlen($foo) . ")");
        $expr = substr($expr, strlen($foo));
        wdbg(1,"$func: Looping back with chunk=>>$expr<<");
    }
    $Level--;
    return($rtn);
}

#WikiShConcatenateStrings
function wshCS($str1, $str2)
{
    return ($str1 . (($str1 && $str2) ? "\n" : "") . $str2);
}

# This function accepts |, &&, ||, and semi-colon-delimited lists of commands
# and executes them.
# It also processes back-quote-delimited expressions.
#
# If you are trying to read this code, my apologies.  The section regarding
# backquotes has become one of the messiest sections of WikiSh.  The problem
# is quotes within quotes and the possibility of quotes coming from data that
# should not be recognized, but other quotes need to be recognized and etc.
function WikiShRunExprNode($pagename, $expr, &$rtn, &$exiting, &$break, &$continue, $grab_pipe=false)
{
    global $WikiShPipeText, $WikiShPipeActive;
    global $WikiShVars, $returning;
    global $KeepToken, $KPV, $MarkupExpr;
    $func = "WikiShRunExprNode($expr)";
    wdbg(4, "$func: Entering");
    $rpat = "/$KeepToken(\\d+P)$KeepToken/e";
    $rrep = 'wshUnHTMLSpecialChars($KPV[\'$1\'], true)';
    # Restore "kept" strings within quotes
    $expr = preg_replace($rpat, "'".CHR(1)."'.".$rrep.".'".CHR(1)."'", $expr);
    while (preg_match("/`([^`]+)`/", $expr, $m)) {
        # change all single- and double- quotes into chr(2) and chr(3)
        # expand variables
        # do a keep as if the chr(2/3) were single-/double-quotes
        # replace any remaining chr(2/3) characters back to quotes
        # run the markup expression found in the backquotes
        # replace in the big-picture expression the results of the backquote
        $tmp='';
        wdbg(3,"$func: Expanding vars on \"$m[1]\"");
        $m[1] = str_replace(array("'", '"'), array(CHR(2), CHR(3)), $m[1]);
        wshExpandVars($pagename, array(), $m[1]);
        $m[1] = preg_replace("/([\\002\\003])([^\\1]*?)\\1/e", "Keep(PSS(str_replace(array(CHR(2), CHR(3)), array(\"'\", '\"'), '$2')),'P')", $m[1]);
        $m[1] = str_replace(array(CHR(2), CHR(3)), array("'", '"'), $m[1]);
        wdbg(3,"$func: Running backquotes on \"$m[1]\"");
        WikiShRunExprNode($pagename, $m[1], $tmp, $exiting, $break, $continue);
        wdbg(3,"$func: Replacing >>$m[1]<< with >>$tmp<<");
        $expr = str_replace($m[0], $tmp, $expr);
        wdbg(3,"$func: Resulting expr = \"$expr\"");
    }
    wshExpandVars($pagename, array(), $expr);
    # "Keep" quoted strings again
    $expr = preg_replace('/\001([^\001]*)\001/e', "Keep(PSS('$1'),'P')", $expr);
    $exprlist = preg_split("/\s*;\s*/", $expr, -1, PREG_SPLIT_NO_EMPTY);
    foreach ($exprlist as $expr) {
        wdbg(2,"$func: Processing expr=>>" . wshDbgOd($expr) . "<<");
        $explist = preg_split("/\s*(\|\||&&)\s*/", $expr, -1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
        $exlist = array(); $lastop = '';
        foreach ($explist as $exp) {
            if ($exp == '||' || $exp == '&&') {
                $lastop = $exp;
                continue;
            }
            $exlist[] = $lastop . $exp;
        }
        foreach ($exlist as $ex) {
            wdbg(2,"$func: Shall we process ex=$ex");
            $First2 = substr($ex, 0, 2);
            if (($First2 == '&&' && $WikiShVars['STATUS'] != 0) ||
                ($First2 == '||' && $WikiShVars['STATUS'] == 0))
                continue;
            if ($First2 == '&&' || $First2 == '||')
                $ex = substr($ex, 2);
            wdbg(2,"$func: Checking negation ex=>>$ex<<");
            wdbg(2,"$func: ex=" . wshDbgOd($ex));
            if (!preg_match("/^\s*(!)?\s*(.*)$/s", $ex, $match))
                wshStdErr($pagename, $opt, "ERROR: $func: Non-matching regex for negation.  Attempting to continue");
            $negate = $match[1];
            $ex = $match[2];
            wdbg(2,"$func: Processing ex=$ex");
            $elist = preg_split("/\s*\|\s*/", $ex, -1, PREG_SPLIT_NO_EMPTY);
            foreach ($elist as $e) {
                wdbg(2,"$func: Processing e=$e");
                if (preg_match("/^\s*exit(?:\s+(\d+))?\s*$/", $e, $m)) {
                    wdbg(4,"$func: Found exit. Getting out of dodge...");
                    if (!$m[1] || $m[1]===0) $WikiShVars['STATUS'] = $m[1];
                    $exiting = true;
                    break 3;
                } elseif (preg_match("/^\s*return(?:\s+(\d+))?\s*$/", $e, $m)) {
                    wdbg(4,"$func: Found RETURN. Getting out of dodge...");
                    if (!$m[1] || $m[1]===0) $WikiShVars['STATUS'] = $m[1];
                    $exiting = true;
                    $returning = true;
                    break 3;
                } elseif (preg_match("/^\s*continue(?:\s+(\d+))?\s*$/", $e, $m)) {
                    wdbg(4,"$func: Found continue. Getting out of dodge...");
                    if ($m[1]) $continue = $m[1];
                    else $continue = 1;
                    break 3;
                } elseif (preg_match("/^\s*break(?:\s+(\d+))?\s*$/", $e, $m)) {
                    wdbg(4,"$func: Found break. Getting out of dodge...");
                    if ($m[1]) $break = $m[1];
                    else $break = 1;
                    break 3;
                } else {
                    $x = array($e);
                    wshExpandVars($pagename, array(), $x);
                    $e = $x[0];
                    if (preg_match('/^\\s*(\\w+)\\b/', $e, $match) && !@$MarkupExpr[$match[0]]) {
                        $tmp = "UNKNOWN MARKUP EXPRESSION COMMAND: $match[0]";
                        $WikiShVars['STATUS'] = 1;
                    } else {
                        wdbg(2,"$func: Really processing e=" . wshDbgOd($e));
                        $tmp = MarkupExpression($pagename, '(' . $e . ')');
                    }
                    $WikiShPipeText = $tmp;
                    $WikiShPipeActive = true;
                }
            }
            if ($negate)
                $WikiShVars['STATUS'] = !$WikiShVars['STATUS'];
            $WikiShPipeActive = $WikiShPipeText = false;
        }
        if ($rtn) 
            $rtn .= "\n" . $tmp;
        else 
            $rtn = $tmp;
        wdbg(2,"$func: rtn til now >>$rtn<<");
    }
    if ($grab_pipe) {
        wdbg(3,"$func: rtn being put in pipe");
        $WikiShPipeText = $rtn;
        $WikiShPipeActive = true;
        $rtn = '';
    }
}

# Basename()
# OPTION:
#   --raw   -- simply apply the PHP function basename() to the input
# Normally apply basename() to text files and PageVar(..., $Name) to all others
$MarkupExpr["${WikiShMXPrefix}basename"] = 'wshBasename($pagename, @$argp, @$args)'; 
function wshBasename($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func="Basename()";
    wdbg(4,"$func: " . implode(" ", $args));
    WikiShCompatible($pagename,$opt,$args); // handle wshInitOpts & wshExpandWildCards
    wdbg(1,"$func: arg0=$args[0]");
    if (@$opt['raw'] or wshIsATextFile('', $args[0])) {
        if (wshIsATextFile('', $args[0]))
            $fn = substr($args[0], strlen(TEXTFILEID));
        else $fn = $args[0];
        $name = basename($fn);
        wdbg(1, "$func: name=$name, fn=$fn");
    } else $name = PageVar($args[0], '$Name');
    if ($name) $WikiShVars['STATUS'] = 0;
    else $WikiShVars['STATUS'] = 1;
    return ($name);
}

# {(cat OPTIONS PAGEPATTERN)} 
# conCATenate all lines of all files passed
# Options: 
# PAGEPATTERN...  - source pages from PageName or Group.Pagename 
#       allowing wiki wildcards * and ?
#       (multiple files/patterns allowed)
$MarkupExpr["${WikiShMXPrefix}cat"] = 'wshCat($pagename, @$argp, @$args)'; 
function wshCat($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func="Cat()";
    wdbg(4,"Cat(): Entering");
    wdbg(1,$args);
    wshInitOpts($pagename, '', $opt, $args);
    wshExpandWildCards($pagename, $opt, $args, false, false, false);
    wshSDOpt($opt, 'file_prefix', '');
    wshSDOpt($opt, 'line_prefix', '');
    wshSDOpt($opt, 'line_suffix', '');
    $newrows = array();
    foreach ($args as $filename) {
        $page = wshReadPage($pagename, $opt, $filename);
        if (wshIsABadFile($page)) {
            wshStdErr($pagename, $opt, "ERROR: $func: No such page: $page[filename]");
            continue;
        }
        $textrows = explode("\n", $page['text']);
        wdbg(1,"Cat: textrows=");
        wdbg(1,$textrows);
        if ($opt['file_prefix'])
            $array_prefix = array(wshReplace($opt, $page, $opt['file_prefix']));
        else
            $array_prefix = array();
        $line_prefix = wshReplace($opt, $page, $opt['line_prefix']);
        $line_suffix = wshReplace($opt, $page, $opt['line_suffix']);
        if ($line_prefix || $line_suffix)
            for ($i = 0; $i < sizeof($textrows); $i++) {
                if (strstr($opt['line_prefix'], 'LINENO')) 
                    $line_prefix = wshReplace($opt, $page, $opt['line_prefix'], $i+1);
                $textrows[$i] = $line_prefix . $textrows[$i] . $line_suffix;
            }
        $newrows = array_merge($newrows, $array_prefix, $textrows);
    }
    $WikiShVars['STATUS'] = 0;
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

# Chmod()
# {(chmod read='secret' edit='secret' attr='secret' page(s))}
# Note that wikish Chmod is pretty far removed from shell chmod and does NOT
# support TEXTFILE-- files and permissions.
# OPTIONS:
# --read:pw    Specify a read passwd
# --edit:pw    Specify an edit passwd
# --attr:pw    Specify an attr passwd
# --upload:pw  Specify an attr passwd
# -a           The new passwd(s) will be ADDED to existing rather than replacing
# -p           Work with page authorizations (default if neither -p nor -g 
#              specified)
# -g           Work with group authorizations (Group.GroupAttributes)
$MarkupExpr["${WikiShMXPrefix}chmod"]='wshChmod($pagename, @$argp, @$args)';
function wshChmod($pagename, $opt, $args)
{
    global $WikiShVars, $GroupAttributesFmt, $EnableWikiShChmod, $wshPmAuthList;
    if (wshNotNow($pagename)) return('');
    $func = 'Chmod()';
    SDV($wshPmAuthList, array('read', 'edit', 'attr', 'upload'));
    if (!$EnableWikiShChmod) {
        wshStdErr($pagename, $opt, "ERROR: $func: not enabled.  See \$EnableWikiShChmod");
        return('');
    }
    SDV($GroupAttributesFmt,'$Group/GroupAttributes');
    WikiShCompatible($pagename,$opt,$args,WIKISH_ACTION_VARIABLE|WIKISH_ACTION_OPTIONS|WIKISH_ACTION_WILDCARD); // handle wshInitOpts & wshExpandWildCards
    if (!@$opt['g']) $opt['p'] = true; // default is page level auth
    if (!isset($opt['read']) && !isset($opt['edit']) && !isset($opt['attr']) && !isset($opt['upload'])) {
        wshStdErr($pagename, $opt, "ERROR: $func: Must specify at least one password.  No password specified.");
        $WikiShVars['STATUS'] = 4;
        return('');
    }
    if ($opt['p']) {
        $newargs = $args;
    } else {
        $newargs = array();
    }
    if ($opt['g']) {
        foreach ($args as $a) {
            $ga = FmtPagename($GroupAttributesFmt, $a);
            if (!in_array($ga, $newargs))
                $newargs[] = $ga;
        }
    }
    foreach ($wshPmAuthList as $plev) {
        if (isset($opt[$plev])) {
            $pw[$plev] = '';
            foreach ((array)$opt[$plev] as $pass) 
                foreach (explode(" ", $pass) as $p)
                    $pw[$plev] .= ' ' . 
                        (preg_match('/^(@|clear|\\w+:)/',$p) ? $p : crypt($p));
            $pw[$plev] = ltrim($pw[$plev]);
        } else $pw[$plev] = 'ignore';
    }
    foreach ((array)$newargs as $filename) {
        wdbg(1,"$func: Processing $filename");
        $page = $newpage = wshReadPage($pagename, $opt, $filename);
        if ($page['type'] != 'wiki') {
            wshStdErr($pagename, $opt, "ERROR: Chmod acts only on wiki pages. Invalid pagename: $filename (type=$page[type])");
            continue;
        }
        // Set the various passwords in the $page array
        foreach ($wshPmAuthList as $plev) {
            if ($pw[$plev] != 'ignore') {
                if (@$opt['a'])
                    $newpage['passwd'.$plev] .= ' ' . $pw[$plev];
                else
                    if ($pw[$plev] == 'clear' || !$pw[$plev])
                        unset($newpage['passwd'.$plev]);
                    else
                        $newpage['passwd'.$plev] = $pw[$plev];
            }
        }
        // Now write the page
        if (!wshWrite($pagename, $opt, $filename, $page['type'], $newpage, $page, 'attr', 'attr')) {
            $WikiShVars['STATUS'] = 2;
            return('');
        }
    }
}

$MarkupExpr["${WikiShMXPrefix}cp"] = 'wshCp($pagename, @$argp, @$args)';
function wshCp($pagename, $opt, $args)
{
    global $EnableWikiShWritePage, $EnableWikiShOverwritePage, $EnableWikiShCreatePage;
    global $WorkDir;
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = "cp(" . implode(" ", $args) . ")";
    wdbg(4,"$func: Entering");
    SDV($EnableWikiShWritePage, false);
    SDV($EnableWikiShOverwritePage, false);
    SDV($EnableWikiShCreatePage, false);
    if (!$EnableWikiShWritePage) {
        wshStdErr($pagename, $opt, "ERROR: $func: Unable to write pages.  Not enabled.");
        $WikiShVars['STATUS'] = 4;
        return ('');
    }
    wshInitOpts($pagename, '', $opt, $args);
    $Target = array_pop($args); // strip off the target before expanding to 
                                // avoid Group being rewritten as GroupA.Group
    wshExpandWildCards($pagename, $opt, $args);
    wshSDOpt($opt, 'q', false);
    wshSDOpt($opt, 'trial', false);
    if (wshIsATextFile('', $Target)) {
        $Target = substr($Target, strlen(TEXTFILEID));
        $Target_Type = 'text';
        if (file_exists($Target))
            $TargetIsDir = is_dir($Target);
        else {
            $TargetIsDir = false;
            if (sizeof($args) > 1) {
                wshStdErr($pagename, $opt, "ERROR: $func: Cannot copy multiple files to non-directory destination $Target");
                $WikiShVars['STATUS'] = 2;
                return ('');
            }
        }
    } else {
        $Target_Type = 'wiki';
        if (!strstr($Target, '.') && !strstr($target, '/'))
            $TargetIsDir = true;
    }
    if (sizeof($args) > 1 && !$TargetIsDir) {
        wshStdErr($pagename, $opt, "ERROR: $func: Cannot copy multiple files to non-" . (($Target_Type == 'wiki') ? "group" : "directory") . " destination $Target");
        $WikiShVars['STATUS'] = 2;
        return ('');
    }
    if (sizeof($args) < 1) {
        wshStdErr($pagename, $opt, "ERROR: $func: No Source file specified to copy to destination $Target");
        $WikiShVars['STATUS'] = 2;
        return ('');
    }
    $newrows = array();
    foreach ($args as $filename) {
        wdbg(1,"$func: Processing $filename");
        $page = wshReadPage($pagename, $opt, $filename);
        if (wshIsABadFile($page)) {
            wshStdErr($pagename, $opt, "ERROR: $func: Cannot copy source file $page[filename].  Error reading.");
            $WikiShVars['STATUS'] = 6;
            return;
        }
        if ($page['filename'] == '-' && $page['type'] == 'inline' && $TargetIsDir) {
            wshStdErr($pagename, $opt, "ERROR: $func: Cannot copy inline file to directory destination $Target");
            continue;
        }
        if ($TargetIsDir) {
            if ($Target_Type == 'wiki') {
                $tmp = preg_replace("/^.*[\/\.]/", "", $page[filename]);
                $tgt = "$Target.$tmp";
            } else
                $tgt = "$Target/$page[filename]";
            wdbg(1,"$func: Created target name $tgt");
        }
        else
            $tgt = $Target;
        wdbg(1,"$func: Target set to $tgt");
        if ($page['filename'] == $tgt) {
            wshStdErr($pagename, $opt, "ERROR: Source ($page[filename]) and Target ($tgt) are the same.  Aborting.");
            $WikiShVars['STATUS'] = 5;
            return;
        }
        $page['type'] = $Target_Type;
        if ($opt['trial']) {
            wdbg(1,"$func: Trial-copying $page[filename] to $tgt");
            if (!$opt['q'])
                $newrows[] = "TRIAL copying " . $page['filename'] . " to $tgt";
        } else {
            if (!$opt['q'])
                $newrows[] = 'Copying: ' . $page['filename'] . " to $tgt";
            if ($opt['encrypt'] && function_exists('WikiShEncrypt'))
                $page['text'] = WikiShEncrypt($pagename, $opt, $page['text']);
            # $auth will be 'overwrite' automatically unless not exist when it
            # will become 'create' automatically.  Thus no need to specify.
            if (!wshWrite($pagename, $opt, $tgt, $page['type'], $page['text'], $page)) {
                $WikiShVars['STATUS'] = 2;
                return('');
            }
        }
    }
    $WikiShVars['STATUS'] = 0;
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

# {(cut OPTIONS PAGEPATTERN)} 
# cut "chunks" (fields or character-spans) from each line
# Options: 
# -dx     x becomes the pattern by which fields are split
#         (default = \s+)
# -fx[-y] cut fields x or x-to-y, with "field" defined by -dx option (1-based)
# -cx[-y] cut characters x or x-to-y (1-based)
# --ofs:y output field separator (defaults to x from -dx if unspecified)
$MarkupExpr["${WikiShMXPrefix}cut"] = 'wshCut($pagename, @$argp, @$args)'; 
function wshCut($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = 'Cut()';
    wdbg(4,"$func: Entering");
    wdbg(1,$args);
    wshInitOpts($pagename, 'd:f:c:', $opt, $args);
    wshSDOpt($opt, 'f', '');
    wshSDOpt($opt, 'c', '');
    wshSDOpt($opt, 'd', "\t");
    wshSDOpt($opt, 'ofs', $opt['d']);
    if (is_array($opt['f'])) $flddef = implode(',', $opt['f']);
    else $flddef = $opt['f'];
    if (is_array($opt['c'])) $chardef = implode(',', $opt['c']);
    else $chardef = $opt['c'];
    wshExpandWildCards($pagename, $opt, $args, false, false, false);
    wshSDOpt($opt, 'file_prefix', '');
    wshSDOpt($opt, 'line_prefix', '');
    wshSDOpt($opt, 'line_suffix', '');
    if (!$opt['f'] && !$opt['c']) {
        $WikiShVars['STATUS'] = 4;
        wshStdErr($pagename, $opt, "ERROR: $func: either -f or -c must be specified");
        return('');
    }
    $newrows = array();
    foreach ($args as $filename) {
        $page = wshReadPage($pagename, $opt, $filename);
        if (wshIsABadFile($page)) {
            wshStdErr($pagename, $opt, "ERROR: $func: No such page: $page[filename]");
            continue;
        }
        $textrows = explode("\n", $page['text']);
        wdbg(1,"$func: textrows=");
        wdbg(1,$textrows);
        if ($opt['file_prefix'])
            $array_prefix = array(wshReplace($opt, $page, $opt['file_prefix']));
        else
            $array_prefix = array();
        $line_prefix = wshReplace($opt, $page, $opt['line_prefix']);
        $line_suffix = wshReplace($opt, $page, $opt['line_suffix']);
        for ($i = 0; $i < sizeof($textrows); $i++) {
            if (strstr($opt['line_prefix'], 'LINENO')) 
                $line_prefix = wshReplace($opt, $page, $opt['line_prefix'], $i+1);
            $line = '';
            if ($flddef) {
                $flds = explode(',', $flddef);
                wdbg(1,"$func: fld search array");
                wdbg(1,$flds);
                $fldcnt=0;
                foreach ($flds as $fld) {
                    wdbg(1,"$func: processing fld def >>$fld<<");
                    $lineflds = explode("$opt[d]", $textrows[$i]);
                    wdbg(1,"$func: fields follow");
                    wdbg(1,$lineflds);
                    if (strstr($fld, '-')) {
                        wdbg(1,"$func: working with a range: $fld");
                        if (!preg_match("/(\d+)-(\d*)/", $fld, $startend)) {
                            wshStdErr($pagename, $opt, "ERROR: $func: Non-matching fld def $fld");
                        } else {
                            wdbg(1,"$func: fld range from $startend[1] to $startend[2]");
                            if ($startend[2]) // x-y
                                for ($j = $startend[1]-1; $j < $startend[2]; $j++) {
                                    $line .= (($fldcnt>0)?$opt['ofs']:'') . $lineflds[$j];
                                    $fldcnt++;
                                }
                            else // x- (to end)
                                for ($j = $startend[1]-1; $j < sizeof($lineflds); $j++) {
                                    $line .= (($fldcnt>0)?$opt['ofs']:'') . $lineflds[$j];
                                    $fldcnt++;
                                }
                        }
                    } else {
                        wdbg(1,"$func: simple fld#=$fld, fld=".$lineflds[$fld-1]);
                        $line .= (($fldcnt>0)?$opt['ofs']:'') . $lineflds[$fld-1];
                        $fldcnt++;
                    }
                }
            }
            if ($chardef) {
                $chars = explode(',', $chardef);
                wdbg(1,"$func: char search array");
                wdbg(1,$chars);
                foreach ($chars as $char) {
                    wdbg(1,"$func: processing char def >>$char<<");
                    if (strstr($char, '-')) {
                        wdbg(1,"$func: working with a range: $char");
                        if (!preg_match("/(\d+)-(\d*)/", $char, $startend)) {
                            wshStdErr($pagename, $opt, "ERROR: $func: Non-matching char def $char");
                        } else {
                            wdbg(1,"$func: range from $startend[1] to $startend[2]");
                            if ($startend[2]) // x-y
                                $line .= substr($textrows[$i], $startend[1]-1, ($startend[2]-$startend[1]+1));
                            else // x- (to end)
                                $line .= substr($textrows[$i], $startend[1]-1);
                        }
                    } else {
                        $line .= substr($textrows[$i], $char-1, 1);
                    }
                }
            }
            wdbg(1,"$func: >>$textrows[$i]<< became >>$line<<");
            $textrows[$i] = $line_prefix . $line . $line_suffix;
        }
        $newrows = array_merge($newrows, $array_prefix, $textrows);
    }
    $WikiShVars['STATUS'] = 0;
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

$MarkupExpr["${WikiShMXPrefix}diff"] = 'wshDiff($pagename, @$argp, @$args)'; 
function wshDiff($pagename, $opt, $args) 
{
    global $WikiShVars, $DiffFunction;
    if (wshNotNow($pagename)) return('');
    $func = "Diff()";
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, '', $opt, $args);
    wshSDOpt($opt, 'q', false);
    wshExpandWildCards($pagename, $opt, $args);
    if (sizeof($args) != 2) {
        wshStdErr($pagename, $opt, "ERROR: $func: Must specify exactly 2 pagenames.");
        $WikiShVars['STATUS'] = 2;
        return('');
    }
    if (!($p1 = wshReadPage($pagename, $opt, $args[0]))) {
        wshStdErr($pagename, $opt, "ERROR: $func: Unable to read page=$args[0].");
        $WikiShVars['STATUS'] = 2;
        return('');
    }
    if (!($p2 = wshReadPage($pagename, $opt, $args[1]))) {
        wshStdErr($pagename, $opt, "ERROR: $func: Unable to read page=$args[1].");
        $WikiShVars['STATUS'] = 2;
        return('');
    }
    $diff_out = $DiffFunction($p1['text']."\n", $p2['text']."\n");
    if ($diff_out) 
        $WikiShVars['STATUS'] = 1;
    else
        $WikiShVars['STATUS'] = 0;
    if ($opt['q'])
        return('');
    else {
        $diff_tmp = explode("\n", $diff_out);
        return (wshPostProcess($pagename, $opt, $diff_tmp));
    }
}

# Dirname()
# OPTION:
#   --raw   -- simply apply the PHP function dirname() to the input
# Normally apply basename() to text files and PageVar(..., $Group) to all others
$MarkupExpr["${WikiShMXPrefix}dirname"] = 'wshDirname($pagename, @$argp, @$args)'; 
function wshDirname($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    WikiShCompatible($pagename,$opt,$args); // handle wshInitOpts & wshExpandWildCards
    if (@$opt['raw'] or wshIsATextFile('', $args[0])) {
        if (wshIsATextFile('', $args[0]))
            $fn = substr($args[0], strlen(TEXTFILEID));
        else $fn = $args[0];
        $group = dirname($fn);
    } else $group = PageVar($args[0], '$Group');
    if ($group) $WikiShVars['STATUS'] = 0;
    else $WikiShVars['STATUS'] = 1;
    return ($group);
}

# {(echo OPTIONS ARGS)} 
# Print arguments
$MarkupExpr["${WikiShMXPrefix}echo"] = 'wshEcho($pagename, @$argp, @$args)'; 
function wshEcho($pagename, $opt, $args) 
{
    global $WikiShVars, $EnableWikiShRawEcho;
    if (wshNotNow($pagename)) return('');
    $func = "Echo()";
    wdbg(4,"$func: Entering: " . implode(" ", $args));
    wdbg(1,$args);
    wshInitOpts($pagename, '', $opt, $args);
    # If I expand wildcards that enables me to use wildcards which is nice,
    # but there's no way to suppress it because quotes are stripped in
    # ParseArgs()...  Better to have an option for it.
    if ($opt['w'])
        wshExpandWildCards($pagename, $opt, $args, true, true);
    wdbg(1,$args);
    $out = '';
    foreach ($args as $arg) {
        $out .= (($out) ? ' ' : '') . $arg;
    }
    $out = str_replace("\\t", "\t", $out);
    $out = explode("\\n", $out);
    $WikiShVars['STATUS'] = 0;
    if ($EnableWikiShRawEcho && $opt['raw'])
        echo print_r($out, true) . "<br>\n";
    else return (wshPostProcess($pagename, $opt, $out, false));
}

# {(false)}
# return nothing.  Set $STATUS to non-zero.
$MarkupExpr["${WikiShMXPrefix}false"] = 'wshFalse($pagename, @$argp, @$args)'; 
function wshFalse($pagename, $opt, $args) 
{
    global $WikiShVars;
    wshNotNow($pagename); // just for RC initializations - don't care about return val
    $func = "False()";
    wdbg(4,"$func: Entering");
    $WikiShVars['STATUS'] = 1;
    return('');
}

#
# {(fetchmail OPTIONS)} 
# OPTIONS
# --close or --end (nullifies any other options)
# --next (default)
# --stay (allow setting other values without going to next)
# --from:fromvar
# --to:tovar
# --cc:ccvar
# --date:datevar
# --subject:subjectvar
# --body:bodyvar
$MarkupExpr["${WikiShMXPrefix}fetchmail"] = 'wshFetchMail($pagename, @$argp, @$args)'; 
function wshFetchMail($pagename, $opt, $args) 
{
    global $WikiShVars, $WikiMailErr, $EnableWikiShFetchmail;
    static $OpenProfiles = array(), $msg = null;
    if (!$EnableWikiShFetchmail) {
        wshStdErr($pagename, $opt, "ERROR: Fetchmail is not enabled.  See \$EnableWikiShFetchmail");
        $WikiShVars['STATUS'] = 5;
        return('');
    }
    if (!defined('WikiMail')) {
        $msg = "ERROR: fetchmail depends on missing recipe wikimail.";
        echo "$msg"."<br>\n";
        $WikiShVars['STATUS'] = 5;
        return($msg);
    }
    if (wshNotNow($pagename)) return('');
    $func="FetchMail()";
    $d = 1;
    wdbg($d*4,"$func: Entering");
    wshInitOpts($pagename, '', $opt, $args);
    SDV($opt['profile'], 'default');
    if ($opt['close'] || $opt['end']) {
        if (wmClosePOP3($opt['profile']) && !$WikiMailErr)
            $WikiShVars['STATUS'] = 0;
        else {
            wshStdErr($pagename, $opt, "ERROR: $func: Error from wikimail: $WikiMailErr");
            $WikiShVars['STATUS'] = 1;
        }
        unset($OpenProfiles[$opt['profile']]);
        return('');
    }
    # If we haven't initialized this profile before then do so now
    if (!$OpenProfiles[$opt['profile']]) {
        SDV($opt['host'], 'default');
        SDV($opt['user'], 'default');
        SDV($opt['pass'], 'default');
        wmMkProfilePOP3($opt['profile'], $opt['host'], $opt['user'], $opt['pass']);
        if (!wmInitPOP3($opt['profile'])) {
            wshStdErr($pagename, $opt, "ERROR: $func: wikimail(init) error \"$WikiMailErr\"");
            $WikiShVars['STATUS'] = 1;
            return('');
        }
        #register_shutdown_function('wmClosePOP3($GLOBAL["wshPop3"])');
        $OpenProfiles[$opt['profile']] = true;
    }
    if ($opt['stay'] && @$opt['delete'] && wmDeletePOP3($opt['profile'])) {
            wshStdErr($pagename, $opt, "ERROR: $func: wikimail(del) error \"$WikiMailErr\"");
            $WikiShVars['STATUS'] = 1;
            return('');
    }
    SDV($opt['delete'], true);
    if (($opt['stay'] && $msg) || ($msg = wmRetrievePOP3($opt['profile'], $opt['delete']))) {
        list($headhash, $headers, $body) = $msg;
        foreach (array('from', 'to', 'cc', 'date', 'subject') as $i)
            if ($opt[$i]) $WikiShVars[$opt[$i]] = $headhash[$i];
        if ($opt['body']) $WikiShVars[$opt['body']] = implode("\n", $body);
        if ($opt['headers']) $WikiShVars[$opt['headers']] = $headers;
        if ($WikiMailErr)
            $WikiShVars['STATUS'] = 2;
        else
            $WikiShVars['STATUS'] = 0;
        return('');
    } else {
        $WikiShVars['STATUS'] = 1;
        return('');
    }
}

#
# {(grep OPTIONS PATTERN PAGEPATTERN ...)} 
# OPTIONS
#   -v        - reVerse the meaning - return lines NOT matching
#   -i        - make matching case-insensitivee (--ic and --ignorecase synonyms)
#   -l        - return ONLY filenames, not lines
#   -F --fixed-strings - interpret PATTERN as fixed string instead of regex
#   --highlight-highlight the matching text
#   --targets - search "targets" instead of text
# REGEX       - display lines matching regex pattern 
# PAGEPATTERN - source pages from PageName or Group.Pagename 
#               allowing wiki wildcards * and ?
$MarkupExpr["${WikiShMXPrefix}grep"] = 'wshGrep($pagename, @$argp, @$args)'; 
function wshGrep($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func="Grep()";
    wdbg(4,"$func: Entering");
    $WikiShVars['STATUS'] = 1; // assume we find nothing
    wshInitOpts($pagename, '', $opt, $args);
    $expr = array_shift($args);
    wshExpandWildCards($pagename, $opt, $args, false, false, false);
    wshSDOpt($opt, 'file_prefix', '');
    wshSDOpt($opt, 'line_prefix', '');
    wshSDOpt($opt, 'line_suffix', '');
    wshSDOpt($opt, 'highlight', false);
    wshSDOpt($opt, 'fixed-strings', false);
    if (@$opt['F']) $opt['fixed-strings'] = true;
    wshSDOpt($opt, 'q', false);
    wdbg(1,"$func: Expr=$expr");
    if ($expr == '' || sizeof($args) == 0) {
        $WikiShVars['STATUS'] = 2;
        return '';
    }
    if (!isset($opt['files-with-matches']))
        $opt['files-with-matches'] = @$opt['l'];
    if (!isset($opt['line-number']))
        $opt['line-number'] = @$opt['n'];
    $reverse = (@$opt['v'] || @$opt['reverse']);
    $regexopt = (@$opt['i'] || @$opt['ignorecase'] || @$opt['ic']) ? 'i' : '';
	if ((sizeof($args) > 1 || 
			(isset($opt['H']) || isset($opt['with-filename']))) && 
			!isset($opt['h']) && !isset($opt['no-filename']) &&
			strpos(@$opt['line_prefix'], 'PAGENAME') === FALSE)
		$opt['line_prefix'] = 'PAGENAME:'.@$opt['line_prefix'];
    $newrows = array();
    foreach ($args as $filename) {
        $page = wshReadPage($pagename, $opt, $filename);
        if (wshIsABadFile($page)) {
            wshStdErr($pagename, $opt, "ERROR: $func: No such page: $page[filename]");
            continue;
        }
        if ($opt['targets']) $page['text'] = str_replace(",", "\n", $page['targets']);
        wdbg(2,"$func: Processing: filename=$filename");
        wdbg(1,"$func: Text=$page[text]");
        $line_prefix = wshReplace($opt, $page, $opt['line_prefix']);
        $line_suffix = wshReplace($opt, $page, $opt['line_suffix']);
        $file_prefix = wshReplace($opt, $page, $opt['file_prefix']);
        # Optimize - if we're only interested in yes/no on a match for the whole
        # file then just figure that out and get out...
        if ($opt['files-with-matches']) { // -l
            if (($opt['fixed-strings'] && strstr($page['text'], $expr)) ||
                (!$opt['fixed-strings'] && preg_match("/$expr/m$regexopt", $page['text']))) {
                if (!$reverse) {
                    if ($file_prefix) $newrows[] = $file_prefix;
                    $newrows[] = $line_prefix . $page['filename'] . $line_suffix;
                    $WikiShVars['STATUS'] = 0; // indicates we found something
                }
            } elseif ($reverse) {
                if ($file_prefix) $newrows[] = $file_prefix;
                $newrows[] = $line_prefix . $page['filename'] . $line_suffix;
                $WikiShVars['STATUS'] = 0; // indicates we found something
            }
            continue;
        }
        # OK, if I get here then we're not interested in files-with-matches
        # so start looking at each individual line.
        $textrows = explode("\n", $page['text']);
        wdbg(1,"$func: " . sizeof($textrows) . " lines to process...");
        wdbg(1,"$func: First Line: $textrows[0]");
        $str = $textrows[0];
        # If we have a regex then it must not contain a / since that is the
        # delimiter we use.  Thus escape it.
        if (!$opt['fixed-strings'])
            $expr = preg_replace('/(?<!\\\\)\//', '\/', $expr);
        // Hmm... If we don't find anything do we want to show this file prefix?
        if ($file_prefix) $newrows[] = $file_prefix;
        $lineno=0;
        foreach ($textrows as $row) {
            $lineno++;
            wdbg(0,"$func: expr=$expr, regexopt=".wshDbgOd($regexpopt));
            if (($opt['fixed-strings'] && strstr($row, $expr)) ||
                (!$opt['fixed-strings'] && preg_match("/($expr)/$regexopt", $row))) {
                wdbg(1,"$func: MATCH!($expr) >>$row<<");
                if (!$reverse) {
                    if ($opt['highlight']) {
                        wdbg(0,"$func: Highlighting (expr=$expr, opts=$regexopt)...>>$row<<");
                        $row = preg_replace("/($expr)/$regexopt", "'''$1'''", $row);
                        wdbg(0,"$func: Highlighted...>>$row<<");
                    }
                    if ($opt['line-number']) $row = $lineno . ': ' . $row;
                    $newrows[] = $line_prefix . $row . $line_suffix;
                    $WikiShVars['STATUS'] = 0; // indicates we found something
                }
            } elseif ($reverse) {
                wdbg(1,"$func: NOMATCH!($expr) >>$row<<");
                if ($opt['line-number']) $row = sprintf("%05d: %s", $lineno, $row);
                $newrows[] = $line_prefix . $row . $line_suffix;
                $WikiShVars['STATUS'] = 0; // indicates we found something
            }
        }
    }
    if ($opt['q']) $newrows = array();
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

# {(head OPTIONS PAGEPATTERN)} 
# Print lines from the beginning of a file
$MarkupExpr["${WikiShMXPrefix}head"] = 'wshHead($pagename, @$argp, @$args)'; 
function wshHead($pagename, $opt, $filelist) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = 'Head()';
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, 'n:', $opt, $filelist);
    wshSDOpt($opt, 'n', 10);
    $opt['startline'][] = 1;
    $opt['endline'][] = $opt['n'];
    $WikiShVars['STATUS'] = 0;
    return(wshExtractLines($pagename, $opt, $filelist, $func));
}

#
# {(ls OPTIONS PAGEPATTERN ...)} 
# OPTIONS
# --raw -  don't go through pmwiki's .pageindex filter to see what's there, but
#          look out there and see what files actually exist
# --list:x like list=x in pagelist
# -a       list all files (shortcut for --list:all)
# -l       long listing (just time & size)
# -c       use create time for -l listing and for -t sorting
# -t       sort by time
# -S       sort by size
# -r       reverse sort
# --nosort don't sort (default is to sort by name)
# --auth   show auth with -l type of format (separate because it's slow)
# PAGEPATTERN - source pages from PageName or Group.Pagename 
#               allowing wiki wildcards * and ?
$MarkupExpr["${WikiShMXPrefix}ls"] = 'wshLs($pagename, @$argp, @$args)'; 
function wshLs($pagename, $opt, $args) 
{
    global $WikiShVars, $SearchPatterns, $WikiLibDirs, $EnableWikiShTextRead;
    global $wshAuthText, $wshAuthPage;
    if (wshNotNow($pagename)) return('');
    $WikiShVars['STATUS'] = 0; // this will be reset if we run into errors
    $func = "Ls():";
    wdbg(4,"$func Entering");
    wshInitOpts($pagename, '', $opt, $args);
    if (!$args) $args[] = '*.*';
    if (sizeof($args) == 1 && $args[0] == '*.*' && !@$opt['line_prefix'] && !@$opt['line_suffix'] && !@$opt['file_prefix'] && !@$opt['l']) $QuickOptimize = true;
    if (!@$opt['list'] && @$opt['a'])
        $opt['list'] = 'all';  // ls -a == ls --list:all
    wshExpandWildCards($pagename, $opt, $args, true, false, false); // keep textID
    # handle --group and --name options (group= and name=)
    $pnfilter = array();
    if (@$opt['group']) $pnfilter[] = FixGlob($opt['group'], '$1$2.*');
    if (@$opt['name']) $pnfilter[] = FixGlob($opt['name'], '$1*.$2');
    if ($pnfilter) $args = MatchPageNames($args, $pnfilter);
    if ($opt['l'] || $opt['t'] || $opt['S']) {
        if (@$opt['g'] || @$opt['G'] || @$opt['p'] || @$opt['P']) {
            wshStdErr($pagename, $opt, "ERROR: -g, -G, -p, and -P cannot be used in conjunction with -l, -t, or -S.");
        }
        foreach ($args as $k => $arg) {
            if (($opt['OS'] || $opt['os'] || (wshIsATextFile('', $arg)))) {
                $fileinfo = false;
                if (wshIsATextFile('', $arg)) {
                    $arg = $args[$k] = substr($arg, strlen(TEXTFILEID));
                    if ($EnableWikiShTextRead && slAuthorized($arg, $wshAuthText, 'read', true) && file_exists($arg))
                        $fileinfo = stat($arg);
                } else {
                    foreach ($WikiLibDirs as $dir) {
                        $pagefile = $dir->pagefile($arg);
                        if (file_exists($pagefile) && $fileinfo = stat($pagefile)) 
                            break;
                    }
                }
                if (@$fileinfo) {
                    wdbg(1,"$func: FOUND OS FILE $arg -> $pagefile");
                    if (@$foo < 3) {
                        wdbg(1,$fileinfo);
                        $foo = @$foo+1;
                    }
                    $size[$k] = $fileinfo['size'];
                    $author[$k] = $fileinfo['uid'];
                    #$group[$k] = $fileinfo['gid'];
                    $time[$k]=($opt['c'] ? $fileinfo['ctime'] : $fileinfo['mtime']);
                } else {
                    wdbg(1,"$func: NOT FOUND OS FILE $arg");
                    $WikiShVars['STATUS'] = 2;
                    $args[$k] = "FILE NOT FOUND: $arg";
                    $size[$k] = 0;
                    $author[$k] = 'UnKnOwN';
                    $time[$k] = 0;
                }
            } else {
                if (PageExists($arg))
                    $page = wshRetrieveAuthPage($arg, 'read', false);
                else $page = false;
                if (!$page) {
                    wdbg(1,"$func: NOT FOUND WIKI PAGE $arg");
                    $WikiShVars['STATUS'] = 2;
                    $args[$k] = "PAGE NOT FOUND: $arg";
                    $size[$k] = 0;
                    $author[$k] = 'UnKnOwN';
                    $time[$k] = 0;
                } else {
                    wdbg(1,"$func: FOUND WIKI PAGE $arg");
                    $size[$k] = strlen($page['text']);
                    if ($opt['c']) {
                        $author[$k] = ($page['cauthor']?$page['cauthor']:$page['author']);
                        $time[$k] = $page['ctime'];
                    } else {
                        $author[$k] = $page['author'];
                        $time[$k] = $page['time'];
                    }
                }
            }
        }
        wdbg(1,"$func: array sizes - file:" . sizeof($args) . " size:" . sizeof($size) . " author:" . sizeof($author) . " time:" . sizeof($time));
    } else {
        foreach ($args as $k => $arg) {
            if (wshIsATextFile('', $arg)) {
                $args[$k] = $arg = substr($arg, strlen(TEXTFILEID));
                if (!$EnableWikiShTextRead || !slAuthorized($arg, $wshAuthText, 'read', true) || !file_exists($arg))
                    $args[$k] = "FILE NOT FOUND: $arg";
            } elseif (wshIsABadFile('', $arg) || !PageExists($arg)) {
                $WikiShVars['STATUS'] = 2;
                $args[$k] = "PAGE NOT FOUND: $arg";
            } elseif ($opt['g'] || $opt['G']) {
                if ($pos = strpos($arg, '.'))
                    $args[$k] = substr($arg, 0, $pos);
            } elseif ($opt['p'] || $opt['P']) {
                if ($pos = strpos($arg, '.'))
                    $args[$k] = substr($arg, $pos+1);
            }
        }
    }
    if (($opt['P'] || $opt['G']) && $args)
        $args = array_unique($args);
    #
    # handle sorting
    #
    if ($opt['sort'] !== false) {
        if (!$opt['t'] && !$opt['S'] && !$opt['l']) {
            if ($opt['r'])
                rsort($args);
            else
                sort($args);
        } else {
            if ($opt['t']) {
                wdbg(1,"$func: Sorting by time");
                $k1 = &$time; $k2 = &$args; $k3 = &$size; $k4 = &$author;
                $k1a= SORT_NUMERIC; $k2a=SORT_STRING;
                $k1b = ($opt['r']?SORT_ASC:SORT_DESC);
            } elseif ($opt['S']) {
                wdbg(1,"$func: Sorting by size");
                $k1 = &$size; $k2 = &$args; $k3 = &$time; $k4 = &$author;
                $k1a= SORT_NUMERIC; $k2a=SORT_STRING;
                $k1b = ($opt['r']?SORT_ASC:SORT_DESC);
            } else {
                wdbg(1,"$func: Sorting by name");
                $k1 = &$args; $k2 = &$size; $k3 = &$time; $k4 = &$author;
                $k1a= SORT_STRING; $k2a=SORT_NUMERIC;
                $k1b = ($opt['r']?SORT_DESC:SORT_ASC);
            }
            array_multisort($k1, $k1a, $k1b, $k2, $k2a, $k3, $k4);
        }
    }
    if ($QuickOptimize) { // no need to check for existence
        $newrows = $args;
    } else {
        $newrows = array();
        foreach ($args as $k => $filename) {
            wdbg(1,"$func processing filename=$filename");
            $page['filename'] = $filename;
            $line_prefix = wshReplace($opt, $page, $opt['line_prefix']);
            $line_suffix = wshReplace($opt, $page, $opt['line_suffix']);
            $file_prefix = wshReplace($opt, $page, $opt['file_prefix']);
            if ($file_prefix) $newrows[] = $file_prefix;
            if ($opt['l']) {
                $newrows[] = $line_prefix . sprintf("%15.15s%8d %s %s", $author[$k], $size[$k], strftime("%b %d %H:%M", $time[$k]), $filename) . $line_suffix;
            } else {
                $newrows[] = $line_prefix . $filename . $line_suffix;
            }
        }
    }
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

# {(mv OPTIONS PAGEPATTERN)} 
# Move pages from one name to another.  Implemented via cp followed by rm.
# Options: 
#   -f  file will be FULLY removed
# if multiple source files are specified then the target file must be a
# group/directory.
$MarkupExpr["${WikiShMXPrefix}mv"] = 'wshMv($pagename, @$argp, @$args)'; 
function wshMv($pagename, $opt, $filelist) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = "Mv()";
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, '', $opt, $filelist);
    wshCp($pagename, $opt, $filelist);
    if ($WikiShVars['STATUS'] != 0) {
        wshStdErr($pagename, $opt, "ERROR: $func: NOT removing source files.  Errors creating destination files.");
        return('');
    }
    $Target = array_pop($filelist); // get rid of the destination
    wshRm($pagename, $opt, $filelist);
    #$WikiShVars['STATUS'] = 0; // leave $WikiShVars['STATUS'] from wshRm().
    return('');
}

# {(null)} 
# Return nothing, regardless of options, arguments, etc.
$MarkupExpr["${WikiShMXPrefix}null"] = 'wshNull($pagename, @$argp, @$args)'; 
function wshNull($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = "Null()";
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, '', $opt, $args); // this is if someone wants to set debug or etc
    $WikiShVars['STATUS'] = 0;
    return('');
}

# {(od)} 
# Return a representation of the string with special characters labeled
# instead of actually output.  Loosely related to the od command
$MarkupExpr["${WikiShMXPrefix}od"] = 'wshOd($pagename, @$argp, @$args)'; 
function wshOd($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = "Od()";
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, '', $opt, $args); // this is if someone wants to set debug or etc
    $WikiShVars['STATUS'] = 0;
    $rtn = array();
    foreach ($args as $arg)
        $rtn[] = wshDbgOd($arg, true);
    return(wshPostProcess($pagename, $opt, $rtn));
}

# {(read OPTIONS VAR1 [VAR2...] INPUTSPEC)} 
$MarkupExpr["${WikiShMXPrefix}read"] = 'wshRead($pagename, @$argp, @$args)'; 
function wshRead($pagename, $opt, $args) 
{
    global $WikiShVars, $WikiShPipeActive, $WikiShPipeText;
    static $ReadSet;
    # $ReadSet:
    #    vars 0..n (each variable)
    #    text 0..n (each line of text)
    #    next (index to next line in text)
    #    IFS (Input Field Separator - a regex to be applied to each line)
    if (wshNotNow($pagename)) return('');
    $func = "Read()";
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, '', $opt, $args, true, false);
    
    # $set represents a certain file to read and the associated pointers
    # it becomes the index into $ReadSet[].
    if ($opt['set']) $set = $opt['set'];
    else $set = 0;
    wdbg(1,"$func: Readset[set=$set]");
    wdbg(1,"$func: opt[next]=" . ($opt[next]?"TRUE":"FALSE"));

    # Handle --clear and get out if that was set
    if (@$opt['clear']) {
        unset($ReadSet[$set]);
        return(''); // we can't really do anything else if we clear it
    }

    # unset all vars in $args in case we have to get out early
    foreach ($args as $var) $WikiShVars[$var] = '';

    #
    # --restoreset
    #
    if ($opt['restoreset']) {
        if(!session_id()) session_start();
        wdbg(2,"$func: Restoring from _SESSION");
        if (!isset($_SESSION['WIKISH_READ'][$set]))
            $WikiShVars['STATUS'] = 3; // no error message, but set a status
        $ReadSet[$set] = $_SESSION['WIKISH_READ'][$set];
        wdbg(2,"$func: restored idx=".$ReadSet[$set]['curline']);
        wdbg(1,"$func: restored text (array) follows");
        wdbg(1,$ReadSet[$set]['text']);
        # If you're not doing anything else go ahead and get out - typical
        # restoration at the start of a script
        if (!@$opt['prev'] && !@$opt['next'] && !@$opt['goto'] && !@$opt['rewind'] && !@$opt['linecount'] && !@$opt['status'] && !@$opt['initialize']) {
            return('');
        }
    }
    if (@$opt['initialize'])
        unset($ReadSet[$set]);
    # The default action is --next:1, but there are a lot of cases in which
    # we don't make that the default...
    if (!@$opt['prev'] && !isset($opt['next']) && !@$opt['goto'] && !@$opt['rewind'] && !@$opt['linecount'] && !@$opt['status'] && !@$opt['initialize'] && isset($ReadSet[$set]))
        $opt['next'] = 1;
    #
    # --clearset
    #
    if (@$opt['clearset']) {
        if(!session_id()) session_start();
        wdbg(2,"$func: Clearing set=$set from _SESSION");
        unset($_SESSION['WIKISH_READ'][$set]);
        # If you're not doing anything else go ahead and get out - typical
        # clearing up at the start of a script
        if (!@$opt['prev'] && !@$opt['next'] && !@$opt['goto'] && !@$opt['rewind'] && !@$opt['linecount'] && !@$opt['status'] && !@$opt['initialize']) {
            return('');
        }
    }
    #
    # --initialize (implicit if not already set)
    #
    if (!isset($ReadSet[$set]) && $opt['initialize'] !== false) {
        wdbg(4,"$func: Initializing");
        if (!isset($opt['stdin']) && !$WikiShPipeActive) {
            # If you're just setting status or linecount then blank is
            # a valid return - missing stdin is OK
            if (@$opt['status'] || @$opt['linecount']) {
                return (wshPostProcess($pagename, $opt, array('')));
            }
            wdbg(4,"$func: stdin not set. exiting with error");
            wshStdErr($pagename, $opt, "ERROR: $func: stdin not set");
            $WikiShVars['STATUS'] = 2;
            return('');
        }
        if ($WikiShPipeActive) {
            wdbg(2,"$func: Reading from pipe");
            $page['text'] = $WikiShPipeText;
            $WikiShPipeText = $WikiShPipeActive = false;
        } else {
            wdbg(2,"$func: Reading from stdin=$opt[stdin]");
            if (!wshIsAWikiPage('', $opt['stdin']) && !wshIsATextFile('', $opt['stdin']))
                $opt['stdin'] = WIKIPAGEID . $opt['stdin'];
            $page = wshReadPage($pagename, $opt, $opt['stdin']);
        }
        wdbg(2,"$func: Read text=>>$page[text]<<");
        if (strstr($page['text'], "\r"))
            $ReadSet[$set]['text'] = explode("\r\n", $page['text']);
        else
            $ReadSet[$set]['text'] = explode("\n", $page['text']);
        wdbg(1,"$func: Exploded text array follows");
        wdbg(1,$ReadSet[$set]['text']);
        $ReadSet[$set]['curline'] = 0;
        if (isset($opt['IFS']))
            $ReadSet[$set]['IFS'] = $opt['IFS'];
        else
            $ReadSet[$set]['IFS'] = '\s';
        wdbg(1,"$func: ReadSet variable follows (set=$set):");
        wdbg(1,$ReadSet);
    }
    #
    # --next:X
    #
    if ($opt['next']) {
        wdbg(2,"$func: Moving index (".$ReadSet[$set]['curline'].") NEXT=$opt[next]");
        if ($opt['next'] === true) $opt['next'] = 1;
        if (is_numeric($opt['next']))
            $ReadSet[$set]['curline'] += $opt['next'];
        else {
            $found = false;
            for ($i = $ReadSet[$set]['curline']+1; $i <= sizeof($ReadSet[$set]['text']); $i++) {
                if ($opt['next'][0] == '/') {
                    if (preg_match($opt['next'], $ReadSet[$set]['text'][$i])) {
                        $ReadSet[$set]['curline'] = $i;
                        $found = true;
                        break;
                    }
                } else {
                    if (strstr($ReadSet[$set]['text'][$i], $opt['next'])) {
                        $ReadSet[$set]['curline'] = $i;
                        $found = true;
                        break;
                    }
                }
            }
            if (!$found) {
                $ReadSet[$set]['curline'] = sizeof($ReadSet[$set]['text'])+1;
            }
        }
    }
    #
    # --prev:X
    #
    if ($opt['prev']) {
        wdbg(2,"$func: Moving index (".$ReadSet[$set]['curline'].") PREV=$opt[prev]");
        if ($opt['prev'] === true) $opt['prev'] = 1;
        if (is_numeric($opt['prev']))
            $ReadSet[$set]['curline'] -= $opt['prev'];
        else {
            $found = false;
            for ($i = $ReadSet[$set]['curline']-1; $i >= 0; $i--) {
                wdbg(2,"$func: Checking idx=$i, line=".$ReadSet[$set]['text'][$i]);
                if ($opt['prev'][0] == '/') {
                    if (preg_match($opt['prev'], $ReadSet[$set]['text'][$i])) {
                        wdbg(2,"$func: regex success");
                        $ReadSet[$set]['curline'] = $i;
                        $found = true;
                        break;
                    }
                } else {
                    if (strstr($ReadSet[$set]['text'][$i], $opt['prev'])) {
                        wdbg(2,"$func: fixed string success");
                        $ReadSet[$set]['curline'] = $i;
                        $found = true;
                        break;
                    }
                }
            }
            if (!$found) {
                wdbg(2,"$func: not found");
                $ReadSet[$set]['curline'] = -1;
            }
        }
    }
    #
    # --rewind (implemented as --goto:0)
    # --goto:X
    #
    if (@$opt['rewind']) $opt['goto'] = 0;
    if (@$opt['goto']) {
        wdbg(2,"$func: Moving index (".$ReadSet[$set]['curline'].") GOTO=$opt[goto]");
        $ReadSet[$set]['curline'] = $opt['goto'];
    }
    $WikiShVars['STATUS'] = 0; // assume we're OK unless reset below
    wdbg(2,"$func: After move curline=".$ReadSet[$set]['curline']);
    #
    # check for EOF
    #
    if ($ReadSet[$set]['curline'] >= sizeof($ReadSet[$set]['text'])) {
        if (@$opt['clear'] === false) // --noclear
            $ReadSet[$set]['curline'] = sizeof($ReadSet[$set]['text']);
        else
            unset($ReadSet[$set]); 
        wdbg(4,"$func: returning EOF (idx=".$ReadSet[$set]['curline'].")");
        $WikiShVars['STATUS'] = 2; // status=2 -> EOF
    }
    #
    # check for BOF
    #
    if ($ReadSet[$set]['curline'] < 0) {
        if (@$opt['clear'] ===false) // --noclear
            $ReadSet[$set]['curline'] = -1;
        else
            unset($ReadSet[$set]); 
        wdbg(4,"$func: returning BOF (idx=".$ReadSet[$set]['curline'].")");
        $WikiShVars['STATUS'] = 1; // status=1 -> BOF
    }
    wdbg(2,"$func: After check EOF/BOF curline=".$ReadSet[$set]['curline']);
    #
    # --saveset
    #
    if ($opt['saveset']) {
        if(!session_id()) session_start();
        wdbg(2,"$func: Saving set=$set to _SESSION (idx=".$ReadSet[$set]['curline'].")");
        $_SESSION['WIKISH_READ'][$set] = $ReadSet[$set];
    }
    # Get out if there was a BOF or EOF condition
    if ($WikiShVars['STATUS']) return(''); // this happens after --saveset
    wdbg(2,"$func: After move line=".$ReadSet[$set]['text'][$ReadSet[$set]['curline']]);
    #
    # --linecount
    # (note that if $ReadSet[$set] was not set it was probably handled 
    # already, up in the --initialize section
    #
    if (@$opt['linecount']) {
        if (isset($ReadSet[$set]))
            return (wshPostProcess($pagename, $opt, array(sizeof($ReadSet[$set]['text']))));
        else
            return (wshPostProcess($pagename, $opt, array(''))); // null set
    }
    #
    # --status
    # (note that if $ReadSet[$set] was not set it was probably handled 
    # already, up in the --initialize section
    #
    if (@$opt['status']) {
        if (isset($ReadSet[$set]))
            return (wshPostProcess($pagename, $opt, array($ReadSet[$set]['curline'])));
        else
            return (wshPostProcess($pagename, $opt, array(''))); // null set
    }
    #
    # --stdout
    #
    if ($opt['stdout']) {
        return (wshPostProcess($pagename, $opt, array($ReadSet[$set]['text'][$ReadSet[$set]['curline']])));
    } elseif ($args) {
        #
        # normal setting of vars
        #
        $idx = $ReadSet[$set]['curline'];
        wdbg(3,"$func: Line to split (idx=$idx): >>" . $ReadSet[$set]['text'][$idx] . "<<");
        $fields = preg_split("/" . $ReadSet[$set]['IFS'] . "/", $ReadSet[$set]['text'][$idx], -1, PREG_SPLIT_OFFSET_CAPTURE);
        # Assign variables 0..n-1
        if (sizeof($args) > 1) {
            for ($i = 0; $i < sizeof($args)-1; $i++) {
                wdbg(1,"$func: Setting ".$args[$i]." to ".$fields[$i][0]);
                $WikiShVars[$args[$i]] = $fields[$i][0];
            }
        } else
            $i = 0;
        # Assign the final variable with all the remaining values (or 
        # nothing if all text is already used up
        if (sizeof($fields) >= sizeof($args))
            $leftover=substr($ReadSet[$set]['text'][$idx], $fields[$i][1]);
        else
            $leftover='';
        wdbg(1,"$func: Setting final var to >>$leftover<<");
        $WikiShVars[$args[$i]] = $leftover;
    }
    $WikiShVars['STATUS'] = 0;
    wdbg(4,"$func: returning next line ($idx): " . $ReadSet[$set]['text'][$idx]);
    return('');
}


# {(rm OPTIONS PAGEPATTERN[s])}
# Remove (delete) files matching page patterns.  "Delete" is a relative term
# since most page deletions in pmwiki are actually a renaming, but you get
# the idea.
# OPTIONS
# -f      This will cause an actual deletion (unlink()) of the file after it
#         has been deleted in the normal fashion.  Danger, Will Robinson!
# --trial (non-standard from shell perspective) debuG the delete - just list
#         the files that WOULD have been deleted without doing any deletion
#
$MarkupExpr["${WikiShMXPrefix}rm"] = 'wshRm($pagename, @$argp, @$args)';
function wshRm($pagename, $opt, $args)
{
    global $DeleteKeyPattern, $DeleteKey;
    global $EnableWikiShRemove, $EnableWikiShRemoveFully,$EnableWikiShTextWrite;
    global $WorkDir, $WikiShVars, $wshAuthText;
    if (wshNotNow($pagename)) return('');
    $func = "rm()";
    SDV($EnableWikiShRemove, false);
    SDV($EnableWikiShRemoveFully, false);
    wdbg(4,"$func: Entering");
    if (!$EnableWikiShRemove) {
        wshStdErr($pagename, $opt, "ERROR: $func: Unable to remove files.  Not enabled.");
        $WikiShVars['STATUS'] = 4;
        return ('');
    }
    SDV($DeleteKey, 'delete');
    if ($DeleteKeyPattern && !preg_match("/$DeleteKeyPattern/", $DeleteKey)) {
        wshStdErr($pagename, $opt, "ERROR: $func: DeleteKey ($DeleteKey) does not match \$DeleteKeyPattern.  Unable to delete.");
        $WikiShVars['STATUS'] = 4;
        return ('');
    }
    wshInitOpts($pagename, '', $opt, $args);
    wshExpandWildCards($pagename, $opt, $args);
    wshSDOpt($opt, 'f', false);
    wshSDOpt($opt, 'trial', false);
    $newrows = array();
    foreach ($args as $filename) {
        wdbg(1,"$func: Processing $filename");
        $page = wshReadPage($pagename, $opt, $filename);
        if ($page['type'] == 'bad') continue; // probably didn't exist
        wdbg(1,"$func: Read Page: $filename (type=".$page['type'].')');
        if ($page[filename] == '-' && $page['type'] == 'inline') {
            wshStdErr($pagename, $opt, "ERROR: $func: File not found: $filename");
            continue;
        }
        if ($opt['trial']) {
            wdbg(1,"$func: Debug-deleting $page[filename]");
            $newrows[] = $page['filename'];
        } else {
            if ($page['type'] == 'wiki') {
                if (!wshWrite($pagename, $opt, $page['filename'], $page['type'], $DeleteKey, $page, 'delete')) {
                    $WikiShVars['STATUS'] = 2;
                    return('');
                }
                $newrows[] = 'Deleted: ' . $page['filename'];
                #wdbg(2,"TESTING GLOB()"); wdbg(1,glob("$WorkDir/*.*"));
                if ($opt['f']) {
                    if (!$EnableWikiShRemoveFully) {
                        wshStdErr($pagename, $opt, "ERROR: $func: Unable to remove file fully.  Not enabled.");
                        $WikiShVars['STATUS'] = 4;
                        return('');
                    }
                    wdbg(1,"$func: Permanently deleting $page[filename] by glob()");
                    foreach(glob("$WorkDir/$page[filename],del-*") as $File2Del) {
                        $newrows[] = "Permanently Deleted: $page[filename] ($File2Del)";
                        wdbg(1,"$func: Permanently deleting file=$File2Del");
                        unlink($File2Del);
                    }
                }
            } elseif ($page['type'] == 'text') {
                if (!$EnableWikiShTextWrite) {
                    wshStdErr($pagename, $opt, "ERROR: $func: Unable to remove text files.  Not enabled.");
                    $WikiShVars['STATUS'] = 4;
                    return('');
                }
                if (!slAuthorized($page['filename'], $wshAuthText, 'delete', true)) {
                    wshStdErr($pagename, $opt, "ERROR: $func: Unable to remove $page[filename].  Not (SecLayer) authorized.");
                    $WikiShVars['STATUS'] = 4;
                    return('');
                }
                $newrows[] = "Deleted textfile: $page[filename]";
                wdbg(1,"$func: Deleting textfile=$page[filename]");
                unlink($page['filename']);
            } elseif ($page['type'] == 'session') {
                wdbg(1,"$func: Deleting (session-type) $page[filename]");
                unset($_SESSION[$page['filename']]);
            } else {
                wshStdErr($pagename, $opt, "ERROR: Lazy me didn't implement rm() for $page[filename] -> $page[type] type files. Sorry.");
            }
        } 
    }
    $WikiShVars['STATUS'] = 0;
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

# {(sed OPTIONS PAGEPATTERN)} 
# Although sed is a HUGELY powerful tool, only very small pieces of its
# functionality have been implemented here, hopefully the most commonly
# used portions...
# sed -n 'x,yp'
#    This will print from line x to line y (y can be $ for end-of-file)
# sed 's/search/replace/'
#    This will do a simple search/replace where the search can be regex and 
#    the replace can contain such "magic" as is supported by preg_replace()
#    
$MarkupExpr["${WikiShMXPrefix}sed"] = 'wshSed($pagename, @$argp, @$args)'; 
function wshSed($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = "Sed()";
    wdbg(4,"$func: Entering");
    # Within wshInitOpts and then wshExpandVars the call to FmtPagename causes the
    # $p from '1,$p' to be sub'd.  So I've got to get rid of it and then get
    # it back after the wshInitOpts call.
    $args = str_replace('$p', '&dollar;p', $args); // '1,$p' sub problem
    wshInitOpts($pagename, 'e:', $opt, $args);
    if (@$opt['i']) {
        $opt['inplaceedit'] = $opt['i'];
        unset($opt['i']);           // wshExtractLines takes -i as ignore case
    }
    if (!isset($opt['e'])) {
        wdbg(1,"$func: Taking 1st arg since no opt[e]");
        $opt['e'][] = array_shift($args);
    }
    if (!is_array($opt['e'])) {
        wdbg(1,"$func: Converting opt[e] into array");
        $opt['e'][] = $opt['e'];
    }
    $opt['e'] = str_replace('&dollar;p', '$p', $opt['e']); // '1,$p' sub problem
    wdbg(2,"$func: opt[e]:"); wdbg(2,$opt['e']);
    foreach ($opt['e'] as $k => $e) wdbg(2, "$k => " . wshDbgOd($e));
    // Now $opt['e'] is definitely an array holding our expressions...
    foreach ($opt['e'] as $expr) {
        wdbg(1,"$func: expr=" . wshDbgOd($expr));
        if (preg_match("/^(\d+|\/[^\/]+\/[iADUX]*),(\d+|\\$|\/[^\/]+\/[iADUX]*)p/", $expr, $m)) {
            wdbg(1,"$func: Matched start/end");
            $opt['startline'][] = $m[1];
            $opt['endline'][] = $m[2];
        } elseif (preg_match("/^s((.)(?:[^\\2]|\\\\\\2)+\\2)((?:[^\\2]|\\\\\\2)*)\\2([gFiADUXu]*)$/", $expr, $m)) {
            wdbg(1,"$func: Matched Find/Replace ($m[1] <--> $m[3] / $m[4])");
            $m[1] = str_replace('\n', "\n", $m[1]); // allow newlines in find
            if (strstr($m[4], 'F')) {
                $opt['find'][] = substr($m[1], 1, -1);
                if (!strstr($m[4], 'g'))
                    wshStdErr($pagename, $opt, "$func: ERROR: F flag requires g flag.  Continuing as if g was specified.");
            } else
                $opt['find'][] = $m[1] . str_replace(array('F', 'g'), array('', ''), $m[4]);
            $m[3] = str_replace('\n', "\n", $m[3]); // allow newlines in replace
            $opt['repl'][] = $m[3];
            if (strstr($m[4], 'g')) $opt['replcnt'][] = -1; 
            else $opt['replcnt'][] = 1;
            $opt['flag'][] = $m[4];
        } else {
            wdbg(1,"$func: Unmatched regex for expression $expr");
            wshStdErr($pagename, $opt, "$func: ERROR: Unknown expression: >>$expr<<");
        }
    }
    wdbg(1,"$func: Starts:"); wdbg(1,$opt['startline']);
    wdbg(1,"$func: Ends:"); wdbg(1,$opt['endline']);
    wdbg(1,"$func: Find:"); wdbg(1,$opt['find']);
    wdbg(1,"$func: Replace:"); wdbg(1,$opt['repl']);
    wdbg(1,"$func: Replace#:"); wdbg(1,$opt['replcnt']);
    wdbg(1,"$func: Flags:"); wdbg(1,$opt['flag']);
    $opt['printall'] = (isset($opt['n']) && $opt['n']) ? false : true;
    $WikiShVars['STATUS'] = 0;
    return(wshExtractLines($pagename, $opt, $args, $func));
}

# {(mailx OPTIONS PAGE)} 
# Send an email
# OPTIONS
# -t address
# --to:address    to whom will the message be sent
# -c address
# --cc:address    to whom will the message be CC'd
# -b address
# --bcc:address   to whom will the message be BCC'd
# -s SUBJ
# --subject:SUBJ  specify the subject of the message
# -f address
# --from:address  the message will appear to be from this address
$MarkupExpr["mailx"] = 'wshMailx($pagename, @$argp, @$args)'; 
function wshMailx($pagename, $opt, $args)
{
    global $WikiShVars, $EnableWikiShMailx;

    if (!$EnableWikiShMailx) {
        wshStdErr($pagename, $opt, "ERROR: Mailx is not enabled.  See \$EnableWikiShMailx");
        $WikiShVars['STATUS'] = 5;
        return('');
    }
    if (wshNotNow($pagename)) return('');
    $func = 'wshMailx()';
    $d = 0;
    wdbg($d*4,"$func: Entering: " . implode(" ", $args));
    if (!defined('WikiMail'))
        return('Mailx depends on WikiMail.  Aborting. Message NOT sent.');
    WikiShCompatible($pagename, $opt, $args, 
        WIKISH_ACTION_OPTIONS|WIKISH_ACTION_VARIABLE|WIKISH_ACTION_WILDCARD, 
        's:f:t:c:b:');
    #Handle aliases s=subject, f=from, t=to, c=cc, b=bcc
    if ($opt['s']) $opt['subject'] = $opt['s'];
    if ($opt['f']) $opt['from'] = $opt['f'];
    if ($opt['t']) $opt['to'] = $opt['t'];
    if ($opt['c']) $opt['cc'] = $opt['c'];
    if ($opt['b']) $opt['bcc'] = $opt['b'];
    #Process CC and BCC for headers
    $headers = '';
    if ($opt['cc']) // possibly an array if WikiSh handled it
        foreach ((array)$opt['cc'] as $cc)
            $headers .= "Cc:$cc\r\n";
    if ($opt['bcc']) // possibly an array if WikiSh handled it
        foreach ((array)$opt['bcc'] as $bcc)
            $headers .= "Bcc:$bcc\r\n";
    # Now build up the message from pages
    $rtn = '';
    foreach ($args as $filename) {
        $page = wshReadPage($pagename, $opt, $filename);
        if (wshIsABadFile($page)) {
            wshStdErr($pagename, $opt, "ERROR: $func: No such page: $page[filename]");
            continue;
        }
        $rtn = ($rtn ? $rtn . "\n" : '') . $page['text'];
    }
    $rtn = wshPostProcess($pagename, $opt, explode("\n", $rtn)); 
    wdbg($d*3,"$func: Message=$rtn");
    if ($x = wmSendMail($pagename, $opt['to'], $opt['subject'], $rtn, $headers, '', $opt['from'], ($opt['html']?'html':'text'), $WikiShVars['MAILXRULES'])) {
        $WikiShVars['STATUS'] = 1;
        $rtn = "Errors occurred.  Message NOT sent. ($x)";
    } else {
        $WikiShVars['STATUS'] = 0;
        $rtn = "Message Sent.";
    }
    return($opt['q']?'':$rtn);
}


# {(set OPTIONS VAR ARGS)} 
# Set $VAR to $ARGS[1] and return the value of whatever it was set to
# OPTIONS
# --pv      Set as a PAGEVAR instead of a WikiSh var (note that PVs are 
#           already interpolated and don't get reprocessed again)
# --session Set as a _SESSION variable instead of WikiSh var
# --form    Set as a $InputValues[] variables instead of WikiSh var
# -v        Return the value assigned instead of returning nothing
# -s        Set a string (put quotes around it and allow non-math symbols)
#CREDIT NOTE: About half of this function is taken DIRECTLY (verbatim) from
#the example given in MarkupExpressionSamples under {(calc)}.  I'm not trying
#to steal anything -- I'm totally willing to give FULL credit to whomever did
#that.
$MarkupExpr["${WikiShMXPrefix}set"] = 'wshSet($pagename, @$argp, @$args)'; 
function wshSet($pagename, $opt, $args) 
{
    global $WikiShVars, $InputValues;
    global $MxCalcMathChars, $MxCalcMathFunctions, $MxCalcMathFunc1;
    global $FmtPV;
    if (wshNotNow($pagename)) return('');
    $Ops = array("=","+=","-=","*=","/=",".=","%=","<","<=",">",">=","++","--");
    $func = "Set()";
    wdbg(4,"$func: Entering: setting: " . implode(" ", $args));
    wshInitOpts($pagename, '', $opt, $args);
    if (!in_array($args[1], $Ops)) {
        wshStdErr($pagename, $opt, "ERROR: $func: Invalid assignment operator \"$args[1]\".  No assignment possible. (set " . implode(" ", $args) . ")");
        return('');
    }
    wshSDOpt($opt, 'v', false);
    wshSDOpt($opt, 's', false);
    SDV($MxCalcMathChars, "/^[-+*,\\/% ()0-9.^=]+$/");
    SDV($MxCalcMathFunctions, array(
        'pow','sqrt','sin','cos','tan','asin','acos','atan','log',
        'max','min','abs','ceil','floor','round','rand','fmod',
        'pi','deg2rad','rad2deg',
    ));
    SDV($MxCalcMathFunc1, array(
        'sqrt','sin','cos','tan','asin','acos','atan','log',
        'abs','ceil','floor','round','deg2rad','rad2deg',
    ));    
    #print_r($args);
    if (@$opt['session']) {
        if(!session_id()) session_start();
        $v = $_SESSION[$args[0]];
    } elseif (@$opt['pv'])
        $v = $FmtPV['$'.$args[0]];
    else
        $v = $WikiShVars[$args[0]];
    wdbg(1,"$func: initial value = " . wshDbgOd($v));
    if ($opt['s']) {
        $op = $args[1];
        $expr = str_replace("'", "\\'", implode(" ", array_slice($args, 2)));
        $expr = $op . " '" . $expr . "'";
        wdbg(1,"$func: expr=>>$expr<<");
    } else {
        $expr = implode(" ", array_slice($args, 1));
        $str = $expr;
        foreach($MxCalcMathFunctions as $fn) {
            $str = str_replace($fn, '', $str);
        }
        if ($str!='' && !preg_match($MxCalcMathChars, $str, $m)) {
            wshStdErr($pagename, $opt, "ERROR: $func: illegal functions ($str) (did you forget -s?)");
            $WikiShVars['STATUS'] = 4;
            return '';
        }
        $expr = str_replace('pi', M_PI, $expr);
        $expr = preg_replace("/(\-?[\d.]+)\^(\-?[\d.]+)/", "pow($1, $2)", $expr);
        foreach($MxCalcMathFunc1 as $fn)
            $expr = preg_replace("/{$fn}\\s*(\-?[\d.]+)/", "{$fn}($1)", $expr);
    }
    wdbg(3,"$func: v=$v, eval'ing >>\\$v $expr;<<");
    wdbg(2,"$func: expr=" . wshDbgOd($expr));
    #echo "\$v $expr;<br>\n";
    eval("\$v $expr;");
    wdbg(4,"$func: after eval, v=" . $v);
    if ($opt['pv']) {
        if ($opt['s'])
            $FmtPV['$'.$args[0]] = "'" . $v . "'";
        else
            $FmtPV['$'.$args[0]] = $v;
        wdbg(2,"$func: after set, FmtPV follows:"); 
        foreach ($FmtPV as $key => $val) wdbg(1,"[" . wshDbgOd($key) . "] => " . wshDbgOd($val));
    } elseif (@$opt['form']) {
        $InputValues[$args[0]] = $v;
    } elseif (@$opt['session']) {
        $_SESSION[$args[0]] = $v;
        wdbg(1,"$func: after set, _SESSION follows:"); wdbg(1,$_SESSION);
    } else {
        $WikiShVars[$args[0]] = $v;
        wdbg(1,"$func: after set, WikiShVars follows:"); wdbg(1,$WikiShVars);
    }
    $WikiShVars['STATUS'] = 0;
    if ($opt['v'])
        return($v);
    else
        return('');
}

$MarkupExpr["${WikiShMXPrefix}sleep"] = 'wshSleep($pagename, @$argp, @$args)'; 
function wshSleep($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = "Sleep()";
    wdbg(4,"$func: Entering:  args[0]=$args[0]");
    wshInitOpts($pagename, '', $opt, $args);
    $seconds = $args[0];
    if (!is_numeric($seconds)) {
        $WikiShVars['STATUS'] = 4;
        wshStdErr($pagename, $opt, "ERROR: $func: non-numeric sleep argument: $seconds");
        return('');
    }
    usleep(floor($seconds*1000000));
    $WikiShVars['STATUS'] = 0;
    return('');
}

# {(sort OPTIONS PAGEPATTERN)} 
# sort all lines of all files passed
# Options: 
# -tx = make x the field separator
# d = dictionary-order
# b = ignore-leading-blanks
# f = ignore-case (fold)
# -k POS1[,POS2][OPTS]
#     POS = X[.Y] where X is a field (1-based) and Y is a character (1-based)
#                 within that field.  POS1 is where the sorting starts and
#                 POS2 is where the sorting stops.
#     Multiple -k options may be given.
#     OPTS consisting of d, b, and/or f may be specified for that given key to
#     override the overall options specified.
# PAGEPATTERN...  - source pages from PageName or Group.Pagename 
#       allowing wiki wildcards * and ?
#       (multiple files/patterns allowed)
#
# UNIQ options
# This function is called by wshUniq() and so these options need to be
# respected to work in that capacity.  Since they are respected a side effect
# is that wshSort() can be called directly with this functionality as well
# OPTIONS
# --ignore-case   (as above)
# --count         (prefix a line with the count of duplicates)
# --unique        (print only a single copy of each line, discarding dups)
#                 (this is the default operation of uniq)
# --duplicated    (print only a copy of duplicated lines, discarding unique 
#                 ones)
# --only-unique   (discard any lines which have duplicates. print only lines
#                 which are unique)
# --no-sort       (don't do the actual sort - just process the uniq/dup opts)
#
$MarkupExpr["${WikiShMXPrefix}sort"] = 'wshSort($pagename, @$argp, @$args)'; 
function wshSort($pagename, $opt, $args) 
{
    global $WikiShVars;
    $func = 'sort()';
    if (wshNotNow($pagename)) return('');
    wdbg(4,"$func: Entering");
    wdbg(1,$args);
    wshInitOpts($pagename, 't:k:', $opt, $args);
    wshExpandWildCards($pagename, $opt, $args, false, false, false);
    wshSDOpt($opt, 'file_prefix', '');
    wshSDOpt($opt, 'line_prefix', '');
    wshSDOpt($opt, 'line_suffix', '');
    $general_opt = '';
    if (@$opt['d'] || @$opt['dictionary-order'])      $general_opt .= 'd';
    if (@$opt['b'] || @$opt['ignore-leading-blanks']) $general_opt .= 'b';
    if (@$opt['f'] || @$opt['ignore-case'])           $general_opt .= 'f';
    if (@$opt['u']) $opt['unique'] = true;
    SDVA($opt, array('unique' => false, 'duplicated' => false, 
        'only-unique' => false, 'count' => false));
    if (isset($opt['k'])) $opt['key'] = $opt['k'];
    wshSDOpt($opt, 'key', '1.1'); // default to field 1, char 1
    if (!is_array($opt['key'])) $opt['key'] = array($opt['key']);
    $keys = array();
    foreach ($opt['key'] as $key) {
        if (preg_match("/^(\d+)(?:\.(\d+))?(?:,(\d+)(?:\.(\d+))?)?([bdf]*)$/", $key, $match)) {
            wdbg(1,"$func: Matched key ($key): $match[1].$match[2],$match[3].$match[4] opt=$match[5]");
            if (!$match[1]) $match[1] = 0; else $match[1]--;
            if (!$match[2]) $match[2] = 0; else $match[2]--; // must 0-base it (1st arg substr())
            if ($match[3]) $match[3]--;
            #if ($match[4]) $match[4]--; // don't 0-base it (2nd arg substr())
            $keys[] = array($match[1], $match[2], $match[3], $match[4], $match[5]);
        } else {
            wshStdErr($pagename, $opt, "$func: ERROR: incorrectly specified key: >>$key<<");
            $keys[] = array(0,0,"","");
            #what defaults?
        }
    }
    wdbg(1,"$func: list keys");
    wdbg(1,$keys);
    $ofs =  CHR(1); // I would use CHR(0) but preg_replace pukes on it
    if (isset($opt['t'])) $opt['field-separator'] = $opt['t'];
    wshSDOpt($opt, 'field-separator', '\s+');
    $ifs = $opt['field-separator'];
    $newrows = array(); $to_sort = array();
    foreach ($args as $filename) {
        $page = wshReadPage($pagename, $opt, $filename);
        if (wshIsABadFile($page)) {
            wshStdErr($pagename, $opt, "ERROR: $func: No such page: $page[filename]");
            continue;
        }
        wdbg(1,"$func: text read = >>" . wshDbgOd($page['text']) . "<<");
        $textrows = explode("\n", $page['text']);
        wdbg(1,"$func: textrows=");
        wdbg(1,$textrows);
        if ($opt['file_prefix'])
            $array_prefix = array(wshReplace($opt, $page, $opt['file_prefix']));
        else
            $array_prefix = array();
        $line_prefix = wshReplace($opt, $page, $opt['line_prefix']);
        $line_suffix = wshReplace($opt, $page, $opt['line_suffix']);
        if ($line_prefix || $line_suffix)
            for ($i = 0; $i < sizeof($textrows); $i++) {
                if (strstr($opt['line_prefix'], 'LINENO')) 
                    $line_prefix = wshReplace($opt, $page, $opt['line_prefix'], $i+1);
                $textrows[$i] = $line_prefix . $textrows[$i] . $line_suffix;
            }
        $newrows = array_merge($newrows, $array_prefix, $textrows);
    }
    foreach($newrows as $origstr) {
        wdbg(1,"$func: Applying sort keys/options to line >>$origstr<<");
        $fields = preg_split("/$ifs/", $origstr);
        $line2sort = '';
        wdbg(1,$keys);
        foreach ($keys as $key) {
            wdbg(1,"$func: key: $key[0].$key[1],$key[2].$key[3] opts=$key[4]");
            $tmp = '';
            if ($key[0] === $key[2] && ($key[2] || $key[2] === 0)) {
                wdbg(1,"$func: one-key optimize...");
                if ($key[3]) {
                    $tmp .= substr($fields[$key[0]], $key[1], $key[3]);
                } else {
                    $tmp .= substr($fields[$key[0]], $key[1]);
                }
            } else {
                wdbg(1,"$func: multi-key full feature...");
                $tmp .= substr($fields[$key[0]], $key[1]);
                $maxidx = ($key[2] || $key[2] === 0) ? $key[2] : sizeof($fields);
                for ($i = $key[0]+1; $i < $maxidx; $i++) {
                    $tmp .= $ofs . $fields[$i];
                }
                if ($key[2] || $key[2] === 0) {
                    if ($key[3]) {
                        $tmp .= $ofs . substr($fields[$key[2]], 0, $key[3]);
                    } else {
                        $tmp .= $ofs . $fields[$key[2]];
                    }
                }
            }
            wdbg(1,"$func: After key applied: >>$tmp<<");
            # Now we've got the text for this key - now apply options to $tmp
            if (strpos($general_opt . $key[4], 'd') !== false)
                $tmp = preg_replace("/[^${ofs}A-Za-z0-9 \t]/", "", $tmp);
            if (strpos($general_opt . $key[4], 'b') !== false)
                $tmp = preg_replace("/^[ \t]*/", "", $tmp);
            if (strpos($general_opt . $key[4], 'f') !== false) {
                wdbg(0,"$func: strtolowering...");
                $tmp = strtolower($tmp);
            }
            wdbg(1,"$func: After opts applied: >>$tmp<<");
            # Now add $tmp onto the end of line2sort and go try the next key
            $line2sort .= $ofs . $tmp;
        }
        wdbg(1,"$func: Final 'line' to sort >>$line2sort<<");
        $to_sort[] = array($line2sort, $origstr);
    }
    if (!@$opt['no-sort']) {
        wdbg(0,"$func: array to sort:");
        wdbg(0,$to_sort);
        $WikiShVars['STATUS'] = 0;
        if (@$opt['r'] || @$opt['reverse']) {
            if (!rsort($to_sort)) {
                $WikiShVars['STATUS'] = 4;
                return('');
            }
        } else {
            if (!sort($to_sort)) {
                $WikiShVars['STATUS'] = 4;
                return('');
            }
        }
    }
    #
    # Now implement --unique, --duplicated, --count, --only-unique
    #   --duplicated and --only-unique are obviously mutually exclusive
    #   --duplicated and --only-unique over-ride --unique
    #
    $newrows = array();
    if ($opt['unique'] || $opt['duplicated'] || $opt['only-unique'] || $opt['count']) {
        wdbg(1,"$func: array follows");
        wdbg(1,$to_sort);
        $lastval = false;
        $lastline = false;
        $dupcnt = 0;
        foreach ($to_sort as $cur_line) {
            wdbg(1,"$func: processing lastline=$lastline, cur_line=$cur_line[1]");
            if ($lastval === $cur_line[0]) {
                wdbg(1,"$func: dup");
                $dupcnt++;
            } else {
                wdbg(1,"$func: change");
                if ($lastval !== false && 
                       (($opt['duplicated'] && $dupcnt > 1) || 
                        ($opt['only-unique'] && $dupcnt == 1) ||
                        (!$opt['duplicated'] && !$opt['only-unique']))) {
                    wdbg(1,"$func: printing");
                    $newrows[] = (($opt['count']) ? "$dupcnt: ":'') . $lastline;
                }
                $lastval = $cur_line[0];
                $lastline = $cur_line[1];
                $dupcnt = 1;
            }
        }
        if ($lastval !== false && 
                (($opt['duplicated'] && $dupcnt > 1) || 
                ($opt['only-unique'] && $dupcnt == 1) ||
                (!$opt['duplicated'] && !$opt['only-unique']))) {
            wdbg(1,"$func: printing last");
            $newrows[] = ($opt['count']?"$dupcnt: ":'') . $lastline;
        }
    } else {
        foreach($to_sort as $tinyarray) {
            $newrows[] = $tinyarray[1];
        }
    }
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

# {(tail OPTIONS PAGEPATTERN)} 
# Print n lines from the end of a file
# Options: 
# file_prefix="string" - string which will become the line leading each set 
#                   of lines from the current file.  
# line_prefix="string" - string which will become the prefix to each line 
# line_suffix="string" - string which will become the prefix to each line 
# PAGEPATTERN...  - source pages from PageName or Group.Pagename 
#       allowing wiki wildcards * and ?
#       (multiple files/patterns allowed)
$MarkupExpr["${WikiShMXPrefix}tail"] = 'wshTail($pagename, @$argp, @$args)'; 
function wshTail($pagename, $opt, $filelist) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = 'Tail()';
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, 'n:', $opt, $filelist);
    wshSDOpt($opt, 'n', -10);
    wdbg(1,$opt['n']);
    if ($opt['n'] > 0) $opt['n'] *= -1;
    $opt['startline'][] = $opt['n'];
    $opt['endline'][] = '$';
    $WikiShVars['STATUS'] = 0;
    return(wshExtractLines($pagename, $opt, $filelist, $func));
}

# {(test OPTIONS PAGEPATTERN)} 
# Implement various methods of boolean testing based on options
# Options: 
# PAGEPATTERN...  - source pages from PageName or Group.Pagename 
#       allowing wiki wildcards * and ?
#       (multiple files/patterns allowed)
# FILE OPTIONS:
#   -d FILE (is a directory/group)
#   -e FILE (exists)
#   -f FILE (exists)
#   -g GROUP (does it exist as a group) (non-shell)
#   -r FILE (is readable)
#   -s FILE (file has something - is non-zero length)
#   -w FILE (is writable)
#   FILE1 -nt FILE2  is file1 newer than file2 by modification date
#                    (or if file1 exists and file2 does not)
#   FILE1 -ot FILE2  is file1 older than file2 by modification date
#                    (or if file2 exists and file1 does not)
# STRING OPTIONS
#   -z STRING (true if string is zero length)
#   -n STRING (true if string is non-zero length)
#   string1 == string2
#   string1 != string2
#   string1 < string2
#   string1 > string2
#   string1 ~= /pat/ (non-shell - regex match)
# INTEGER OPTIONS
#   INT1 OPERATOR INT2
#     OPERATOR: -eq, -ne, -lt, -le, -gt, -ge
#
$MarkupExpr["${WikiShMXPrefix}test"] = 'wshTest($pagename, @$argp, @$args)'; 
function wshTest($pagename, $opt, $args) 
{
    global $WikiShVars, $Conditions, $wshAuthPage, $wshAuthText;
    global $EnableWikiShTextRead, $EnableWikiShTextWrite;
    global $KeepToken, $KPV;
    $func = "Test()";
    wdbg(4,"$func: Entering args=>>" . implode(" ", $args) . "<<");
    $rpat = "/$KeepToken(\\d+P)$KeepToken/e";
    $rrep = '$KPV[\'$1\']';
    if (wshNotNow($pagename, true)) return('');
    // Since wshTest does not call wshInitOpts() but we want a --pmwiki option
    // we have to do this little workaround...
    if (is_numeric($i = array_search('--pmwiki', $args))) {
        $opt['pmwiki'] = true;
        unset($args[$i]);
    }
    $oplist = array('-d','-e','-f','-g','-r','-s','-w',
        '-n','-z',
        '-ot','-nt',
        '-eq','-ne','-lt','-le','-gt','-ge',
        '==','!=','<','>', '>=', '<=', '~=');
    $state = 0; // 0=starting new, 1=arg1 collected, 2=op collected
    $rtn = false;
    wshExpandVars($pagename, $opt, $args);
    array_push($args, ''); // add a blank onto the end for quoting probs
    if (@$opt['pm'] || @$opt['pmwiki']) {
        $condspec = implode(" ", $args);
        if (!preg_match("/^\\s*(!?)\\s*(\\S*)\\s*(.*?)\\s*$/", $condspec, $match)) 
            echo "ERROR: No Match on condspec \"$condspec\"<br>\n";
        list($x, $not, $condname, $condparm) = $match;
        if (!isset($Conditions[$condname])) {
            $WikiShVars['STATUS'] = 4;
            wshStdErr($pagename, $opt, "ERROR: $func: Unknown condition \"$condname\"");
            return('');
        }
        $tf = @eval("return ({$Conditions[$condname]});");
        if ($tf xor $not) $WikiShVars['STATUS'] = 0;
        else $WikiShVars['STATUS'] = 1;
        return('');
    } else {
        foreach ($args as $arg) {
            if ($state == 3) continue; // got the spare blank one - ignore it
            wdbg(2,"$func: Processing arg=\"$arg\", state=$state");
            if ($state == 2) {
                $arg2 = $arg;
                switch ($op) {
                ##
                ## HANDLE INTEGER OPERATORS
                ##
                case '-ne':
                case '-eq':
                case '-lt':
                case '-le':
                case '-gt':
                case '-ge':
                    wdbg(2,"$func: op=\"$op\"");
                    if (!is_numeric($arg1)) {
                        wshStdErr($pagename, $opt, "$func: ERROR: non-numeric arg1 ($arg1) for numeric operator ($op). Treating as 0.");
                        $arg1 = 0;
                    }
                    if (!is_numeric($arg2)) {
                        wshStdErr($pagename, $opt, "$func: ERROR: non-numeric arg2 ($arg2) for numeric operator ($op). Treating as 0.");
                        $arg2 = 0;
                    }
                    switch ($op) {
                    case '-eq':
                        $rtn = ($arg1 == $arg2);
                        break;
                    case '-ne':
                        $rtn = ($arg1 != $arg2);
                        break;
                    case '-lt':
                        $rtn = ($arg1 < $arg2);
                        break;
                    case '-le':
                        $rtn = ($arg1 <= $arg2);
                        break;
                    case '-gt':
                        $rtn = ($arg1 > $arg2);
                        break;
                    case '-ge':
                        $rtn = ($arg1 >= $arg2);
                        break;
                    }
                    wdbg(2,"$func: int op $op: returning " . (($rtn) ? "TRUE" : "FALSE"));
                    $arg1 = $op = $arg2 = $negop = '';
                    $state = 3;
                    break;
                ##
                ## HANDLE STRING OPERATORS
                ##
                case '==':
                case '!=':
                case '>=':
                case '<=':
                case '~=':
                case '<':
                case '>':
                case '-z':
                case '-n':
                    wdbg(2,"$func: STRING op=\"$op\" (arg1=@$arg1, arg2=@$arg2)");
                    switch ($op) {
                    case '==':
                        $rtn = (strcmp($arg1, $arg2) == 0);
                        break;
                    case '!=':
                        $rtn = (strcmp($arg1, $arg2) != 0);
                        break;
                    case '>=':
                        $rtn = (strcmp($arg1, $arg2) >= 0);
                        break;
                    case '<=':
                        $rtn = (strcmp($arg1, $arg2) <= 0);
                        break;
                    case '~=':
                        wdbg(3,"$func: doing preg_match(\"$arg2\", \"$arg1\")");
                        wdbg(2,"$func: arg1=" . wshDbgOd($arg1));
                        $rtn = (preg_match($arg2, $arg1));
                        break;
                    case '<':
                        $rtn = (strcmp($arg1, $arg2) < 0);
                        break;
                    case '>':
                        $rtn = (strcmp($arg1, $arg2) > 0);
                        break;
                    case '-z':
                        $rtn = (strlen($arg2) == 0);
                        break;
                    case '-n':
                        $rtn = (strlen($arg2) > 0);
                        break;
                    }
                    $arg1 = $op = $arg2 = $negop = '';
                    $state = 3;
                    break;
                ##
                ## HANDLE FILE OPERATORS
                ##
                #### NOTE: I should *really* "optimize" the lines of code by 
                #### reusing code as I've done above...
                case '-d':
                case '-g': // group/directory exists
                    $arg2 = $arg2 . '.*';
                case '-e':
                case '-f': // exists
                    wdbg(2,"$func: op=\"$op\"");
                    wdbg(2, "DEBUG: arg2=".print_r($arg2,true)."<br>\n");
                    $tmparg = array(wshMakePageName($pagename, $opt, $arg2));
                    wshExpandWildCards($pagename, $opt, $tmparg, true);
                    $filename = $tmparg[0];
                    wdbg(2,"$func: -f|-g Checking if PageExists($filename)".wshDbgOd($filename, true));
                    if (wshIsATextFile("", $filename)) {
                        $filename = preg_replace($rpat, $rrep, $filename);
                        $filename = substr($filename, strlen(TEXTFILEID));
                        wdbg(1, "DEBUG: Checking text file with file_exists($filename)<br>\n");
                        $rtn = ($EnableWikiShTextRead && file_exists($filename) && slAuthorized($filename, $wshAuthText, 'read', true));
                    } elseif (wshIsASessionFile('', $filename) || wshIsASessionGroup($filename)) {
                        if (wshIsASessionFile('', $filename))
                            $filename = substr($filename, strlen(SESSFILEID));
                        $rtn = isset($_SESSION[$filename]);
                    } else 
                        $rtn = PageExists($filename);
                    wdbg(2,"$func: -f|-g PageExists() returns " . 
                        ($rtn?"TRUE":"FALSE") . " (negop=$negop)");
                    $rtn = (($rtn && !$negop) || ($negop && !$rtn));
                    $arg1 = $op = $arg2 = $negop = '';
                    $state = 3;
                    break;
                case '-r': // exists and is readable
                    wdbg(2,"$func: op=\"$op\"");
                    $tmparg = array(wshMakePageName($pagename, $opt, $arg2));
                    wshExpandWildCards($pagename, $opt, $tmparg, true);
                    $filename = $tmparg[0];
                    wdbg(2,"$func: -r Checking if PageExists($tmparg[0]) xor >>$negop<<");
                    if (wshIsATextFile("", $filename)) {
                        $filename = preg_replace($rpat, $rrep, $filename);
                        $filename = substr($filename, strlen(TEXTFILEID));
                        wdbg(1, "DEBUG: Checking text file with file_exists($filename)<br>\n");
                        $rtn = ($EnableWikiShTextRead && is_readable($filename) && slAuthorized($filename, $wshAuthText, 'read', true));
                    } elseif (wshIsASessionFile('', $filename) || wshIsASessionGroup($filename)) {
                        if (wshIsASessionFile('', $filename))
                            $filename = substr($filename, strlen(SESSFILEID));
                        $rtn = isset($_SESSION[$filename]); // session files always readable
                    } else 
                        $rtn = (PageExists($filename) && slAuthorized($filename, $wshAuthPage, 'read') && (CondAuth($filename, 'read') || slAuthorized($filename, $wshAuthPage, 'forceread')));
                    wdbg(2,"$func: -r PageExists() && CondAuth() && slAuthorized() return " . ($rtn?"TRUE":"FALSE") . " (negop=$negop)");
                    $rtn = (($rtn && !$negop) || ($negop && !$rtn));
                    $arg1 = $op = $arg2 = $negop = '';
                    $state = 3;
                    break;
                case '-w': // exists and is writable
                    wdbg(2,"$func: op=\"$op\"");
                    $tmparg = array(wshMakePageName($pagename, $opt, $arg2));
                    wshExpandWildCards($pagename, $opt, $tmparg, true);
                    $filename = $tmparg[0];
                    wdbg(2,"$func: -w Checking if PageExists($tmparg[0]) xor >>$negop<<");
                    if (wshIsATextFile("", $filename)) {
                        $filename = preg_replace($rpat, $rrep, $filename);
                        $filename = substr($filename, strlen(TEXTFILEID));
                        wdbg(1, "DEBUG: Checking text file with file_exists($filename)<br>\n");
                        $rtn = ($EnableWikiShTextWrite && is_writable($filename) && slAuthorized($filename, $wshAuthText, 'overwrite', true));
                    } elseif (wshIsASessionFile('', $filename) || wshIsASessionGroup($filename)) {
                        if (wshIsASessionFile('', $filename))
                            $filename = substr($filename, strlen(SESSFILEID));
                        $rtn = isset($_SESSION[$filename]); // session files always writable
                    } else // regular pmwiki page
                        $rtn = (PageExists($filename) && slAuthorized($filename, $wshAuthPage, 'overwrite') && (CondAuth($filename, 'edit') || slAuthorized($filename, $wshAuthPage, 'forceedit')));
                    wdbg(2,"$func: -w PageExists() && CondAuth() return " . 
                        ($rtn?"TRUE":"FALSE") . " (negop=$negop)");
                    $rtn = (($rtn && !$negop) || ($negop && !$rtn));
                    $arg1 = $op = $arg2 = $negop = '';
                    $state = 3;
                    break;
                case '-s': // exists and is readable and has *something* in it
                    wdbg(2,"$func: op=\"$op\"");
                    $tmparg = array(wshMakePageName($pagename, $opt, $arg2));
                    wshExpandWildCards($pagename, $opt, $tmparg, true);
                    $filename = $tmparg[0];
                    wdbg(2,"$func: -s Checking if PageExists($tmparg[0]) xor >>$negop<<");
                    if (wshIsATextFile("", $filename)) {
                        $filename = preg_replace($rpat, $rrep, $filename);
                        $filename = substr($filename, strlen(TEXTFILEID));
                        wdbg(1, "DEBUG: Checking text file with filesize($filename) <br>\n");
                        $rtn = ($EnableWikiShTextRead && file_exists($filename) && filesize($filename) && slAuthorized($filename, $wshAuthText, 'read', true));
                    } elseif (wshIsASessionFile('', $filename) || wshIsASessionGroup($filename)) {
                        if (wshIsASessionFile('', $filename))
                            $filename = substr($filename, strlen(SESSFILEID));
                        $rtn = (boolean)@$_SESSION[$filename]['text'];
                    } else { // regular pmwiki page
                        $rtn = false; // assume either no exist or no contents
                        if (PageExists($filename)) {
                            $page = wshRetrieveAuthPage($filename, 'read', false);
                            wdbg(2,"$func: text=>>" . $page['text'] . "<<");
                            if (!$page) {
                                wshStdErr($pagename, $opt, "$func: ERROR: error reading page >>$filename<< (authorizations perhaps?)");
                                $WikiShVars['STATUS'] = 2;
                                return ('');
                            }
                            $rtn = (boolean)$page['text'];
                        }
                    }
                    if ($negop)
                        $rtn = !$rtn;
                    $arg1 = $op = $arg2 = $negop = '';
                    $state = 3;
                    break;
                case '-ot': // arg1 is older than arg2
                    $tmp  = $arg1;
                    $arg1 = $arg2;
                    $arg2 = $tmp;
                    // no break intentionally...
                case '-nt': // arg1 is newer than arg2
                    wdbg(2,"$func: op=\"$op\"");
                    $tmparg = array(wshMakePageName($pagename, $opt, $arg1));
                    wshExpandWildCards($pagename, $opt, $tmparg, true, true);
                    $filename1 = $tmparg[0];
                    $tmparg = array(wshMakePageName($pagename, $opt, $arg2));
                    wshExpandWildCards($pagename, $opt, $tmparg, true, true);
                    $filename2 = $tmparg[0];
                    wdbg(2,"$func: $op Checking if PageExists($filename1,$filename2)");
                    if (!PageExists($filename1)) {
                        wshStdErr($pagename, $opt, "$func: ERROR: page $filename1 does not exist");
                        $WikiShVars['STATUS'] = 2;
                        return ('');
                    }
                    if (!PageExists($filename2)) {
                        wshStdErr($pagename, $opt, "$func: ERROR: page $filename2 does not exist");
                        $WikiShVars['STATUS'] = 2;
                        return ('');
                    }
                    $page1 = wshRetrieveAuthPage($filename1, 'read', false);
                    if (!$page1) {
                        wshStdErr($pagename, $opt, "$func: ERROR: error reading page >>$filename1<<");
                        $WikiShVars['STATUS'] = 2;
                        return('');
                    }
                    $page2 = wshRetrieveAuthPage($filename2, 'read', false);
                    if (!$page2) {
                        wshStdErr($pagename, $opt, "$func: ERROR: error reading page >>$filename2<<");
                        $WikiShVars['STATUS'] = 2;
                        return('');
                    }
                    wdbg(2,"$func: time(" . $filename1 . ")=" . $page1['time']);
                    wdbg(2,"$func: time(" . $filename2 . ")=" . $page2['time']);
                    $rtn = ($page1['time']+0 > $page2['time']+0);
                    wdbg(2,"$func: time " . $page1['time'] . " > time " . $page2['time'] . ": returned " .  ($rtn?"TRUE":"FALSE"));
                    $arg1 = $op = $arg2 = $negop = '';
                    $state = 3;
                    break;
                default:
                    wdbg(1,"$func: UNKNOWN op=\"$op\"");
                    wshStdErr($pagename, $opt, "$func: ERROR: Unknown operator option \"$op\"");
                }
                continue;
            }
            if ($arg == '!') {
                $negop = true;
                // no need to change state...
                continue;
            } elseif (in_array($arg, $oplist)) {
                if ($state > 1) {
                    wshStdErr($pagename, $opt, "$func: ERROR: Argument mismatch.  $arg unexpected.");
                    $WikiShVars['STATUS'] = 4;
                    return('');
                }
                $op = $arg;
                $state = 2;
                continue;
            } else {
                $arg1 = $arg;
                $state = 1;
                wdbg(2,"$func: arg1=$arg1");
                continue;
            }
        }
    }
    wdbg(4,"$func: Returning " . ($rtn ? "TRUE" : "FALSE"));
    $WikiShVars['STATUS'] = ($rtn ? 0 : 1);
    return('');
}

# {(true)}
# return nothing.  Set $STATUS to zero.
$MarkupExpr["${WikiShMXPrefix}true"] = 'wshTrue($pagename, @$argp, @$args)'; 
function wshTrue($pagename, $opt, $args) 
{
    global $WikiShVars;
    wshNotNow($pagename); // just for RC initializations - don't care about return val
    $func = "True()";
    wdbg(4,"$func: Entering");
    $WikiShVars['STATUS'] = 0;
    return('');
}

# {(uniq OPTIONS PAGEPATTERN)} 
# Return unique lines in an already sorted list
#
# NOTE: NON-SHELL EXCEPTION.  I have chosen to use the OPTIONS from sort rather
# then from uniq.  Those options are much more flexible for specifying keys.
#
# Options: 
# -d only print duplicate lines
#    NOTE: If you want to pass -d onto sort then use --dictionary-order
# -c prefix lines by a count of the number of occurrences of that line
# -f,-s,-w -- THESE ARE *NOT* IMPLEMENTED -- USE -k POS1,POS2 syntax instead
# -u only print unique lines (omit duplicate lines)
#
# As a result of the above exception this function will simply call wshSort()
# with a slightly altered set of options...
#
$MarkupExpr["${WikiShMXPrefix}uniq"] = 'wshUniq($pagename, @$argp, @$args)'; 
function wshUniq($pagename, $opt, $args) 
{
    global $WikiShVars;
    $func = 'uniq()';
    if (wshNotNow($pagename)) return('');
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, '', $opt, $args);
    # These options could be ambiguous with similarly named options within
    # sort() and so they are being renamed to their long version here.
    wdbg(1,"$func: checking options for rename");
    if (@$opt['i']) {
        wdbg(1,"$func: -i");
        $opt['ignore-case'] = true;
        unset($opt['i']);
    }
    if (@$opt['c'] || @$opt['count']) {
        wdbg(1,"$func: -c");
        $opt['count'] = true;
        unset($opt['c']);
    }
    if (@$opt['d'] || @$opt['duplicated']) {
        wdbg(1,"$func: -d");
        $opt['duplicated'] = true;
        unset($opt['d']);
    }
    # only-unique means if a line is duplicated you print NOTHING, not even
    # one of them.  If you're wondering how to pass the -u semantics into
    # sort, that is the default meaning of calling uniq, so there's no need
    # to specify it.
    if (@$opt['u'] || @$opt['unique']) {
        wdbg(1,"$func: -u");
        $opt['only-unique'] = true;
        unset($opt['u']);
        unset($opt['unique']);
    }
    # These options make wshSort() behave like uniq.
    $opt['no-sort'] = true; // uniq does not imply a sort
    $opt['unique'] = true;  // make sort -u do its stuff
    wdbg(1,"$func: Calling Sort()");
    return (wshSort($pagename, $opt, $args));
}

# {(wc OPTIONS PAGEPATTERN)} 
# Give stats on wordcount, charactercount, or linecount or all of the above
# Options: 
# -l -- just linecount
# -c -- just charactercount
# -w -- just wordcount
# -q -- suppress labels 'Line: ', 'Word: ', and 'Character: '
# --only_total only print the total, not per-file counts (total_only synonym)
# (if none of the above then assume all 3 of the above)
# PAGEPATTERN...  - source pages from PageName or Group.Pagename 
#       allowing wiki wildcards * and ?
#       (multiple files/patterns allowed)
$MarkupExpr["${WikiShMXPrefix}wc"] = 'wshWc($pagename, @$argp, @$args)'; 
function wshWc($pagename, $opt, $args) 
{
    global $WikiShVars;
    if (wshNotNow($pagename)) return('');
    $func = 'wc()';
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, '', $opt, $args);
    wshExpandWildCards($pagename, $opt, $args, false, false, false);
    wshSDOpt($opt, 'only_total', false);
    # This is a little messy, but nice for users to not have to remember
    wshSDOpt($opt, 'total_only', false);
    if ($opt['total_only']) $opt['only_total'] = $opt['total_only'];
    wshSDOpt($opt, 'file_prefix', '');
    wshSDOpt($opt, 'line_prefix', '');
    wshSDOpt($opt, 'line_suffix', '');
    wshSDOpt($opt, 'l', false);
    wshSDOpt($opt, 'w', false);
    wshSDOpt($opt, 'c', false);
    wshSDOpt($opt, 'q', false);
    if (!$opt['l'] && !$opt['w'] && !$opt['c'])
        $opt['l'] = $opt['w'] = $opt['c'] = true;
    $newrows = array();
    $total_wc = $total_lc = $total_cc = $wordcnt = $linecnt = $charcnt = 0;
    foreach ($args as $filename) {
        $page = wshReadPage($pagename, $opt, $filename);
        if (wshIsABadFile($page)) {
            wshStdErr($pagename, $opt, "ERROR: $func: No such page: $page[filename]");
            continue;
        }
        wdbg(0,"$func: text");
        wdbg(0,$page['text']);
        if ($opt['l'])
            $linecnt = substr_count($page['text'], "\n") + 1;
        if ($opt['c'])
            $charcnt = strlen($page['text']) - $linecnt;
        if ($opt['w'])
            $wordcnt = str_word_count($page['text']);
        $total_wc += $wordcnt;
        $total_lc += $linecnt;
        $total_cc += $charcnt;
        if (!$opt['only_total']) {
            if ($opt['file_prefix']) {
                $file_prefix = wshReplace($opt, $page, $opt['file_prefix']);
                $newrows[] = $file_prefix;
            }
            $myline = wshReplace($opt, $page, $opt['line_prefix']);
            if ($opt['l']) $myline .= (($opt['q']) ? "" : "lines: ") . "$linecnt ";
            if ($opt['w']) $myline .= (($opt['q']) ? "" : "words: ") . "$wordcnt ";
            if ($opt['c']) $myline .= (($opt['q']) ? "" : "characters: ") . "$charcnt ";
            $myline .= wshReplace($opt, $page, $opt['line_suffix']);
            $newrows[] = $myline;
        }
    }
    if (sizeof($args) > 1) {
        $funnypage['filename'] = 'TOTAL';
        $funnypage['title'] = 'TOTAL';
        if ($opt['file_prefix']) {
            $file_prefix = wshReplace($opt, $funnypage, $opt['file_prefix']);
            $newrows[] = $file_prefix;
        }
        $myline = wshReplace($opt, $funnypage, $opt['line_prefix']);
        if ($opt['l']) $myline .= (($opt['q']) ? "" : "lines: ") . "$total_lc ";
        if ($opt['w']) $myline .= (($opt['q']) ? "" : "words: ") . "$total_wc ";
        if ($opt['c']) $myline .= (($opt['q']) ? "" : "characters: ") . "$total_cc ";
        $myline .= wshReplace($opt, $funnypage, $opt['line_suffix']);
        $newrows[] = $myline;
    }
    $WikiShVars['STATUS'] = 0;
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

$MarkupExpr['wikish_active'] = 'wshActive($pagename, preg_replace($rpat, $rrep, $params))'; 
function wshActive($pagename, $expr)
{
    global $WikiShVars;
    if (wshNotNow($pagename, true)) return(Keep('', 'P'));
    $func = "wikishactive()";
    #echo "$func: Entering<br>\n";
    wdbg(4,"$func: Entering");
    $expr = substr($expr, 1); // for some reason it gives a leading space
    wdbg(1,$expr);
    $expr = '(' . $expr . ')';
    wdbg(4,"$func: evaling \"$expr\"");
    #echo "$func: evaling $expr<br>\n";
    $rtn = MarkupExpression($pagename, $expr);
    #echo "$func: status=$WikiShVars[STATUS]<br>\n";
    if ($WikiShVars['STATUS'] == 0) {
        $WikiShVars['ACTIVE'] = true;
    } else {
        $WikiShVars['ACTIVE'] = false;
    }
    #echo "$func: Leaving with active=" . ($WikiShVars['ACTIVE'] ? "TRUE" : "FALSE") . "<br>\n";
    return(Keep($rtn, 'P'));
}

# {(wikish_button OPTIONS ID LABEL)}
# OPTIONS:
#   -r       - return a useful value (normally returns an empty string) of 1
#              for true (the first time) and 0 for false (subsequent 
#              invocations)
#   -q       - don't generate a button - just activate/deactivate based on $_GET
#*  -n       - no form header (already generated - we don't need to do it)
#*  -g|-p    - get vs post method (-g=default) (or --method:get/post?)
#* these are not implemented
# ARGUMENT: 
#   id       - a string to uniquely define THIS call to once() (REQUIRED) 
#              Non-alphanumerics (plus underscore & dash) will be ignored in
#              creating the actual ID.  Be aware of this for uniqueness.
#   label    - a string which appears on the button itself (Optional. Will use
#              ID for label if not specified for the button.  Will be ignored
#              if -q is set.)
# This function will display a button.  The button will set certain GET
# parameters when it is set.  If those parameters are set then WikiSh will
# be activated.  If those parameters are not set then WikiSh will be
# deactivated.
#
$MarkupExpr["wikish_button"] = 'wshButton($pagename, @$argp, @$args)'; 
function wshButton($pagename, $opt, $args) 
{
    global $WikiShVars, $wshFormStarted;
    if (wshNotNow($pagename, true)) return ('');
    #print_r($opt);
    #echo "<br>\n";
    #print_r($args);
    #echo "<br>\n";
    $func = 'wikish_button()';
    wdbg(4,"$func: Entering");
    wshInitOpts($pagename, "", $opt, $args);
    wshSDOpt($opt, 'r', false);
    wshSDOpt($opt, 'q', false);
    if (!@$args[1]) $args[1] = $args[0];
    $args[0] = preg_replace("/[^A-Za-z_-]/", "", $args[0]);
    SDV($wshFormStarted, false);
    $rtn = '';
    if (!$wshFormStarted && !$opt['q']) {
        $rtn .= '(:input form method="GET":)'."\n".'(:input hidden name=n value="{*$FullName}":)';
        $wshFormStarted = true;
    }
    if (!$opt['q'])
        $rtn .= "(:input submit abc name=\"$args[0]\" value=\"$args[1]\":)";
    if ($_GET[$args[0]]) {
        wdbg(1,"$func: activating (" . $args[0] . "->" . $_GET[$args[0]] . ")");
        $WikiShVars['ACTIVE'] = true;
    } else {
        wdbg(1,"$func: DEactivating (" . $args[0] . "->" . $_GET[$args[0]] . ")");
        wdbg(0,$_GET);
        wdbg(0,$args);
        $WikiShVars['ACTIVE'] = false;
    }
    wdbg(1,"$func: Returning: " . wshDbgOd($rtn));
    return($rtn);
}


# {(wikish_once OPTIONS ONCE-ID KEY)}
# OPTIONS:
#   -c page  - a page to use instead of $WikiShControlPage to hold the static
#              values needed to implement (once...)
#   -r       - return a useful value (normally returns an empty string) of 1
#              for true (the first time) and 0 for false (subsequent 
#              invocations)
# ARGUMENT: 
#   once-id  - a string to uniquely define THIS call to once()
#   key      - a string you can change to re-run it.
# Note that you could just use a single string, but using the 2 strings
# makes the controlpage much more readable and etc
# This function is true a single time with the current set of arguments.  
# It requires read & write privilege to the file named in $WikiShControlPage.  
# If it does not have read & write privilege then it will NEVER return true.
# Note that read/write privilege include setting of appropriate $Enable* vars.
# Note that "return true" really means returning a STATUS of 0.
$MarkupExpr["wikish_once"] = 'wshOnce($pagename, @$argp, @$args)'; 
function wshOnce($pagename, $opt, $args) 
{
    global $WikiShVars, $WikiShControlPage;
    #echo "once: entering (" . microtime() . ")<br>\n";
    if (wshNotNow($pagename, true)) return ('');
    #print_r($opt);
    #echo "<br>\n";
    #print_r($args);
    #echo "<br>\n";
    $func = 'once()';
    wdbg(4,"$func: Entering");
    # Establish the real name of WikiShControlPage (prepending group if needed)
    wshInitOpts($pagename, 'c:', $opt, $args);
    wshSDOpt($opt, 'r', false);
    if ($opt['c'])
        $ControlPage = $opt['c'];
    else
        $ControlPage = $WikiShControlPage;
    if (!strpos($ControlPage, '.') && !strpos($ControlPage, '/')) {
        wdbg(1,"$func: No dot in ControlPage=$ControlPage");
        // Hmmm... They forgot a group name.  That doesn't work.
        $Grp = PageVar($pagename, '$Group');
        $ControlPage = $Grp . '.' . $ControlPage;
        wdbg(1,"$func: Altered filename to $ControlPage (based on $pagename)");
    }
    # Set the pattern we will search for from $pagename and $args
    $pat = ':' . $pagename;
    if (sizeof($args) > 1) {
        $pat .= '-' . array_shift($args);
    }
    $pat .= ':' . implode(" ", $args);
    # Grep for the pattern
    $rtn = '';
    wshGrep($pagename, array('q'=>true), array("^$pat$", $ControlPage));
    if ($WikiShVars['STATUS'] == 0) {
        # If found then we shouldn't run again - return false
        $WikiShVars['STATUS'] = 1;
        wdbg(4,"$func: Returning (a) with STATUS=" . $WikiShVars['STATUS']);
        return($opt['r'] ? '0' : '');
    } else {
        # not found - WRITE the pattern to $WikiShControlPage
        # if successful write to $WikiShControlPage then return true
        wshEcho($pagename, array('stdout'=>$ControlPage, 'stdout_append'=>true), array("$pat"));
        # $WikiShVars['STATUS'] was set by echo - if it succeeded then we
        # succeed.  If it didn't succeed then we don't succeed.  Thus no
        # need to check it or set it.
        wdbg(4,"$func: Returning (b) with STATUS=" . $WikiShVars['STATUS']);
        return($opt['r'] ? '1' : '');
    }
}

# WikiShSystemCall
#
# Run a given system command after doing variable substitution.
# NOTE that this is *not* part of *any* MX nor markup unless the system
# administrator (or another recipe) creates said MX or markup.
# NOTE that this means the presence of this command is no more threatening
# to security than the fact that PHP has a system() command.  Both are
# unavailable unless specifically enabled by the administrator.
#
# $security can contain one of these strings:
#    FULL-ARGS - use escapeshellarg() on every arg
#    SPACE-ARGS- put quotes around args with spaces
#    SEMI-ARGS - put quotes around args with spaces or | or > or < characters
#    FULL-LINE - use escapeshellcommand() on the whole command
function WikiShSystemCall($pagename, $opt, $dir, $cmd, $args, $security = "SEMI-ARGS", $SafeVars = array())
{
    global $WikiShVars;
#echo "SystemCall: Entering ($pagename, opt, $dir, $cmd, args, $security, safe<br>\n";
#print_r ($SafeVars);
#echo "<br>\n";
    $func = 'WikiShSystemCall()';
    wdbg(4,"$func: Entering: $cmd " . implode(" ", $args));
    wdbg(1,"$func: PWD=" . getcwd());
    $cmd = array($cmd);
    $rtn = wshExpandVars($pagename, $opt, $cmd, $SafeVars);
    $cmd = $cmd[0];
    #there is no opt processing - must use var=val syntax instead of --var:val
    $rtn = $rtn && wshExpandVars($pagename, $opt, $args, $SafeVars); 
    if ($cmd) $command = $cmd;
    else $command = array_shift($args);
    if (!$rtn) {
        wshStdErr($pagename, $opt, "ERROR: $func: Insecure variable.  System call aborted.");
        return('');
    }
    foreach ($args as $arg) {
        switch ($security) {
        case "FULL-ARGS":
            $command .= ' ' . escapeshellarg($arg);
            break;
        case "SPACE-ARGS":
            if (strstr($arg, ' '))
                $command .= ' ' . escapeshellarg($arg);
            else
                $command .= ' ' . $arg;
            break;
        case "SEMI-ARGS":
            if (preg_match("/[ |><;`(){}]/", $arg))
                $command .= ' ' . escapeshellarg($arg);
            else
                $command .= ' ' . $arg;
            break;
        default:
            $command .= ' ' . $arg;
            break;
        }
        wdbg(3,"$func: Handling arg=$arg - now command = " . wshDbgOd($command));
    }
    $rtn = array(); $status = 0;
    if ("FULL-LINE" == $security)
        $command = escapeshellcmd(str_replace('`', '\"', $command));
    wdbg(4,"$func: Running this commmand >>" . wshDbgOd($command). "<<");
    if ($dir) {
        $odir = getcwd();
        wdbg(3,"$func: chdir'ing to this dir: $dir (from $odir)");
        chdir($dir);
    }
    exec($command, $rtn, $status);
    if ($dir) {
        wdbg(3,"$func: chdir'ing back to this dir: $odir");
        chdir($odir);
    }
    $WikiShVars['STATUS'] = $status;
    return (wshPostProcess($pagename, $opt, $rtn));
}

# wshExpand1Var()
# Expand a single variable
function wshExpand1Var($pagename, $text)
{
    $arrtext = array($text);
    wshExpandVars($pagename, array(), $arrtext);
    return($arrtext[0]);
}

# wshExpandVars()
# Find any pattern '/\${\w+}/ anywhere in the $args list and 
# expand it according to the value found in $WikiShVars[].
# FUTURE: in general, I HAVE to have some way to suppress variable interpolation
#  some equivalent of single- as opposed to double-quotes...
function wshExpandVars($pagename, $opt, &$args, $secure = array())
{
    global $WikiShVars, $MarkupTable, $Version, $RecipeInfo;
    $func="wshExpandVars()";
    $rtn = true;
    wdbg(3,"$func: Entering: ".(is_array($args)?implode(",",$args):$args));
    wdbg(1,"$func: WikiShVars follows"); wdbg(1,$WikiShVars);
    if (!is_array($args)) {
        $singleton = true;
        $args = array($args);
    } else $singleton = false;
    for ($i=0; $i< sizeof($args); $i++) {
        wdbg(3,"$func: Working with " . wshDbgOd($args[$i]));
        $id = '{$var}';
        if (stristr($WikiShVars['PAGEVARS'], 'pre')) {
            $RefPage = (@$opt['refpage'] ? $opt['refpage'] : (@$WikiShVars['REFPAGE'] ? $WikiShVars['REFPAGE'] : $pagename));
            $args[$i] = FmtPageName($args[$i], $RefPage, true);
            wdbg(2,"$func: After FmtPageName: " . wshDbgOd($args[$i]));
        }
        $pat = "/(?<!\\\\)\\$\\{(?:(?P<prefix>[!~])|(?P<strlen>#))?(?P<var>(?:[@*#]|\w+))(?:(?P<prefix_op>[*@])|(?P<delete_op>#{1,2}|%{1,2})(?P<delete>[^}]+)|:(?P<default_op>[-=+])(?P<default>[^}]*)|\\/(?P<search>[^\\/]+)\\/(?P<replace>[^\\}]*)|[:,](?P<offset>[-\\d]+)(?:[:,](?P<length>[-\\d]+))?)?\\}/";
        wdbg(0,"$func: pat=" . wshDbgOd($pat));
        while (preg_match($pat, $args[$i], $m)) {
            wdbg(2,"$func: Found match: 0=$m[0], strlen=$m[strlen], var=$m[var], search=$m[search], replace=$m[replace], offset=$m[offset] length=$m[length] default=$m[default], dflt_op=$m[default_op], delete_op=$m[delete_op], delete=$m[delete], prefix=$m[prefix], prefix_op=$m[prefix_op]");
            $var = $m['var'];
            if ((@$var||$var===0||$var==='0') && isset($WikiShVars[$var])) {
                #if ($secure) { print_r($secure); echo "<br>\n"; }
                if (!$secure[$var] || wshMatchPageName($WikiShVars[$var], $secure[$var])) {
                    $val = $WikiShVars[$var];
                    #echo "TESTING: Expanding $var to $val " . ($secure[$val] ? "(secured)":"(unsecured)"). "<br>\n";
                } else {
                    wshStdErr($pagename, $opt, "ERROR: $func: Insecure value $WikiShVars[$var] for variable \${$var}.");
                    $val = '!!ERROR!!';
                    $rtn = false;
                    #echo "TESTING: NOT Expanding $var to $val " . ($secure[$val] ? "(secured)":"(unsecured)"). "<br>\n";
                }
            } else {
                switch ($var) {
                case 'PWD': // Present Working Directory
                case 'CWD': // Current Working Directory
                    $val = getcwd();
                    break;
                case 'WIKISH_VERSION':
                    $val = $RecipeInfo['WikiSh']['Version'];
                    break;
                case 'PMWIKI_VERSION':
                    $val = $Version;
                    break;
                case 'RANDOM':
                    $val=rand($WikiShVars['RANDOM_MIN'], $WikiShVars['RANDOM_MAX']);
                    break;
                case 'SECONDS':
                    $val = microtime(true) - $WikiShVars['SECONDS_START'];
                    break;
                case 'SECONDSLEFT':
                    $val = ini_get('max_execution_time') - (microtime(true) - $WikiShVars['SECONDSLEFT_START']);
                    break;
                case 'NOW':
                    $val = microtime(true);
                    break;
                case 'HOSTIP':
                    $val=gethostbyname($_SERVER["SERVER_NAME"]);
                    break;
                case 'HOSTNAME':
                    $val=gethostbyaddr(gethostbyname($_SERVER["SERVER_NAME"]));
                    break;
                case 'SESSIONID':
                    $val= session_id();
                    break;
                case '*':
                case '@':
                    $val = '';
                    for ($y = 1; isset($WikiShVars[$y]); $y++)
                        $val .= ($val?' ':'') . $WikiShVars[$y];
                    break;
                case 'LPAREN':
                    $val = '(';
                    break;
                case 'RPAREN':
                    $val = ')';
                    break;
                default: // truly an unset variable
                    $val = '';
                    break;
                }
            }
            wdbg(2,"$func: before modifiers: var=$var, val=>>$val<<");
            if ($m['prefix']=='!' && $m['prefix_op']) {
                $val = implode(" ", preg_grep("/^$var/", array_keys($WikiShVars)));
            } elseif ($m['prefix'] == '!') {
                $val = $_REQUEST[$var];
            } elseif ($m['prefix'] == '~') {
                if(!session_id()) session_start();
                $val = $_SESSION[$var];
            }
            if ($m['default']) {
                if ($val) {
                    if ($m['default_op'] == '+')
                        $val = $m['default'];
                } else {
                    if ($m['default_op'] == '-' || $m['default_op'] == '=')
                        $val = $m['default'];
                    if ($m['default_op'] == '=') $WikiShVars[$var] = $val;
                }
            }
            if ($m['search']) {  // ${var/search/replace/}
                $search = wshGlobToPCRE($m['search']);
                wdbg(1,"$func: var searchpat=" . wshDbgOd($search));
                $val = preg_replace("/$search/", $m['replace'], $val);
            } elseif ($m['offset'] !== null) { // ${var,offset,length}
                if ($m['length']!==null) 
                    $val = substr($val, $m['offset'], $m['length']);
                else $val = substr($val, $m['offset']);
            } elseif ($m['delete']) {
                $delete = wshGlobToPCRE($m['delete'], ($m['delete_op'][0] == '#'?true:false), ($m['delete_op'][0] == '%'?true:false), (strlen($m['delete_op'])>1 ? true : false));
                wdbg(2,"$func: var searchpat=" . wshDbgOd($delete));
                # We have to treat the ungreedy strip off the end differently
                # because there's no way to make PCRE ungreedy from a 
                # end-of-string-oriented perspective.  It always starts at 
                # the beginning so we just add a "fake" regex on the front
                if ($m['delete_op'] == '%')
                    $val = preg_replace("/(.*)$delete/", '$1', $val);
                else
                    $val = preg_replace("/$delete/", '', $val);
            }
            if ($m['strlen']) { // ${#var}
                $val = strlen($val);
            }
            wdbg(2,"$func: after modifiers: var=$var, val=$val, WikiShVar=$WikiShVars[$var]");
            $fullpat = preg_quote($m[0], '/');
            wdbg(1,"$func: replacement pattern: " . wshDbgOd($fullpat));
            # If the val contains a $1 pattern it should NOT be substituted
            # in this context, so we escape it.  (crypt'd passwords have this)
            wdbg(2, "$func: before rid of backrefs: ".wshDbgOd($val));
            $val = preg_replace("/(?<!\\\\)(\\$\\{?[0-9])/", '\\\\$1', $val);
            wdbg(2, "$func: got rid of backrefs: ".wshDbgOd($val));
            # Note: I have to use preg_replace because str_replace() doesn't 
            # have the count capability (do only the 1st)
            $args[$i] = preg_replace("/(?<!\\\\)$fullpat/", $val, $args[$i], 1);
            wdbg(3,"$func: Expanded (\$)$m[var] to " . wshDbgOd($val) . ": >>" . wshDbgOd($args[$i]) . "<<");
        }
        # Sometimes a PV might have been formed by interpolating the WikiShVar
        # above - therefore check this one more time...
        if (stristr($WikiShVars['PAGEVARS'], 'post')) {
            wdbg(0,"$func: Before PTV replace: " . wshDbgOd($args[$i]));
            # Handle PTVs
            while (preg_match("/\\{([\\w-.\\/]*)\\$:([\\w 0-9]+)\\}/", $args[$i], $m)) {
                wdbg(1,"$func: Working on PTV $args[$i] (full=$m[0], 1=$m[1], 2=$m[2])");
                $pn = ($m[1] ? $m[1] : $pagename);
                $pn = wshMakePageName($pagename, $opt, $pn);
                $ptv = $m[2];
                $val = PageTextVar($pn, $ptv);
                $args[$i] = str_replace($m[0], $val, $args[$i]);
                wdbg(1,"$func: After PTV replacement: $args[$i]");
            }
            wdbg(0,"$func: Before 2nd FmtPageName: " . wshDbgOd($args[$i]));
            $RefPage = (@$opt['refpage'] ? $opt['refpage'] : (@$WikiShVars['REFPAGE'] ? $WikiShVars['REFPAGE'] : $pagename));
            $args[$i] = FmtPageName($args[$i], $RefPage, true);
            wdbg(2,"$func: After 2nd FmtPageName (ref=".($WikiShVars['REFPAGE']?$WikiShVars['REFPAGE']:$pagename)."): " . wshDbgOd($args[$i]));
        }
    }
    if ($singleton)
        $args = $args[0];
    return($rtn);
}

function wshDbgOd($foo, $override = false)
{
    global $WikiShVars;
    if (!$override) {
        if ($WikiShVars['DEBUGLEVEL'] >= 5) return($foo);
        if (!$WikiShVars['DEBUG_OD']) return($foo);
    }
    $repl = array(':' => 'COLON', ';' => 'SEMI-COLON', '/'=>'SLASH', 
        '(' => 'OPEN-PAREN', ' '=>' _ ', '@' => 'AT', '\\' => 'BACKSLASH',
        CHR(10)=>'LF', CHR(13)=>'CR', ')'=>'CLOSE-PAREN', '.' => 'DOT', 
        '{'=>'OPEN-CURLY', '}'=>'CLOSE-CURLY', '='=>'EQUALS', '|' => 'VERT', 
        '#'=>'POUND', ','=>'COMMA', '$'=>'DOLLAR', '-'=>'DASH', '+'=>'PLUS', 
        '"' => 'DOUBLE-QUOTE', "'" => 'SINGLE-QUOTE', '!' => 'BANG',
        '?' => 'QUESTION', '<' => 'OPEN-ANGLE', '>' => 'CLOSE-ANGLE',
        '*' => 'ASTERISK', '&' => 'AMPERSAND');
    $rtn = '';
    for ($i = 0; $i < strlen($foo); $i++)
        if (in_array($foo[$i], array_keys($repl))) {
            #echo "$foo[$i](" . ord($foo[$i]) . ") => " . $repl[$foo[$i]] . "<br>\n";
            $rtn .= ' ' . $repl[$foo[$i]] . ' ';
        }
        elseif (ord($foo[$i]) < 65 && (ord($foo[$i])<48 || ord($foo[$i])>57))
            $rtn .= 'CHR(' . ord($foo[$i]). ')';
        else
            $rtn .= $foo[$i];
    return($rtn);
}

# wshInitOpts() (SH NOTE: Think "getopts" but doing a little more automatically)
# This function takes an array of arguments.  It processes through these
# looking for those arguments that begin with - or -- or are arguments to
# these options.  When it has gotten to the first non-option argument then
# it exits having set $opt[x] for each of those options.
#
# EXAMPLE: $ValidOpts = 'x:'
# -abc --hello:goodbye -x foo
# $opt['a'] == true
# $opt['b'] == true
# $opt['c'] == true
# $opt['hello'] == "goodbye"
# $opt['x'] == "foo"
#
# $ValidOpts
#   This is a string where each character refers to a possible argument.  If
#   a given character is followed by a colon then that option will be returned
#   with the following argument in the array.
#   SH NOTE: options NOT included in $ValidOpts do NOT cause an error as they
#   would in shell programming.  Thus you can safely pass an empty list here
#   if you want.  The main purpose of $ValidOpts, then is to specify options
#   which require an argument.  I.e., if I have function foo() that can take
#   arguments -a, -b, -c, or -d but only -b and -c take arguments then I can
#   legitimately pass just 'b:c:' OR 'ab:c:d' - they are functionally identical.
# $args
#   This is an array of arguments.  The OPTIONS that we are interested in are
#   always preceded with a hyphen.  (If a leading hyphen is preceded by a back
#   slash then that backslash will be stripped and the argument will NOT be
#   interpreted as an OPTION.
# SH NOTE: This function is similar to getopts in shell, but it automatically
# handles ALL options without being re-invoked.  (I.e., call this function just
# once).  If you have required values or default values just check for those
# in $opt after you have invoked this function and report errors or assign
# default values as appropriate...
function wshInitOpts($pagename, $ValidOpts, &$opt, &$args, $ExpandVars = true, $ProcPipe=true)
{
    global $WikiShVars;
    global $WikiShPipeText, $WikiShPipeActive;
    $func = 'wshInitOpts';
    wdbg(3,"$func: Entering: args array follows:"); wdbg(3,$args);
    if ($ExpandVars) {
        wshExpandVars($pagename, $opt, $args);
    }
    for($i=0; $i <sizeof($args); $i++) {
        $myarg = $args[$i];
        wdbg(0,"$func: myarg=$myarg");
        if ($myarg == '-' || $myarg[0] != '-') { 
            if (substr($myarg, 0, 2) == '\-') $args[0] = substr($args[0], 1);
            break;
        } elseif ($myarg == '--') {
            $i++;
            break;
        } elseif (substr($myarg, 0, 2) == '--' && strlen($myarg) > 2) {
            //arguments preceded by -- set that $opt[val] to true
            //i.e., --foo causes $opt['foo'] = true
            //if set to a particular value it would be --foo:xyz
            $foo = substr($myarg, 2);
            if ($colon_pos = strpos($foo, ':')) {
                $optname = substr($foo, 0, $colon_pos);
                $optval = substr($foo, $colon_pos+1);
                if ($opt[$optname]) {
                    if (!is_array($opt[$optname])) {
                        $opt[$optname] = array($opt[$optname], $optval);
                    } else {
                        $opt[$optname][] = $optval;
                    }
                } else
                    $opt[substr($foo, 0, $colon_pos)] = 
                        substr($foo, $colon_pos+1);
                wdbg(1,"$func: $myarg - set opt[" . substr($foo, 0, $colon_pos) . "] to " . substr($foo, $colon_pos+1));
            } else {
                if (strlen($foo) == 1 && strstr($ValidOpts, $foo.':')) {
                    $opt[$foo] = $args[$i+1];
                    $i++; // skip the next argument since it was arg to this opt
                } else {
                    if (substr($foo, 0, 2) == 'no') {
                        $foo = substr($foo, 2);
                        $opt[$foo] = false;
                        wdbg(1,"$func: $myarg - set opt[$foo] to FALSE");
                    } else {
                        $opt[$foo] = true;
                        wdbg(1,"$func: $myarg - set opt[$foo] to TRUE");
                    }
                }
            }
        } elseif ($myarg[0] == '-') {
            //arguments preceded by - set all following characters to true
            //i.e., -abc causes $opt['a'], $opt['b'], and $opt['c'] to all be 
            //true
            for ($j=1; $j<strlen($myarg); $j++) {
                wdbg(1,"$func: $myarg - setting opt[$myarg[$j]]...");
                $foo = $myarg[$j];
                if (strstr($ValidOpts, $foo.':')) {
                    if ($j+1 < strlen($myarg)) {
                        $optarg = substr($myarg, $j+1);
                        $j = strlen($myarg);
                    } else {
                        $optarg = $args[$i+1];
                        $i++;
                    }
                    wdbg(1,"$func: optarg=$optarg");
                    // If the user has already set this option then use an array
                    if (isset($opt[$foo])) {
                        if (!is_array($opt[$foo])) {
                            wdbg(1,"$func: Converting opt[$foo] to an array (was $opt[$foo]");
                            wdbg(1,"$func: opt[$foo]: BEFORE"); wdbg(1,$opt[$foo]);
                            $a = $opt[$foo];
                            unset($opt[$foo]);
                            $opt[$foo][] = $a;
                            wdbg(1,"$func: opt[$foo]: AFTER"); wdbg(1,$opt[$foo]);
                        }
                        wdbg(1,"$func: Adding " . $optarg . " as array option to opt[$foo]");
                        $opt[$foo][] = $optarg;
                        wdbg(1,"$func: opt[$foo]"); wdbg(1,$opt[$foo]);
                    } else {
                        wdbg(1,"$func: Setting opt[$foo] to $optarg");
                        $opt[$foo] = $optarg;
                    }
                } else {
                    wdbg(1,"$func: $myarg - setting opt[$foo] to true");
                    $opt[$foo] = true;
                }
            }
        }
    }
    wdbg(1,"$func: Opt array="); wdbg(1,$opt);
    # Now look for OUTPUTSPEC or INPUTSPEC as the last element of $args
    # Note that if multiple stdin or stdout are specified only the 1st will 
    # take effect.  Tough beans.
    if (sizeof($args) > 0) {
        wdbg(1,"$func: Looking for OUTSPEC: >>".wshDbgOd(end($args))."<<");
        for ($arg = end($args); $arg !== false && preg_match("/^(?:<|>|&gt;(?:&gt;)?|&lt;).+$/", $arg); $arg = end($args)) {
            $arg = str_replace(array("&gt;", "&lt;"), array(">", "<"), array_pop($args));
            wdbg(1,"$func: IN/OUTSPEC: arg=" . wshDbgOd($arg));
            if ($arg[0] == '>') {
                # >pagename
                # >>pagename
                # >pagename<pattern (_TOP or _BEGIN = magic pattern)
                # >pagename=pattern (_TOP or _BEGIN = magic pattern)
                # >pagename>pattern (_BOTTOM or _END = magic pattern)
                # >pagename#section (write to section (and other # specs))
                # >pagename$::var (deflist type of ptv)
                # >pagename$:var (standard ptv)
                # >pagename$:(var) (hidden ptv)
                # >pagename$var  (hidden ptv)
                $opt['stdout'] = $arg;
                if (wshIsATextFile('', $opt['stdout'], ($arg[1] == '>' ? 2 : 1))) {
                    wdbg(1,"$func: outputspec is a text file");
                    $opt['stdout'] = str_replace(TEXTFILEID, "", $arg);
                    $opt['stdout_type'] = 'text';
                } elseif (wshIsASessionFile('', $opt['stdout'], ($arg[1] == '>' ? 2 : 1)) || wshIsASessionGroup(substr($opt['stdout'], ($arg[1] == '>' ? 2:1)))) {
                    if (wshIsASessionFile('', $opt['stdout']))
                        $opt['stdout'] = str_replace(SESSFILEID, "", $arg);
                    $opt['stdout_type'] = 'session';
                } else
                    wshSDOpt($opt, 'stdout_type', 'wiki');
            } elseif ($arg[0] == '<') {
                # <pagename
                # <pagename#section
                $opt['stdin'] = substr($arg, 1); // strip off the < sign
            }
        }
    }
    if (@$opt['timeout'] > 0) set_time_limit($opt['timeout']);
    if (isset($opt['debug']))
        $WikiShVars['DEBUGLEVEL'] = $opt['debug'];
    // Now $args should hold only unprocessed values...  We'll assume that $i
    // was left pointing at the first non-option argument.
    $args = array_slice($args, $i);
    if ($ProcPipe && $WikiShPipeActive) {
        wdbg(1,"wshInitOpts: Adding pipe text >>" . wshDbgOd($WikiShPipeText) . "<<");
        if (!$opt['xargs']) $args[] = '-';
        $args[] = $WikiShPipeText;
        $WikiShPipeActive = false;
    }
}

# wshExpandWildCards()
# Finds any wildcards and expands them into all matching individual filenames
#   Honors $opt['list'] if specified to strip any undesired filenames
#   Honors $WikiShVars['LIST'] if specified and $opt['list'] not specified
#   Honors $SearchPatterns['default'] if specified and $WikiShVars['LIST'] 
#          and $opt['list'] not specified
#
# Note that wshInitOpts() should be called PRIOR to this function so that all
# option-type arguments are already gone.
# 
# if $args[n] is a single hyphen then $args[n+1] is the contents of a file
# rather than a filename and so we just ignore it.
#
# Note that $opt is currently ignored, but it will probably be important at
# some point in the future and I'd rather not have to adapt every call at that
# point.  This leaves us with a nice, standard call...
#
function wshExpandWildCards($pagename, $opt, &$args, $NoPageID = false, $NoTextID = false, $NoBadID = false, $NoSessID = false)
{
    global $WikiShVars, $SearchPatterns, $EnableWikiShTextRead, $wshAuthText;
    $func = "wshExpandWildCards()";
    wdbg(3,"$func: Entering");
    wdbg(1,$args);
    if ($opt['list'])
        $list = $opt['list'];
    elseif ($WikiShVars['LIST'])
        $list = $WikiShVars['LIST'];
    elseif (isset($SearchPatterns['default']))
        $list = 'default';
    else
        $list = '';
    $grp = PageVar($pagename, '$Group');
    $FileList = array();
    for ($i = 0; $i < sizeof($args); $i++) {
        $myarglist = explode("\n", $args[$i]); // can be newline-separated list
        foreach ($myarglist as $myarg) {
            wdbg(1,"$func: myarg=$myarg");
            if ($myarg == '-') {
                $FileList[] = $args[$i+1];
                $i++; // skip the next argument - we've already handled it
                continue;
            } elseif (wshIsASessionFile('', $myarg) || wshIsASessionGroup($myarg)) {
                if (wshIsASessionFile('', $myarg))
                    $myarg = substr($myarg, strlen(SESSFILEID));
                $orig_arg = $myarg;
                if (strstr($myarg, '#')) {
                    if (preg_match("/(#.*)$/", $myarg, $matches))
                        $mysection = $matches[1];
                    $myarg = MakePageName($pagename, $myarg);
                    wdbg(1,"$func: orig=$orig_arg, myarg=$myarg, section=$mysection");
                } else
                    $mysection = '';
                $prpat = GlobToPCRE(FixGlob($myarg));
                //make list from preg name pattern
                wdbg(3,"$func: session pattern=$prpat[0]");
                if(!session_id()) session_start();
                $ShortList = preg_grep("/$prpat[0]/i", array_keys($_SESSION));
                foreach ($ShortList as $filename) {
                    if ($mysection)
                        $filename .= $mysection;
                    wdbg(3, "$func: session file: $filename");
                    $FileList[] = ((!$NoSessID) ? SESSFILEID : '') . $filename;
                }
            } elseif (wshIsATextFile('', $myarg)) {
                // strip off the initial identifier
                $pat = substr($myarg, strlen(TEXTFILEID));
                // make list from preg name pattern
                $globlist = glob("$pat");
                $ShortList = array();
                foreach ($globlist as $x)
                    if ($EnableWikiShTextRead && slAuthorized($x, $wshAuthText, 'read', true))
                        $ShortList[] = $x;
                if ($ShortList) {
                    wdbg(1,"$func: $myarg ($pat) - translated to these pages");
                    wdbg(1,$ShortList);
                    foreach ($ShortList as $filename) {
                        if (!HasGlob($pat) || slAuthorized($filename, $wshAuthText, 'read', true))
                            $FileList[] = ((!$NoTextID) ? TEXTFILEID : '') . $filename;
                    }
                } else {
                    // Pass it through "unharmed" if no file match.  Better to
                    // show it as an error than to just have it disappear.
                    $FileList[] = $myarg;
                }
                continue;
            } else {
                //OK, we will assume this is a WIKI filename or filename pattern
                //check for group.name pattern
                wdbg(1,"$func: $myarg - assuming file name/pattern");
                if ($z = strpos($myarg, '@')) {
                    $since = substr($myarg, $z);
                    $myarg = substr($myarg, 0, $z);
                } else $since = '';
                $orig_arg = $myarg;
                if (strstr($myarg, '#')) {
                    if (preg_match("/(#.*)$/", $myarg, $matches))
                        $mysection = $matches[1];
                    $myarg = MakePageName($pagename, $myarg);
                    wdbg(1,"$func: orig=$orig_arg, myarg=$myarg, section=$mysection");
                } else
                    $mysection = '';
                if (strstr($myarg,'.')) 
                    $pat = $myarg;
                else $pat = $grp.".".$myarg;
                //make preg pattern from wildcard pattern
                $prpat = GlobToPCRE(FixGlob($pat));
                //make list from preg name pattern
                $ShortList = ListPages("/$prpat[0]/i");
                wdbg(1,"Pages listed by $prpat[0]:");
                wdbg(1,$ShortList);
                if ($ShortList) {
                    wdbg(1,"$func: $myarg ($prpat[0]) - translated to these pages");
                    wdbg(1,$ShortList);
                    if (HasGlob($pat) && $list) {
                        $ShortList = 
                            MatchPageNames($ShortList, $SearchPatterns[$list]);
                        wdbg(1,"$func: After MatchPageNames($list)");
                        wdbg(1,$ShortList);
                    }
                    foreach ($ShortList as $filename) {
                        if ($mysection)
                            $filename .= $mysection;
                        if ($since)
                            $filename .= $since;
                        $FileList[] = ((!$NoPageID) ? WIKIPAGEID : '') . $filename;
                    }
                } else {
                    wdbg(1,"$func: $myarg - unmatched $prpat[0]. Passing on.");
                    // Pass it through "unharmed" if no file match.  Better to
                    // show it as an error than to just have it disappear.
                    $FileList[] = ($NoBadID?'':BADFILEID) . $myarg;
                }
            }
        }
    }
    wdbg(1,"$func: ending with this array of args...");
    wdbg(1,$FileList);
    $args = $FileList;
}

function wshReadPage($pagename, $opt, $filename, $onlyfile=false)
{
    global $EnableWikiShTextRead, $wshAuthText, $wshAuthPage;
    $func = "wshReadPage()";
    wdbg(3,"ShReadpage($pagename, ..., $filename): Entering");
    SDV($EnableWikiShTextRead, false);

    $orig_filename = $filename;
    if (wshIsASessionFile('', $filename) || wshIsASessionGroup($filename)) {
        if (wshIsASessionFile('', $filename))
            $filename = substr($filename, strlen(SESSFILEID));
        wdbg(1,"$func: Reading (session-type) $filename");
        if(!session_id()) session_start();
        if (isset($_SESSION[$filename])) {
            wdbg(1,"$func: SESSION[$filename] exists");
            $page = $_SESSION[$filename];
            $page['filename'] = $filename;
            $page['type'] = 'session';
        } else {
            wdbg(1,"$func: SESSION[$filename] does NOT exist");
            #wdbg(1, $_SESSION);
            $page = array('text'=>'', 'filename' => $filename, 'type' => 'bad');
        }
    } elseif (wshIsAWikiPage('', $filename)) {
        wdbg(1,"ShReadpage(): processing WIKI type");
        # If they specified a specific version of page then strip it off...
        if (preg_match('/(^[^@]+)@(\d+)$/', $filename, $m)) {
            $filename = $m[1];
            $since = $m[2];
        } else $since = 0;
        $filename = substr($filename, strlen(WIKIPAGEID));
        $filename = wshMakePageName($pagename, $opt, $filename);
        #if ($filename==$pagename) return(array(''));
        wdbg(1,"wshReadPage(): Reading from $filename");
        if (strstr($filename, '#'))
            $filename = MakePageName($pagename, $filename);
        if (!strpos($filename, '.') && !strpos($filename, '/')) {
            wdbg(1,"wshReadPage: No dot in filename=$filename");
            // Hmmm... They forgot a group name.  That doesn't work.
            $Grp = PageVar($pagename, '$Group');
            $filename = $Grp . '.' . $filename;
            wdbg(1,"wshReadPage: Altered filename to $filename (based on $pagename)");
        }
        # Even if they have 'forceread' permission they still need 'read' 
        # permission.  Since we want a special message for this, it is double-
        # checked, once here and once in wshRetrieveAuthPage().
        if (!slAuthorized($filename, $wshAuthPage, 'read')) {
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Read: You do not have \"read\" authorization for page $filename.  See SecLayer configuration");
            $page = array('filename' => $filename, 'type' => 'bad');
        } else
            # If you read this with $since then it loses history and saves
            # that version in the page read cache so no older history can ever
            # be read in this run of PHP
            $page = wshRetrieveAuthPage($filename, 'read', false, $since);
        if ($page === FALSE) {
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Read: You do not have \"read\" authorization for page $filename.  See PmWiki authorizations");
            $page = array('filename' => $filename, 'type' => 'bad');
        }
        # If they specified a version, calculate that
        if ($since) {
            $page['text'] = RestorePage($filename, $page, $junk, 'diff:'.$since.':999');
        }
        wdbg(1,"$func: Checking for section with filename=$orig_filename");
        if (strstr($orig_filename, '#')) {
            wdbg(0,"$func: Looking for sub-section $orig_filename: >>" . $page['text'] . "<<");
            $page['text'] = TextSection($page['text'], $orig_filename);
            wdbg(1,"$func: Got sub-section: >>" . wshDbgOd($page['text']) . "<<");
        }
        $page['filename'] = $filename;
        $page['type'] = 'wiki';
        wdbg(1,"ShReadpage(): Read text=$page[text]");
    } elseif (wshIsATextFile('', $filename)) {
        $filename = substr($filename, strlen(TEXTFILEID));
        wdbg(1,"wshReadPage(): Reading TEXT from $filename");
        if ($EnableWikiShTextRead && slAuthorized($filename, $wshAuthText, 'read', true)) {
            clearstatcache(); // otherwise filesize() might get confused
            if (file_exists($filename) && ($fh = fopen($filename, 'r'))) {
                $page['text'] = fread($fh, filesize($filename));
                wdbg(1,"wshReadPage(): text=>>$page[text]<<");
                fclose($fh);
                $page['filename'] = $filename;
                $page['type'] = 'text';
                $page['ctime'] = $Now;
                $page['time'] = $Now;
                $page['title'] = '$filename';
            } else {
                $page['text'] = '';
                $page['filename'] = $filename;
                $page['type'] = 'bad';
            }
        } else {
            if (!$EnableWikiShTextRead) 
                $msg = 'Text Reading not enabled.  See $EnableWikiShTextRead configuration';
            else
                $msg = "You do not have \"read\" authorization for text file $filename.  See SecLayer configuration";
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Read: Unable to read from text file.  $msg");
        }
    } elseif (wshIsABadFile('', $filename)) {
        $filename = substr($filename, strlen(BADFILEID));
        wdbg(1,"wshReadPage(): Unable to read TEXT from BAD $filename");
        wshStdErr($pagename, $opt, "ERROR: WikiSh.Read: Unable to read $filename.  Does not exist.");
        $page = array('filename' => $filename, 'type' => 'bad');
    } else {
        // Actual contents of file is contained here due to nesting of MX
        // Create an approximation of a $page
        $page = array();
        $page['filename'] = '-';
        $page['ctime'] = $Now;
        $page['time'] = $Now;
        $page['title'] = '-';
        $page['type'] = 'inline';
        if ($onlyfile) {
            wshStdErr($pagename, $opt, "ERROR: wshReadPage: page $filename does not exist.");
            wdbg(1,"ShReadpage(): nonexistent file. returning nothing.");
            $page['text'] = '';
        } else {
            wdbg(1,"ShReadpage(): processing NESTED MX type");
            $page['text'] = $filename;
        }
    }
    if ($opt['decrypt'] && function_exists('WikiShDecrypt'))
        $page['text'] = WikiShDecrypt($pagename, $opt, $page['text']);
    return($page);
}

# wshWrite
#   pagename - just for reference purposes, to get the group name, etc.
#   filename - the name to write out
#   type     - the type (wiki, text, or inline (or session))
#   newtext  - the text of the file/page to be written (OR overloaded with 
#              secondary definition as the page array)
#   page     - an existing $page array to base the new one off of (may be false)
#   auth     - what authorization is needed to do this write 
#              (possibilities = insert,append,prepend,overwrite)
function wshWrite($pagename, $opt, $filename, $type, $newtext, $page, $slauth='overwrite', $pmauth='edit')
{
    global $EnableWikiShTextWrite, $EnableWikiShWritePage, $WikiShWriting, 
        $EnableWikiShCreatePage, $EnableWikiShOverwritePage, $Author, 
        $WikiShVars, $wshAuthPage, $wshAuthText;
    $func = 'wshWrite';
    #echo "wshWrite: Entering with filename = $filename<br>\n";
    #global $EditFunctions;

#for ($i=0; $i < sizeof($EditFunctions); $i++)
    wdbg(2,"wshWrite($filename): Entering");
    wdbg(1,"$func: newtext=$newtext");
    wdbg(1,"$func: oldtext=$page[text]");
    wdbg(1,"$func: isarray(page)=".(is_array($page)?"YES":"NO"));

    if (wshIsATextFile('', $filename)) {
        $filename = substr($filename, strlen(TEXTFILEID));
        if ($type != 'text') {
            wshStdErr($pagename, $opt, "ERROR: Write to $filename: file ID (TEXTFILEID) and type ($type) do not match. Attempting to continue.");
        }
    }
    if (wshIsAWikiPage('', $filename)) {
        $filename = substr($filename, strlen(WIKIPAGEID));
        if ($type != 'wiki') {
            wshStdErr($pagename, $opt, "ERROR: Write to $filename: file ID (WIKIPAGEID) and type ($type) do not match. Attempting to continue.");
        }
    }
    wdbg(2,"wshWrite: entering: filename=$filename, type=$type");
    if ($filename == "/dev/null" || $filename == "null" || $filename == "nul") {
        return (true); // this is not an error condition, just a statement
                       // that we don't want to do anything...
    }
    $section = ''; $ptvname = '';
    if (($pos=strpos($filename, '#')) !== FALSE) {
        $section = substr($filename, $pos);
        $filename = substr($filename, 0, $pos);
    } elseif (preg_match("/(?P<file>[\w.\/]+)\\$(?P<colon>[.:]*)?(?P<paren1>\()?(?P<varname>[-\w]+)(?P<paren2>\))?/", $filename, $m)) {
        $filename = $m['file'];
        $ptvname = $m['varname'];
        if ($m['colon'] == '::') 
            $ptvtype = 'deflist';
        elseif ($m['paren1'] || $m['colon'] == '.' || !$m['colon']) 
            $ptvtype = 'hidden';
        else 
            $ptvtype = 'text';
    }
    wdbg(1,"$func: filename=$filename");
    if (wshIsASessionFile('', $filename) || wshIsASessionGroup($filename)) {
        if (wshIsASessionFile('', $filename))
            $filename = substr($filename, strlen(SESSFILEID));
        if (is_array($newtext)) $newpage = $newtext;
        else {
            $newpage = $page;
            $newpage['text'] = $newtext;
            $newpage['csum'] = "WikiSh automatic edit";
            $newpage['filename'] = $filename;
            $newpage['type'] = 'session';
        }
        if(!session_id()) session_start();
        $_SESSION[$filename] = $newpage;
    } elseif (wshIsAWikiPage(array('type'=>$type))) {
        if (!$EnableWikiShWritePage) {
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: Unable to write to wiki pages.  Not enabled.");
            return (false);
        }
        $filename = wshMakePageName($pagename, $opt, $filename);
        wdbg(1,"wshWrite(): Writing out wikifile $filename");
        $page_exists = PageExists($filename);
        if (!is_array($page)) {
            if ($page_exists) {
                if (!($page = wshRetrieveAuthPage($filename, $pmauth, false))) {
                    wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: wshRetrieveAuthPage($filename, '$pmauth', ...) failed.  Might there be a problem with page authorization?");
                    return(false);
                }
            } else {
                $page = array();
            }
        }
        if ($section) {
            if (!defined('toolbox')) {
                wshStdErr($pagename, $opt, "ERROR: Redirecting to a section depends on installation of toolbox.php");
                $WikiShVars['STATUS'] = 4;
                return(false);
            }
            list($a, $b, $c) = tbTextSection($page['text'], $section);
            wdbg(1,"$func: section=$section");
            wdbg(1,"$func: a=$a");
            wdbg(1,"$func: b=$b");
            wdbg(1,"$func: c=$c");
            if (is_array($newtext)) $newtext['text'] = $a.$newtext['text'].$c;
            else $newtext = $a.$newtext.$c;
        } elseif ($ptvname) {
            if (!defined('toolbox')) {
                wshStdErr($pagename, $opt, "ERROR: Redirecting to PTVs depends on installation of toolbox.php");
                $WikiShVars['STATUS'] = 4;
                return(false);
            }
            wdbg(1,"$func: filename=$filename");
            wdbg(1,"$func: ptvname=$ptvname");
            wdbg(1,"$func: ptvtype=$ptvtype");
            if (is_array($newtext)) 
                $newtext['text'] = ptv2text($page['text'], $ptvname, 
                    $newtext['text'], $ptvtype);
            else $newtext = ptv2text($page['text'], $ptvname, 
                $newtext, $ptvtype);
        }
        if (is_array($newtext)) $newpage = $newtext;
        else {
            $newpage = $page;
            $newpage['text'] = $newtext;
            $newpage['csum'] = "WikiSh automatic edit";
        }
        wdbg(1,"$func: text being written follows");
        wdbg(1,$newpage['text']);
        if (!$EnableWikiShCreatePage && !$page_exists) {
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: Unable to create pages.  Not enabled.");
            return(false);
        }
        if (!$EnableWikiShOverwritePage && $page_exists) {
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: Unable to overwrite pages.  Not enabled.");
            return(false);
        }
        # Check for SecLayer permissions according to the $slauth passed in
        if ($slauth != 'create' && $slauth != 'attr' && !$page_exists) 
            $slauth = 'create';
        if (!slAuthorized($filename, $wshAuthPage, $slauth)) {
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: You do not have \"$slauth\" authorization on the page \"$filename\".");
            return(false);
        }
        if (!CondAuth($filename, $pmauth) && !slAuthorized($filename, $wshAuthPage, 'forceedit')) {
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: Unable to write to $filename.  No pmwiki authorization for '$pmauth'.");
            return(false);
        }
        #echo "wshWrite: Writing to $filename<br>\n";
        if (!isset($Author) || empty($Author)) 
            $Author = $WikiShVars['AUTHOR'];
        wdbg(1,"$func: calling UpdatePage()");
        if ($WikiShWriting) {
            wdbg(1,"$func: File Write during recursive call of UpdatePage() SUPPRESSED.");
        } else {
            $WikiShWriting = true;
            if (!UpdatePage($filename, $page, $newpage)) {
                $WikiShWriting = false;
                wshStdErr($pagename, $opt, "ERROR: $func: Failure writing to $filename");
                return(false);
            }
            $WikiShWriting = false;
        }
        wdbg(1,"$func: UpdatePage() has come back...");
    } elseif (wshIsATextFile(array('type'=>$type))) {
        if ($slauth != 'create' && !file_exists($filename)) $slauth = 'create';
        if ($EnableWikiShTextWrite && slAuthorized($filename, $wshAuthText, $slauth, true)) {
            wdbg(1,"wshWrite(): Trying to write out textfile $filename");
            if ($fp = @fopen("$filename", "w")) { 
                wdbg(1,"wshWrite(): Writing out textfile $filename");
                if (is_array($newtext)) $newtext = $newtext['text'];
                $fwstat = fwrite($fp, $newtext);
                fclose($fp); 
                if ($fwstat !== false)
                    return(true);
                else {
                    wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: Unable to fwrite to $filename.");
                    return(false);
                }
            } else {
                wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: Unable to open $filename.");
                return(false);
            }
        } else {
            wdbg(1, "$func: No write to $filename");
            if (!$EnableWikiShTextWrite) 
                $msg = 'Text Writing not enabled.  See $EnableWikiShTextWrite configuration';
            else
                $msg = "You do not have \"$slauth\" authorization for text file \"$filename\".  See SecLayer configuration.";
            wshStdErr($pagename, $opt, "ERROR: WikiSh.Write: Unable to write to text file.  $msg");
        }
    } else {
        wshStdErr($pagename, $opt, "ERROR: wshWrite: Unknown file type=$type");
    }
    wdbg(1,"$func: returning true");
    return(true);
}

# This function takes an array of strings representing all the lines that 
# should be returned from this MXP, makes sure there is a double-backslash
# at the end of each of them, and then implodes them all with \n and returns
# that value.
# NOTE that wshPostProcess WILL change the exit status if it is being asked to
# write to a file and it is unsuccessful in that write...
function wshPostProcess($pagename, $opt, $outrows, $page = array('type' => 'inline', 'text' => '', 'filename' => '-'))
{
    global $WikiShVars;
    $func = 'wshPostProcess';
    #for ($i = 0; $i < count($outrows); $i++) {
        #$outrows[$i] = "$i: $outrows[$i]";
        #if (substr($outrows[$i], -2) != '\\\\') $outrows[$i] .= '\\\\';
    #}
    #wdbg(1,implode("\n", $outrows));
    wdbg(2,"$func: Entering");
    wdbg(1,"$func: opt[stdout]=$opt[stdout]");
    $nl = ((isset($opt['newline'])) ? $opt['newline'] : "\n");
    if ($outrows)
        $final = implode($nl, $outrows);
    else $outrows = '';
    if (isset($opt['markup'])) {
        $final = str_replace(
            array('{',     '}',     '(',    ')',    '<',        '>',
                  '[',     ']',     "'"), 
            array('&#123;','&#125;','&#40;','&#41;','&lsaquo;', '&rsaquo;',
                  '&#91',  '&#93',  '&#39'), 
            $final);
    }
    $rtn = $final;
    wdbg(1,"$func: stdout=$opt[stdout] stdout_type=$opt[stdout_type]");
    if (isset($opt['stdout'])) {
        $auth = 'overwrite'; //default unless overridden
        if (!isset($opt['stdout_type'])) {
            if (wshIsATextFile('', $opt['stdout']))
                $opt['stdout_type'] = 'text';
            elseif (wshIsASessionFile('', $opt['stdout']) || wshIsASessionGroup($opt['stdout']))
                $opt['stdout_type'] = 'session';
            else
                $opt['stdout_type'] = 'wiki';
        }
        if (isset($opt['stdout_op']) && isset($opt['stdout_loc'])) {
            $stdout     = $opt['stdout'];
            $stdout_op  = $opt['stdout_op'];
            $stdout_loc = $opt['stdout_loc'];
        } elseif (preg_match("/^>>([-\w.\/]+)$/", $opt['stdout'], $match)) {
            wdbg(1, "$func: >> shortcut match");
            $stdout     = $match[1];
            if (wshIsATextFile('', $stdout)) 
                $stdout = substr($stdout, strlen(TEXTFILEID));
            if (wshIsASessionFile('', $stdout)) 
                $stdout = substr($stdout, strlen(SESSFILEID));
            $stdout_op  = '>';
            $stdout_loc = '_END';
        } elseif (preg_match("/^>([-\w.\/]+)(?:([<=>])(.+))?$/", $opt['stdout'], $match)) {
            wdbg(1, "$func: typical stdout match");
            $stdout     = $match[1];
            $stdout_op  = $match[2];
            $stdout_loc = $match[3];
        } else
            $stdout = $opt['stdout'];
        wdbg(1,"$func: OUTPUTSPEC=" . htmlspecialchars($opt['stdout']) . ", stdout=$stdout, loc=" . htmlspecialchars($stdout_loc) . ", op=" . htmlspecialchars($stdout_op));
        if ($stdout_op) {
            wdbg(1,"$func: Inserting ($stdout_op-$stdout_loc) somewhere into stdout=".$opt['stdout']. " (stdout_type=$opt[stdout_type])");
            if (!wshIsATextFile('', $stdout) && !wshIsASessionFile('', $stdout) &&
                !wshIsAWikiPage('', $stdout)) {
                if ($opt['stdout_type'] == 'text')
                    $tmp_stdout = TEXTFILEID . $stdout;
                elseif ($opt['stdout_type'] == 'session')
                    $tmp_stdout = SESSFILEID . $stdout;
                else
                    $tmp_stdout = WIKIPAGEID . $stdout;
            }
            wdbg(0,"$func: tmp_stdout=$tmp_stdout");
            $oldpage = wshReadPage($pagename, $opt, $tmp_stdout);
            #$page = $oldpage; // NOTE THIS IS UNTESTED - HOPING TO FIX THE HISTORY LOSS PROBLEM
            #echo "ShPostProc: Reading<br>\n";
            wdbg(1,"$func: Before (prepend/append/insert) text=$oldpage[text]");
            if ($oldpage['text']) {
                if ($stdout_op == '>' && ($stdout_loc == '_END' || $stdout_loc == '_BOTTOM')) {
                    wdbg(1, "$func: loc=$stdout_loc: optimized _END match");
                    $auth = 'append';
                    $final = $oldpage['text'] . "\n" . $final;
                } elseif ($stdout_op == '<' && ($stdout_loc == '_TOP' || $stdout_loc == '_BEGIN')) {
                    wdbg(1, "$func: loc=$stdout_loc: optimized _TOP match");
                    $auth = 'prepend';
                    $final = $final . "\n" . $oldpage['text'];
                } elseif ($stdout_op == '=') {
                    wdbg(1, "$func: loc=$stdout_loc, replacing");
                    if (preg_match('/^([^a-zA-Z0-9\\\\])(?:[^\1]|\\\1)*\1[^e\1]*$/', $stdout_loc)) {
                        $final = preg_replace($stdout_loc, $final, $oldpage['text']);
                        wdbg(1,"oldpage=$oldpage[text]");
                        wdbg(1,"final=$final");
                    } else
                        wshStdErr($pagename, $opt, "ERROR: Location on stdout mis-formed.  \"$stdout_loc\" is illegal. (must be surrounded with legal delimiters and no e modifier)");
                } else {
                    $auth = 'insert';
                    $rows = explode("\n", $oldpage['text']);
                    if ($stdout_loc == '_TOP' || $stdout_loc == '_BEGIN') {
                        wdbg(1, "$func: loc=$stdout_loc: _TOP match");
                        $rowloc = 0;
                    } elseif ($stdout_loc == '_END' || $stdout_loc == '_BOTTOM') {
                        wdbg(1, "$func: loc=$stdout_loc: _END match");
                        $rowloc = sizeof($rows);
                    } else {
                        for ($i = 0; $i < sizeof($rows); $i++) {
                            $foundrow = false;
                            if (preg_match("/$stdout_loc/", $rows[$i])) {
                                $rowloc = $i;
                                $foundrow = true;
                                wdbg(1, "$func: loc=$stdout_loc: found pat on line $i ($rows[$i])");
                                break;
                            }
                        }
                        if (!$foundrow) {
                            if ($stdout_op == '<')
                                $rowloc = 0;
                            else
                                $rowloc = sizeof($rows);
                            wdbg(1, "$func: no found loc=$stdout_loc: defaulting to line $rowloc");
                        }
                    }
                    if ($stdout_op == '>') $rowloc++;
                    if ($rowloc > 0)
                        $final = implode("\n", array_slice($rows, 0, $rowloc)) . "\n" . $final;
                    if ($rowloc < sizeof($rows))
                        $final .= "\n" . implode("\n", array_slice($rows, $rowloc));
                }
            }
            wdbg(1,"$func: After (prepend/append/insert) text=$final");
        }
        if ($opt['encrypt'] && function_exists('WikiShEncrypt'))
            $final = WikiShEncrypt($pagename, $opt, $final);
        wdbg(2,"$func: writing out to stdout=$stdout");
        #echo "$func: writing out to stdout=$stdout<br>\n";
        #echo "ShPostProc: Writing<br>\n";
        if (!wshWrite($pagename, $opt, $stdout, $opt['stdout_type'], $final, $page, $auth)) {
            $WikiShVars['STATUS'] = 2;
            return('');
        }
        if (!@$opt['tee']) {
            wdbg(2,"$func: Emptying output - no tee.");
            $final = '';
            $rtn = '';
        }
    }
    if ($opt['display'] || $opt['html'])
        $rtn = PVSE($rtn);
    wdbg(1,"$func:($stdout) returning >>" . wshDbgOd($rtn) . "<<");
    return ($rtn);
}

# SDOpt() = set default option
function wshSDOpt(&$opt, $key, $val)
{
    if (!isset($opt[$key])) $opt[$key] = $val;
}

# wshExtractLines
# This is the central function called for Head(), Tail(), Sed(), etc.
function wshExtractLines($pagename, $opt, $filelist, $func)
{
    wdbg(3,"wshExtractLines(): Entering");
    $func .= '-EL';
    wshExpandWildCards($pagename, $opt, $filelist, false, false, false);
    wshSDOpt($opt, 'file_prefix', '');
    wshSDOpt($opt, 'line_prefix', '');
    wshSDOpt($opt, 'line_suffix', '');
    wshSDOpt($opt, 'inplaceedit', false);
    wshSDOpt($opt, 'printall', false); // Only sed would default this false
    # Some chars (notably slashes) need to be escaped via backslash to make it
    # through the search/replace pattern in Sed().  However, we don't want to
    # leave those backslashes in, so this chunk gets rid of escapes.
    if ($opt['repl']) {
        foreach ($opt['repl'] as $k => $v) {
            wdbg(0,"$func: repl before: " . wshDbgOd($v));
            $opt['repl'][$k] = preg_replace("/\\\\(.)/", "$1", $v);
            wdbg(0,"$func: repl after: " . wshDbgOd($opt['repl'][$k]));
        }
    }
    wdbg(1,"opt[startline]:"); wdbg(1,$opt['startline']);
    wdbg(1,"opt[endline]:"); wdbg(1,$opt['endline']);
    wdbg(1,"opt[find]:"); wdbg(1,$opt['find']);
    wdbg(1,"opt[repl]:"); wdbg(1,$opt['repl']);
    wdbg(1,"opt[replcnt]:"); wdbg(1,$opt['replcnt']);
    wdbg(1,"opt[flag]:"); wdbg(1,$opt['flag']);
    $newrows = array(); $postrows = array();
    foreach ($filelist as $filename) {
        $page = wshReadPage($pagename, $opt, $filename);
        if (wshIsABadFile($page)) {
            wshStdErr($pagename, $opt, "ERROR: $func: No such page: $page[filename]");
            continue;
        }
        $textrows = explode("\n", $page['text']);
        wdbg(0,"wshExtractLines: filename=" . $page['filename'] . " textrows: ");
        wdbg(0,$textrows);
        $file_prefix_printed = false;
        $line_prefix = wshReplace($opt, $page, $opt['line_prefix']);
        $line_suffix = wshReplace($opt, $page, $opt['line_suffix']);
        $startlines = $opt['startline'];
        $endlines = $opt['endline'];
        // handle some specific situations for line numbers:
        //   negative numbers should become offset from end of file
        //   $ should become last line of file
        for ($i=0; $i<sizeof($startlines); $i++) {
            if ($startlines[$i] < 0) $startlines[$i] += sizeof($textrows) + 1;
            if ($startlines[$i] == '$') $startlines[$i] = sizeof($textrows);
            if ($endlines[$i] < 0) $endlines[$i] += sizeof($textrows);
            if ($endlines[$i] == '$') $endlines[$i] = sizeof($textrows);
            $matchstatus[$i] = 0;
        }
        for ($i=0; $i<sizeof($textrows); $i++) {
            if (wshLineShouldPrint($opt, $startlines, $endlines, $matchstatus, $i+1, $textrows[$i])) {
                if (!$file_prefix_printed && $opt['file_prefix'] != '') {
                    $newrows[] = wshReplace($opt, $page, $opt['file_prefix']);
                    $file_prefix_printed = true;
                }
                if (strstr($opt['line_prefix'], 'LINENO')) $line_prefix = wshReplace($opt, $page, $opt['line_prefix'], $i+1);
                if (strstr($opt['line_suffix'], 'LINENO')) $line_suffix = wshReplace($opt, $page, $opt['line_suffix'], $i+1);
                if (@$opt['find']) {
                    for ($j=0; $j<sizeof($opt['find']); $j++) {
                        if (strstr(@$opt['flag'][$j], 'F')) {
                            if (strstr(@$opt['flag'][$j], 'i'))
                                $textrows[$i] = str_ireplace($opt['find'][$j], $opt['repl'][$j], $textrows[$i]);
                            else
                                $textrows[$i] = str_replace($opt['find'][$j], $opt['repl'][$j], $textrows[$i]);
                        } else
                            $textrows[$i] = preg_replace($opt['find'][$j], $opt['repl'][$j], $textrows[$i], $opt['replcnt'][$j]);
                    }
                }
                $newrows[] = $line_prefix . $textrows[$i] . $line_suffix;
            }
        }
        if ($opt['inplaceedit']) {
            $newtext = implode("\n", $newrows);
            # Don't write anything unless there's been a change...
            if ($newtext != $page['text']) {
                if ($opt['v']) $postrows[] = 'Updated: ' . $page['filename'];
                if (!wshWrite($pagename, $opt, $page['filename'], $page['type'], $newtext, $page, 'overwrite')) {
                    $WikiShVars['STATUS'] = 2;
                    return('');
                }
            }
            $newrows = array(); // start over again...
        }
    }
    if ($opt['inplaceedit']) $newrows = $postrows;
    return (wshPostProcess($pagename, $opt, $newrows, $page));
}

function wshIsASessionGroup($pagename)
{
    global $WikiShSessionPagePat;
    foreach ($WikiShSessionPagePat as $pat) {
        if (preg_match($pat, $pagename))
            return true;
    }
    return false;
}
function wshIsASessionFile($page, $filename='', $offset = 0)
{
    if ($page)
        return ($page['type'] == 'session');
    else
        return (substr($filename, $offset, strlen(SESSFILEID))==SESSFILEID);
}
function wshIsAWikiPage($page, $filename='')
{
    if ($page)
        return ($page['type'] == 'wiki');
    else
        return (strncmp($filename, WIKIPAGEID, strlen(WIKIPAGEID))==0);
}
function wshIsATextFile($page, $filename='', $offset = 0)
{
    if ($page)
        return ($page['type'] == 'text');
    else
        return (substr($filename, $offset, strlen(TEXTFILEID))==TEXTFILEID);
}
function wshIsABadFile($page, $filename='')
{
    if ($page)
        return ($page['type'] == 'bad'); // not used anywhere - just consistency
    else
        return (strncmp($filename, BADFILEID, strlen(BADFILEID))==0);
}
function HasGlob($pat)
{
    return (preg_match('/[][*?]/', $pat));
}

// x,y             $matchstatus never reset after y
// /regex/,y       $matchstatus never reset after y
// x,/regex/       $matchstatus never reset after closing regex
// /regex/,/regex/ $matchstatus can be reset after closing regex
// $matchstatus: 0=pre-start, 1=post-start and matching, 2=post-end and no more
function wshLineShouldPrint($opt, $start, $end, &$matchstatus, $curlineno, $line)
{
    wdbg(0,"LineShdPrint(): Entering: curlineno=$curlineno, line=$line");
    if ($opt['printall']) return true; // probably will only happen from sed...
    wdbg(0,"LineShdPrint: start:");
    wdbg(0,$start);
    wdbg(0,"LineShdPrint: end:");
    wdbg(0,$end);
    wdbg(0,"LineShdPrint: MatchStatus:");
    wdbg(0,$MatchStatus);
    $matched = false;
    for ($i = 0; $i < sizeof($start); $i++) {
        // 2 ways to match the beginning:
        //    - numeric and current number falls after
        //    - regex matches (in which case we need to set $matchstatus
        if ($matchstatus[$i]==0)
            if ((is_numeric($start[$i]) && $curlineno >= $start[$i]) ||
                (!is_numeric($start[$i]) && preg_match($start[$i], $line)))
                    $matchstatus[$i] = 1;
        if ($matchstatus[$i] == 1) $matched = true;
        // at this point I'd like to break out as an optimization, but I've got
        // to make sure all my $matchstatus values are set...
        if (!is_numeric($end[$i]) && preg_match($end[$i], $line))
            $matchstatus[$i] = 0; // inclusive, just don't go on next line
        if (is_numeric($end[$i]) && $curlineno >= $end[$i])
            $matchstatus[$i] = 2;
    }
    wdbg(0, "LineShdPrint: Returning " . (($matched)?"True":"False"));
    return($matched);
}

#
# wshReplace()
# Given a string, replace certain keywords with values related to the current
# page.
# ARGUMENTS
# $opt - (currently unused, but keeping it in there because probably we'll
#         want it at some point)
# $page - array containing at minimum $page['filename']
# $string - string to be operated on
function wshReplace($opt, $page, $string, $lineno='')
{
    wdbg(0,"wshReplace($string): Entering");
    if ($string == '') return '';
    $find_str = array('PAGENAME',        'PAGELINK');
    $repl_str = array($page['filename'], "[[" . $page['filename'] . "]]");
    if (is_numeric($lineno)) {
        $find_str[] = 'LINENO';
        $repl_str[] = $lineno;
    }
    if (strstr($string, 'PAGETITLE')) {
        $find_str[] = 'PAGETITLE';
        $repl_str[] = PageVar($page['filename'], '$Title');
    }
    $string = str_replace($find_str, $repl_str, $string);
    wdbg(0,"wshReplace($string): Returning");
    return($string);
}

# wshRetrieveAuthPage()
# This function acts as RetrieveAuthPage() except:
#   (1) SecLayer 'read' authorization is required (WikiSh doesn't support the
#       notion of a "write-only" authorization
#   (2) if SecLayer 'forceread' authorization is present on a 'read' 
#       attempt then pmwiki authorization is ignored (i.e., we go directly 
#       to ReadPage().)  Similarly on an 'edit' attempt if 'forceedit' is
#       present in SecLayer then pmwiki 'edit' authorization is not required -
#       we go straight to ReadPage().
function wshRetrieveAuthPage($pagename, $auth, $passprompt=false, $since=0)
{
    global $wshAuthPage;
    $func='wshRetrieveAuthPage($pagename, $auth)';
    $d=1;
    if (!slAuthorized($pagename, $wshAuthPage, 'read')) {
        wdbg($d*1, "$func: No 'read' SecLayer authorization. Returning FALSE.");
        # No wshStdErr() because the calling function should handle it.
        return(false);
    }
    if (($auth == 'read' && slAuthorized($pagename, $wshAuthPage, 'forceread')) || ($auth == 'edit' && slAuthorized($pagename, $wshAuthPage, 'forceedit')))
        return(ReadPage($pagename, $since));
    else
        return(RetrieveAuthPage($pagename, $auth, $passprompt, $since));
}

# wshNotNow
# (1) Pages get processed after clicking "save" on an edit and before the new 
#     page gets loaded.  That messes things up if you are writing to a file or 
#     doing something with "wikish_once" or something, so I suppress that 
#     processing of WikiSh MXes.
# (2) Pages get processed during the UpdatePage().  This has potential to cause
#     an infinite loop if pagea writes to pageb and pageb writes to pagea.  
#     Thus this processing needs to be suppressed as well (technically I could
#     just suppress the writing of files, but I think it'll speed things up if
#     I suppress all WikiSh markup processing).
# Note that since wshNotNow() is called at the start of every wikish MX it is
# a convenient place to handle RC file processing.  Doesn't fit with the name
# of the function, but what's a guy to do?! :-)
function wshNotNow($pagename, $OverrideActive = false)
{
    global $WikiShVars, $WikiShRCPages, $WikiShRCRules, $action, 
        $WikiShWriting, $MXWhileEditing, $wshAuthPage;
    static $RCDone = false;
    $OKActions = array('browse', 'search', 'approvesites');
    $func = 'wshNotNow()';
    wdbg(1,"$func: Entering (action=$action, writing=".($WikiShWriting?"TRUE":"FALSE").")");
    # Since this function is called at the very beginning of nearly every
    # WikiSh MX I go ahead and run the RC commands here.  It would be better
    # to do it in the mainline, but we don't have other necessary 
    # initializations done at that point.
    if (!$RCDone) {
        $RCDone = true; // to prevent never-ending recursion
        $OldDfltDbg = $WikiShVars['DEFAULT_DEBUG'];
        $OldDbg = $WikiShVars['DEBUGLEVEL'];
        $WikiShVars['DEBUGLEVEL'] = $WikiShVars['RC_DEBUG'];
        foreach ($WikiShRCPages as $k => $rcpagename) {
            wdbg(3,"$func: RCPage=$rcpagename (pagename=$pagename)");
            $tmp = wshMakePageName($pagename, array(), FmtPagename($rcpagename, $pagename));
            wdbg(1,"$func:post RCPage=$rcpagename (pagename=$pagename)");
            if (!$tmp || !PageExists($tmp)) {
                wdbg(3,"$func: RCPage=$rcpagename does not exist");
                continue;
            }
            if (!($page = wshRetrieveAuthPage($tmp, 'read', false))) {
                wshStdErr($pagename, array(), "ERROR: $func: no read authorization for $tmp");
                continue;
            }
            if (defined('toolbox'))
                $text = RunMarkupRules($pagename, $WikiShRCRules, $page['text']);
            else
                $text = $page['text'];
            $text = wshParseCode($text);
            MarkupExpression($rcpagename, "(wikish $text)");
        }
        if ($OldDfltDbg == $WikiShVars['DEFAULT_DEBUG'])
            $WikiShVars['DEBUGLEVEL'] = $OldDbg; // back to prior debug level
        else // they explicitly set a new default in RC processing - use it
            $WikiShVars['DEBUGLEVEL'] = $WikiShVars['DEFAULT_DEBUG'];
    }
    if ((!$OverrideActive && !$WikiShVars['ACTIVE']) || (!in_array($action, $OKActions) && !$MXWhileEditing) || ($MXWhileEditing && $action != 'edit') || $WikiShWriting) {
        wdbg(1,"$func: returning true");
        return(true);
    } else {
        wdbg(1,"$func: returning false");
        return(false);
    }
}

function wshMakePageName($pagename, $opt, $tgt)
{

    if (wshIsATextFile('', $tgt)) return($tgt);
    return(MakePageName($pagename, $tgt));
}

# wshUnHTMLSpecialChars()
# This function will undo the most commonly replaced strings when converting
# to HTML special chars
# If $onlyescaped is true then the &lt; has to have a backslash before it in 
# order for it to be replaced - this facilitates sed 's/\</something else/g'
# and just requires you to think of these characters as "magic" characters like
# regex chars (even though they aren't).
function wshUnHTMLSpecialChars(&$str, $onlyescaped=false)
{
    $rplc = array( 
        ($onlyescaped?'\\':'').'&lt;' => '<', 
        ($onlyescaped?'\\':'').'&le;' => '<=', 
        ($onlyescaped?'\\':'').'&gt;' => '>', 
        ($onlyescaped?'\\':'').'&ge;' => '>=',
        ($onlyescaped?'\\':'').'&nbsp;' => ' ', 
        ($onlyescaped?'\\':'').'&amp;' => '&'
    );
    $str = str_replace(array_keys($rplc), array_values($rplc), $str);
    wdbg(1,"unhtml: returning >>" . wshDbgOd($str) . "<<");
    return($str);
}

# wshParseCode
# This function reads code from an external file and converts it into the
# single-line, semi-colon delineated format the wikish so loves...
# Comments will be stripped as long as there is whitespace immediately
# preceding the pound sign (or if the # is at the start of the line).
# Functions will be defined (and stripped) as appropriate.
function wshParseCode($code, $SuppressKeep = false)
{
    global $wshFunctionList, $MarkupExpr;
    global $KeepToken, $KPV;
    $rpat = "/$KeepToken(\\d+P)$KeepToken/e";
    $rrep = '$KPV[\'$1\']';
    $func = "ParseCode";
    wdbg(2, "$func: Entering");
    wdbg(1,"$func: code=>>" . wshDbgOd($code) . "<<");
    #$code = str_replace("\r", "", $code);
    wdbg(1,"$func: after linefeed replace code=>>" . wshDbgOd($code) . "<<");
    $newlines = array();
    if (preg_match_all("/^\\s*(?:function\\s+(?P<fname1>\\w+)\\s*(?:\\(\\))?|(?P<fname2>\\w+)\\s*\\(\\))\\s*\\{(?P<fdef>.*?)^\\}/sim", $code, $m, PREG_SET_ORDER)) {
        foreach ($m as $funcdef) {
            wdbg(1, "$func: fname1=$funcdef[fname1], fname2=$funcdef[fname2], fdef=$funcdef[fdef]");
            $funcname = ($funcdef['fname1']?$funcdef['fname1']:$funcdef['fname2']);
            wdbg(1, "$func: funcname=$funcname");
            $wshFunctionList[$funcname] = 'wikish ' . wshParseCode($funcdef['fdef']);
            $MarkupExpr[$funcname] = "wshDoFunction(\$pagename, '$funcname', \$params, \$exiting)"; 
            $code = str_replace($funcdef[0], "", $code);
        }
    }
    wdbg(1,"wshFunctionList:"); wdbg(1,$wshFunctionList);
    $codelines = explode("\n", $code);
    foreach ($codelines as $line) {
        if ($SuppressKeep) {
            # we've got to get rid of quoted strings so they can contain a #
            # without it being stripped as a comment.
            wdbg(1, "$func: Before dumping quotes: " . wshDbgOd($line));
            $line = preg_replace('/(([\'"]).*?\\2)/e', "Keep(PSS('$1'),'P')", $line);
        } else {
            $line = preg_replace('/([\'"])(.*?)\\1/e', "Keep(PSS('$2'),'P')", $line);
            $line = preg_replace('/\\(\\W/e', "Keep(PSS('$2'),'P')", $line);
        }
        wdbg(1,"$func: After dumping quotes: >>" . wshDbgOd($line) . "<<");
        # Note that the whitespace before the comment is necessary so that
        # we can have page sections specified as MyGroup.MyPage#MySection
        $line = preg_replace("/(?:^|\s)\s*#.*$/", "", $line);
        wdbg(1,"$func: After dumping comments: >>" . wshDbgOd($line) . "<<");
        if ($SuppressKeep) {
            $line = preg_replace($rpat, $rrep, $line);
        }
        wdbg(1,"$func: After replacing quotes: >>" . wshDbgOd($line) . "<<");
        if ($line && !preg_match("/^\s*$/", $line))
            $newlines[] = $line;
    }
    $newcode = implode(";", $newlines);
    $newcode = preg_replace("/;*$/", "", $newcode) . ';';
    wdbg(1,"$func: returning >>" . wshDbgOd($newcode) . "<<");
    return($newcode);
}

# wshStdErr()
# This function handles error messages
# Error messages are routed based on the values of 
#    $opt['stderr']
#      ! stdout   - just put the error messages in with stdout
#        messages - put error messages in $MessagesFmt[] for (:messages:)
#      * echo     - echo error messages directly/immediately via echo
#      ! PAGE/FILE- write to this page or file
#      ! PAGE>PAT - write to page after pattern (_END or _BOTTOM valid)
#      ! PAGE<PAT - write to page before pattern (_BEGIN or _TOP valid)
#  * default
#  ! future - not yet implemented
#
function wshStdErr($pagename, $opt, $text)
{
    global $MessagesFmt;
    SDV($opt['stderr'], 'echo');
    switch ($opt['stderr']) {
    case 'messages':
        if (is_array($text)) {
            $MessagesFmt[] = "<pre>" . print_r($text,true) . "</pre><br>\n";
        } else {
            $MessagesFmt[] = $text . "\n";
        }
        break;
    case 'echo':
        if (is_array($text)) {
            echo "<pre>" . print_r($text,true) . "</pre><br>\n";
        } else {
            echo $text . "<br>\n";
        }
        break;
    case 'nul':
    case 'null':
    case '/dev/null':
        // Do nothing
        break;
    default:
        echo "ERROR: Unhandled stderr=$opt[stderr]<br>\n";
    }
}

if (!function_exists("wdbg")) {
# Printlevel indicates what level of importance this line is.  
#  0=never print
#  1=very detailed debugging
#  2=a little less detailed
#  3=fairly normal debugging
#  4=high-level, print it without thinking about it
#  (you can go higher, but the indentation doesn't work)
function wdbg($printlevel, $text, $txt2 = '', $txt3 = '', $txt4='', $txt5='')
{
    global $MessagesFmt, $WikiShVars, $EnableWikiShDebug;
    #if ($printlevel >= 3) echo "$text (" . (microtime(true) - $WikiShVars['SECONDS_START']) . ")<br>\n";
    if (!@$EnableWikiShDebug || $printlevel<$WikiShVars['DEBUGLEVEL']) return;
    foreach (array($text, $txt2, $txt3) as $txt) {
        if ($txt == '')
            break;
        elseif (is_array($txt)) {
            $MessagesFmt[] = "<pre>" . print_r($txt,true) . "</pre>";
        } else {
            $suffix = "</li></ul>";
            $prefix = '';
            for ($i = 4-$printlevel; $i > 0; $i--) {
                $prefix .= "<dl><dd>";
                $suffix .= "</dd></dl>";
            }
            $prefix .= "<ul><li>";
            $MessagesFmt[] = $prefix . $txt . $suffix . "\n";
        }
        #echo $MessagesFmt[sizeof($MessagesFmt)-1] . "<br>\n";
    }
}
} // if !function_exists()

# Generic markup used for testing...
$MarkupExpr["wikish_devtest"] = 'wshDevTest($pagename, @$argp, @$args)'; 
function wshDevTest($page, $opts, $args)
{
    global $WikiShVars, $InputValues, $FmtV, $foo;
    echo "PRE: " . print_r($opts,true) . "</pre><br>\n";
}

## wshMatchPageName is blatantly copied and slightly modified from 
## MatchPageName() in pmwiki.php.  Credit is thus deserved and duly rendered.
## A page is handed to wshMatchPageName (not a list of pages as before).
## An array of patterns is also passed in.  As with MatchPageNames, the 
## patterns can be specified as follows:
##   Patterns can be either regexes to include ('/'), regexes to exclude ('!'), 
##     or wildcard patterns (all others) (wildcard patterns optionally 
##     preceded by - or ! to indicate that they are to be excluded.
## The difference with wshMatchPageName is that all INCLUSIVE elements in 
## the array will be logically joined with OR instead of AND.  EXCLUSIVE 
## elements are still logically joined to all other conditions with the 
## boolean "AND NOT".
##
## A page MUST match some INCLUSIVE condition and must NOT match any EXCLUSIVE 
## condition if it is to be matched.
##
function wshMatchPageName($page, $pat, $allowslash = false) 
{
    $MatchedInclusive = false;
    foreach((array)$pat as $p) {
        if (!$p) continue;
        switch ($p{0}) {
        case '/': 
            if (preg_match($p, $page))
                $MatchedInclusive = true; 
            continue;
        case '!':
            if (preg_match($p, $page))
                return(false); 
            continue;
        default:
            if ($allowslash)
                list($inclp, $exclp) = GlobToPCRE($p);
            else
                list($inclp, $exclp) = GlobToPCRE(str_replace('/', '.', $p));
            if ($exclp && preg_match("/$exclp/i", $page)) 
                return(false); 
            if ($inclp && preg_match("/$inclp/i", $page))
                $MatchedInclusive = true;
    }
  }
  return $MatchedInclusive;
}

## wshGlobToPCRE is yet another prime example of prudent borrowing.
## All credit to PM -- I needed this function to not automatically assuming
## anchors and it was easier to copy it in and modify it slightly.  I also
## got rid of the - and the ! and the exclude array and etc.
## Basically now it converts a wildcard pattern into a pcre pattern
function wshGlobToPCRE($pat, $anchor_beg=false, $anchor_end=false, $greedy=true)
{
  $pat = preg_quote($pat, '/');
  $pat = str_replace(array('\\*', 
                            '\\?', '\\[', '\\]', '\\^'),
                     array('.*' . ($greedy?'':'?'),  
                            '.',   '[',   ']',   '^'), $pat);
  $incl = array();
  foreach(preg_split('/,+\s?/', $pat, -1, PREG_SPLIT_NO_EMPTY) as $p) {
    $incl[] = ($anchor_beg?'^':'') . "$p" . ($anchor_end?'$':'');
  }
  return (implode('|', $incl));
}

#BUGLIST
# Add in usage messages obtained with -? option on all commands
#
# Not a bug, but I need to update the appropriate page in pmwiki to tell them
# that [@...@] surrounding (:markup:)[= ... =] gets ignored.  That is, the 
# [@ ... @] gets ignored -- the stuff gets processed.
#
# bug: echo abc >Test.Abc#abc 
# does not seem to update history

#ROADMAP (in addition to that on the web)
#
# Document more "pmwiki" type of options (stdout=PAGE instead of >PAGE kind 
# of thing)
#
# --checkpoint:seconds argument to sed and perhaps others (capability 
# exists using read --saveset and --restoreset, but it would be nice to
# avoid having to write a loop when sed can handle it completely...)
#
# -w or -g option for sed to use glob patterns
#
# -e for grep to have multiple patterns
# -f for grep to get patterns (fixed strings?) from file
# -h (--no-filename) and -H (--with-filename) for grep (default -H if 
# multi-args)
# -n for line numbers in grep
#
# ${body/wikibox-password:/wikibox-password:xyz/} ended up putting a / on the
# end of the replaced text...
#
# I've temporarily added the $WikiShVars['MAILXRULES'] but really it should be
# more flexible with a $WikiShRules['a'], ...['b'], etc. that the user can
# then choose different rulesets rather than being tied to a single one...
#
# If you are accessing a temporary/virtual/session file then you shouldn't
# check for CondAuth() -- there could be a site-wide password that you could
# be stepping on when non-pmwiki-pages should not be subject to that kind
# of restriction