// ============================================================ //
//                                                              //
//   File      : gb_aci.cxx                                     //
//   Purpose   : ARB command interpreter (ACI)                  //
//                                                              //
//   http://www.arb-home.de/                                    //
//                                                              //
// ============================================================ //

#include "gb_aci.h"
#include "gb_aci_impl.h"

#include <arb_strbuf.h>
#include <arb_match.h>

using namespace GBL_IMPL;

GBL_command_lookup_table::GBL_command_lookup_table(const GBL_command_definition *table, unsigned size) {
    /*! create table to lookup ACI commands
     * @param table command-table (has to exist as long as GBL_command_lookup_table exists; needs sentinel)
     * @param size  number of commands in 'table' ( = size of 'table' - 1)
     */

    for (unsigned i = 0; i<size; ++i) {
        const GBL_command_definition& cmd = table[i];
        gb_assert(cmd.is_defined());

        defined[cmd.identifier] = cmd.function;
    }
    gb_assert(table[size].is_sentinel());
}

namespace GBL_IMPL {
    static const char *search_matching_dquote(const char *str) {
        int c;
        for (; (c=*str); str++) {
            if (c=='\\') {      // escaped characters
                str++;
                if (!(c=*str)) return NULp;
                continue;
            }
            if (c=='"') return (char *)str;
        }
        return NULp;
    }
    inline char *search_matching_dquote(char *str) {
        return const_cast<char*>(search_matching_dquote(const_cast<const char*>(str)));
    }
    const char *search_matching_parenthesis(const char *source) {
        int c;
        int deep = 0;
        if (*source != '(') deep --;    // first bracket
        for (; (c=*source); source++) {
            if (c=='\\') {      // escaped characters
                source++;
                if (!*source) break;
                continue;
            }
            if (c=='(') deep--;
            else if (c==')') deep++;
            if (!deep) return (char *)source;
            if (c=='"') {       // search the second "
                source = search_matching_dquote(source);
                if (!source) return NULp;
            }
        }
        if (!c) return NULp;
        return source;
    }
    static const char *search_next_separator(const char *source, const char *seps) {
        // search the next separator
        static char tab[256];
        static bool init = false;

        if (!init) {
            memset(tab, 0, 256);
            init = true;
        }

        for (const char *p = seps; *p; ++p) tab[safeCharIndex(*p)] = 1;

        tab['(']  = 2; // exclude () pairs
        tab['"']  = 2; // exclude " pairs
        tab['\\'] = 2; // exclude \-escaped chars

        for (; *source; ++source) {
            const char chType = tab[safeCharIndex(*source)];
            if (chType == 0) continue; // accept char
            if (chType == 1) break;    // found separator

            if (*source == '\\') {
                ++source;              // -> skip over next char
                if (!source[0]) break; // abort if end of string seen
            }
            else if (*source == '(') {
                source = search_matching_parenthesis(source);
                if (!source) break;
            }
            else if (*source == '\"') {
                source = search_matching_dquote(source+1);
                if (!source) break;
            }
        }
        for (const char *p = seps; *p; ++p) tab[safeCharIndex(*p)] = 0; // clear tab
        return source && source[0] ? source : NULp;
    }
    inline char *search_next_separator(char *source, const char *seps) {
        return const_cast<char*>(search_next_separator(const_cast<const char*>(source), seps));
    }
};

static void dumpStreams(const char *name, const GBL_streams& streams) {
    if (streams.empty()) {
        print_trace(GBS_global_string("%s [none]\n", name));
    }
    else {
        int   count  = streams.size();
        char *header = GBS_global_string_copy("%s", name);

        print_trace(GBS_global_string("%s [0]='%s'", header, streams.get(0)));
        if (count>1) {
            LocallyModify<int> inc(traceIndent, traceIndent+strlen(header)+1);
            for (int c = 1; c<count; c++) {
                if (c == 10 || c == 100 || c == 1000) --traceIndent; // dec indentation
                print_trace(GBS_global_string("[%i]='%s'\n", c, streams.get(c)));
            }
        }
        free(header);
    }
}

static const char *shortenLongString(const char *str, size_t wanted_len) {
    // shortens the string 'str' to 'wanted_len' (appends '[..]' if string was shortened)

    const char *result;
    size_t      len = strlen(str);

    gb_assert(wanted_len>4);

    if (len>wanted_len) {
        static char   *shortened_str;
        static size_t  short_len = 0;

        if (short_len >= wanted_len) {
            memcpy(shortened_str, str, wanted_len-4);
        }
        else {
            freeset(shortened_str, ARB_strpartdup(str, str+wanted_len));
            short_len = wanted_len;
        }
        strcpy(shortened_str+wanted_len-4, "[..]");
        result = shortened_str;
    }
    else {
        result = str;
    }
    return result;
}

static char *apply_ACI(const char *str, const char *commands, const GBL_call_env& callEnv) {
    char *buffer = ARB_strdup(commands);

    // ********************** remove all spaces and tabs *******************
    {
        const char *s1;
        char *s2;
        s1 = commands;
        s2 = buffer;
        {
            int c;
            for (; (c = *s1); s1++) {
                if (c=='\\') {
                    *(s2++) = c;
                    if (!(c=*++s1)) { break; }
                    *(s2++) = c;
                    continue;
                }

                if (c=='"') {       // search the second "
                    const char *hp = search_matching_dquote(s1+1);
                    if (!hp) {
                        GB_export_errorf("unbalanced '\"' in '%s'", commands);
                        free(buffer);
                        return NULp;
                    }
                    while (s1 <= hp) *(s2++) = *(s1++); // LOOP_VECTORIZED
                    s1--;
                    continue;
                }
                if (c!=' ' && c!='\t') *(s2++) = c;
            }
        }
        *s2 = 0;
    }

    GBL_streams orig;

    orig.insert(ARB_strdup(str));

    GB_ERROR error = NULp;
    GBL_streams out;
    {
        char *s1, *s2;
        s1 = buffer;
        if (*s1 == '|') s1++;

        // ** loop over all commands **
        for (; s1;  s1 = s2) {
            int separator;
            GBL_COMMAND command;
            s2 = search_next_separator(s1, "|;,");
            if (s2) {
                separator = *(s2);
                *(s2++) = 0;
            }
            else {
                separator = 0;
            }
            // collect the parameters
            GBL_streams argStreams;
            if (*s1 == '"') {           // copy "text" to out
                char *end = search_matching_dquote(s1+1);
                if (!end) {
                    UNCOVERED(); // seems unreachable (balancing is already ensured by search_next_separator)
                    error = "Missing second '\"'";
                    break;
                }
                *end = 0;

                TRACE_ACI(GBS_global_string("copy    '%s'\n", s1+1));
                out.insert(ARB_strdup(s1+1));
            }
            else {
                char *bracket = strchr(s1, '(');
                if (bracket) {      // I got the parameter list
                    *(bracket++) = 0;
                    int slen  = strlen(bracket);
                    if (slen<1 || bracket[slen-1] != ')') {
                        error = "Missing ')'";
                    }
                    else if (slen == 1) {
                        error = "Invalid empty parameter list '()'. To pass an empty argument use '(\"\")'";
                    }
                    else {
                        // go through the parameters
                        char *p1, *p2;
                        bracket[slen-1] = 0;
                        for (p1 = bracket; p1;  p1 = p2) {
                            p2 = search_next_separator(p1, ";,");
                            if (p2) {
                                *(p2++) = 0;
                            }
                            if (p1[0] == '"') { // remove "" pairs
                                int len2;
                                p1++;
                                len2 = strlen(p1)-1;

                                if (p1[len2] != '\"') {
                                    error = GBS_global_string("Invalid parameter syntax for '%s' (needs '\"' at begin AND end of parameter)", p1-1);
                                }
                                else {
                                    p1[len2] = 0;
                                }
                            }
                            argStreams.insert(ARB_strdup(p1));
                        }
                    }
                }
                if (!error && (bracket || *s1)) {
                    command = callEnv.get_env().lookup_command(s1);
                    if (!command) {
                        error = GBS_global_string("Unknown command '%s'", s1);
                    }
                    else {
                        GBL_command_arguments args(callEnv, s1, orig, argStreams, out);

                        TRACE_ACI(GBS_global_string("execute '%s':\n", args.get_cmdName()));
                        {
                            LocallyModify<int> inc(traceIndent, traceIndent+1);
                            if (traceACI) {
                                dumpStreams("ArgStreams", args.get_param_streams());
                                dumpStreams("InpStreams", args.input);
                            }

                            error = command(&args); // execute the command

                            if (!error && traceACI) dumpStreams("OutStreams", args.output);
                        }

                        if (error) {
                            char *dup_error = ARB_strdup(error);

#define MAX_PRINT_LEN 200

                            char *paramlist = NULp;
                            for (int j = 0; j<args.param_count(); ++j) {
                                const char *param       = args.get_param(j);
                                const char *param_short = shortenLongString(param, MAX_PRINT_LEN);

                                if (!paramlist) paramlist = ARB_strdup(param_short);
                                else freeset(paramlist, GBS_global_string_copy("%s,%s", paramlist, param_short));
                            }
                            char *inputstreams = NULp;
                            for (int j = 0; j<args.input.size(); ++j) {
                                const char *input       = args.input.get(j);
                                const char *input_short = shortenLongString(input, MAX_PRINT_LEN);

                                if (!inputstreams) inputstreams = ARB_strdup(input_short);
                                else freeset(inputstreams, GBS_global_string_copy("%s;%s", inputstreams, input_short));
                            }
#undef MAX_PRINT_LEN
                            if (paramlist) {
                                error = GBS_global_string("while applying '%s(%s)'\nto '%s':\n%s", s1, paramlist, inputstreams, dup_error);
                            }
                            else {
                                error = GBS_global_string("while applying '%s'\nto '%s':\n%s", s1, inputstreams, dup_error);
                            }

                            free(inputstreams);
                            free(paramlist);
                            free(dup_error);
                        }
                    }
                }
            }

            if (error) break;

            if (separator == '|') { // out -> in pipe; clear in
                out.swap(orig);
                out.erase();
            }
        }
    }

    {
        char *s1 = out.concatenated();
        free(buffer);
        if (!error) return s1;
        free(s1);
    }

    GB_export_errorf("Command '%s' failed:\nReason: %s", commands, error);
    return NULp;
}
// --------------------------------------------------------------------------------

char *GBL_streams::concatenated() const {
    int count = size();
    if (!count) return ARB_strdup("");
    if (count == 1) return ARB_strdup(get(0));

    GBS_strstruct buf(1000);
    for (int i=0; i<count; i++) {
        const char *s = get(i);
        if (s) buf.cat(s);
    }
    return buf.release_memfriendly();
}

NOT4PERL char *GB_command_interpreter_in_env(const char *str, const char *commands, const GBL_call_env& callEnv) {
    /* simple command interpreter returns NULp on error (which should be exported in that case)
     * if first character is == ':' run string parser
     * if first character is == '/' run regexpr
     * else run ACI
     */

    // @@@   most code here and code in apply_ACI could be moved into GBL_call_env::interpret_subcommand

    LocallyModify<int> localT(traceACI);    // localize effect of command 'trace'
    SmartMallocPtr(char) heapstr;

    if (!str) {
        if (!callEnv.get_item_ref()) {
            GB_export_error("ACI: no input streams found");
            return NULp;
        }

        if (GB_read_type(callEnv.get_item_ref()) == GB_STRING) {
            str = GB_read_char_pntr(callEnv.get_item_ref());
        }
        else {
            char *asstr = GB_read_as_string(callEnv.get_item_ref());
            if (!asstr) {
                GB_export_error("Can't read this DB entry as string");
                return NULp;
            }

            heapstr = asstr;
            str     = &*heapstr;
        }
    }

    if (traceACI) {
        print_trace(GBS_global_string("CI: command '%s' apply to '%s'\n", commands, str));
    }
    modify_trace_indent(+1);

    char *result = NULp;

    if (!commands || !commands[0]) { // empty command -> do not modify string
        result = ARB_strdup(str);
    }
    else if (commands[0] == ':') { // ':' -> string parser
        result = GBS_string_eval_in_env(str, commands+1, callEnv);
    }
    else if (commands[0] == '/') { // regular expression
        GB_ERROR err = NULp;
        result       = GBS_regreplace(str, commands, &err);

        if (!result) {
            if (strcmp(err, "Missing '/' between search and replace string") == 0) {
                // if GBS_regreplace didn't find a third '/' -> silently use GBS_regmatch:
                size_t matchlen;
                err = NULp;

                const char *matched = GBS_regmatch(str, commands, &matchlen, &err);

                if (matched) result   = ARB_strndup(matched, matchlen);
                else if (!err) result = ARB_strdup("");
            }

            if (!result && err) GB_export_error(err);
        }
    }
    else {
        result = apply_ACI(str, commands, callEnv);
    }

    modify_trace_indent(-1);
    if (traceACI) {
        GBS_strstruct final_msg(1000);
        if (result) {
            final_msg.cat("CI: result ='");
            final_msg.cat(result);
        }
        else {
            final_msg.cat("CI: no result. error ='");
            final_msg.cat(GB_get_error());
        }
        final_msg.put('\'');
        final_msg.put('\n');
        final_msg.nput('-', final_msg.get_position()-1);
        final_msg.put('\n');

        print_trace(final_msg.get_data());
    }

    gb_assert(contradicted(result, GB_have_error()));
    return result;
}

char *GB_command_interpreter(const char *str, const char *commands, GBDATA *gb_main) {
    //! @see GB_command_interpreter_in_env - this flavor runs in dummy environment
    GBL_env      env(gb_main, NULp);
    GBL_call_env callEnv(NULp, env);

    return GB_command_interpreter_in_env(str, commands, callEnv);
}

void GBL_custom_command_lookup_table::warn_about_overwritten_commands(const GBL_command_definition *custom_table, unsigned custom_size) const {
    int errcount = 0;
    for (unsigned i = 0; i<custom_size; ++i) {
        const GBL_command_definition& cdef = custom_table[i];
        gb_assert(cdef.is_defined());

        const char *cmd = cdef.identifier;
        if (base_table.lookup(cmd)) {
            fprintf(stderr, "Warning: ACI-command '%s' is substituted w/o permission\n", cmd);
            ++errcount;
        }
    }
    if (errcount>0) {
        fprintf(stderr, "Warning: Detected probably unwanted substitution of %i ACI-commands\n", errcount);
        gb_assert(0); // either use PERMIT_SUBSTITUTION or fix command names
    }
}

// --------------------------------------------------------------------------------

#ifdef UNIT_TESTS
#include <test_unit.h>

#include <arb_defs.h>

#define TEST_CI__INTERNAL(input,cmd,expected,got,TEST_RESULT,callEnv) do {                                      \
        char *result;                                                                                           \
        TEST_EXPECT_RESULT__NOERROREXPORTED(result = GB_command_interpreter_in_env(input, cmd, callEnv));       \
        TEST_RESULT(result,expected,got);                                                                       \
        free(result);                                                                                           \
    } while(0)

#define TEST_CI(input,cmd,expected)                    TEST_CI__INTERNAL(input,    cmd, expected, narg, TEST_EXPECT_EQUAL__IGNARG, callEnv)
#define TEST_CI_WITH_ENV(input,env,cmd,expected)       TEST_CI__INTERNAL(input,    cmd, expected, narg, TEST_EXPECT_EQUAL__IGNARG, env)
#define TEST_CI__BROKEN(input,cmd,expected,regr)       TEST_CI__INTERNAL(input,    cmd, expected, regr, TEST_EXPECT_EQUAL__BROKEN, callEnv)
#define TEST_CI_NOOP(inandout,cmd)                     TEST_CI__INTERNAL(inandout, cmd, inandout, narg, TEST_EXPECT_EQUAL__IGNARG, callEnv)
#define TEST_CI_NOOP__BROKEN(inandout,regr,cmd)        TEST_CI__INTERNAL(inandout, cmd, inandout, regr, TEST_EXPECT_EQUAL__BROKEN, callEnv)

#define TEST_CI_INVERSE(in,cmd,inv_cmd,out) do {        \
        TEST_CI(in,  cmd,     out);                     \
        TEST_CI(out, inv_cmd, in);                      \
    } while(0)

// @@@ rename errorpart_expected -> expected_errorpart

#define TEST_CI_ERROR_CONTAINS(input,cmd,errorpart_expected)                                                            \
    TEST_EXPECT_NORESULT__ERROREXPORTED_CONTAINS(GB_command_interpreter_in_env(input, cmd, callEnv), errorpart_expected)

#define TEST_CI_ERROR_CONTAINS__BROKEN(input,cmd,errorpart_expected,unexpected_result) do{                              \
        char         *result;                                                                                           \
        TEST_EXPECT_NORESULT__ERROREXPORTED_CONTAINS__BROKEN(result = GB_command_interpreter_in_env(input, cmd, callEnv), errorpart_expected); \
        TEST_EXPECT_EQUAL(result, unexpected_result);                                                                   \
        free(result);                                                                                                   \
    }while(0)

static GBDATA     *RCI_gb_main = NULp;
static const char *RCI_input   = NULp;
static const char *RCI_cmd     = NULp;
static GBDATA     *RCI_gbd     = NULp;

inline void run_ci() {
    GBL_env      env(RCI_gb_main, NULp);
    GBL_call_env callEnv(RCI_gbd, env);
    GB_command_interpreter_in_env(RCI_input, RCI_cmd, callEnv);
}

#define TEST_CI_SEGFAULTS(input,cmd) do{        \
        RCI_gb_main = gb_main;                  \
        RCI_input   = input;                    \
        RCI_cmd     = cmd;                      \
        RCI_gbd     = gb_data;                  \
        TEST_EXPECT_SEGFAULT(run_ci);           \
    }while(0)

#define TEST_CI_SEGFAULTS__UNWANTED(input,cmd) do{        \
        RCI_gb_main = gb_main;                            \
        RCI_input   = input;                              \
        RCI_cmd     = cmd;                                \
        RCI_gbd     = gb_data;                            \
        TEST_EXPECT_SEGFAULT__UNWANTED(run_ci);           \
    }while(0)

#define ACI_SPLIT          "|split(\",\",0)"
#define ACI_MERGE          "|merge(\",\")"
#define WITH_SPLITTED(aci) ACI_SPLIT aci ACI_MERGE

static GB_ERROR gbx_custom(GBL_command_arguments *args) {
    EXPECT_NO_PARAM(args);
    for (int i=0; i<args->input.size(); ++i) {
        args->output.insert(strdup("4711"));
    }
    return NULp;
}

class ACI_test_env : virtual Noncopyable {
    GB_shell            shell;
    GBDATA             *gb_main;
    LocallyModify<int>  traceMode;
    GB_transaction      ta;
    GBDATA             *gb_species;
public:
    ACI_test_env() :
        gb_main(GB_open("TEST_aci.arb", "rw")),
        traceMode(traceACI, 0), // set to 1 to trace all ACI tests
        ta(gb_main)
    {
        gb_assert(gb_main);
        gb_species = GBT_find_species(gb_main, "LcbReu40"); // ../UNIT_TESTER/run/TEST_aci.arb@LcbReu40
    }
    ~ACI_test_env() {
        TEST_EXPECT_NO_ERROR(ta.close(NULp));
        GB_close(gb_main);
    }

    GBDATA *gbmain() const { return gb_main; }
    GBDATA *gbspecies() const { return gb_species; }
};

__ATTR__REDUCED_OPTIMIZE__NO_GCSE void TEST_GB_command_interpreter_1a() {
    ACI_test_env E;
    GBL_env      base_env(E.gbmain(), NULp);

    // execute ACI on species container (=GB_DB) in this section ------------------------------
    GBDATA * const gb_data = E.gbspecies();
    GBL_call_env   callEnv(gb_data, base_env);

    TEST_CI_NOOP("bla", "");

    TEST_CI("bla", ":a=u", "blu"); // simple SRT

    // GBS_REGREPLACE_TESTS:
    TEST_CI("bla",    "/a/u/",      "blu");  // simple regExp replace

    TEST_CI("test",   "/_[0-9]+//", "test"); // simple regExp replace (failing, ie. no match -> no replace)

    TEST_CI("blabla", "/l.*b/",     "lab");  // simple regExp match
    TEST_CI("blabla", "/b.b/",      "");     // simple regExp match (failing)

    TEST_CI("tx_01_2", "/_[0-9]+//",  "tx");    // simple regExp replace (replace all occurrences)
    TEST_CI("tx_01_2", "/_[0-9]+$//", "tx_01"); // simple regExp replace (replace one occurrence)

    TEST_CI_ERROR_CONTAINS("xx_____",    "/_*//",   "regular expression '_*' matched an empty string"); // caused a deadlock until [16326]
    TEST_CI("xx_____",   "/_//",                  "xx");      // working removal of multiple '_'
    TEST_CI("xx_____yy", "/(_+)([^_]|$)/-=-\\2/", "xx-=-yy"); // replace multiple consecutive '_'
    TEST_CI("xx_____",   "/(_+)([^_]|$)/-=-\\2/", "xx-=-");   // replace multiple consecutive '_'

    TEST_CI("xx_____", "/_*$//", "xx"); // removal of multiple '_' from end of sequence
    TEST_CI("xx",      "/_*$//", "xx"); // removal of no '_' from end of sequence
    TEST_CI("_____yy", "/^_*//", "yy"); // removal of multiple '_' from start of sequence
    TEST_CI("yy",      "/^_*//", "yy"); // removal of no '_' from start of sequence

    TEST_CI("",    "/^$/ABC/",  "ABC"); // replacing a complete empty string should be possible
    TEST_CI("XXX", "/^XXX$//",  "");    // erase whole known text

    TEST_CI("xx/yy/zz",   "/\\//-/", "xx-yy-zz");   // search expression with an escaped slash
    TEST_CI("xx-yy-zz",   "/-/\\//", "xx/yy/zz");   // reverse (escaped slash in replace expression)

    TEST_CI_ERROR_CONTAINS("xx",    "\\///",   "Unknown command");
    TEST_CI               ("x/x",   "/\\//",   "/");
    TEST_CI_ERROR_CONTAINS("xx",    "//\\/",   "railing backslash"); // [Tt]railing (lib dependent?)

    // escape / quote
    TEST_CI_INVERSE("ac", "|quote",        "|unquote",          "\"ac\"");
    TEST_CI_INVERSE("ac", "|escape",       "|unescape",         "ac");
    TEST_CI_INVERSE("ac", "|escape|quote", "|unquote|unescape", "\"ac\"");
    TEST_CI_INVERSE("ac", "|quote|escape", "|unescape|unquote", "\\\"ac\\\"");

    TEST_CI_INVERSE("a\"b\\c", "|quote",        "|unquote",          "\"a\"b\\c\"");
    TEST_CI_INVERSE("a\"b\\c", "|escape",       "|unescape",         "a\\\"b\\\\c");
    TEST_CI_INVERSE("a\"b\\c", "|escape|quote", "|unquote|unescape", "\"a\\\"b\\\\c\"");
    TEST_CI_INVERSE("a\"b\\c", "|quote|escape", "|unescape|unquote", "\\\"a\\\"b\\\\c\\\"");

    TEST_CI_NOOP("ac", "|unquote");
    TEST_CI_NOOP("\"ac", "|unquote");
    TEST_CI_NOOP("ac\"", "|unquote");

    TEST_CI               ("blabla", "|coUNT(ab)",         "4");                            // simple ACI
    TEST_CI               ("l",      "|\"b\";dd;\"a\"|dd", "bla");                          // ACI with muliple streams
    TEST_CI_ERROR_CONTAINS("bla",    "|count()",           "Invalid empty parameter list"); // no longer interpret '()' as "1 empty arg" (force use of explicit form; see next line)
    TEST_CI               ("bla",    "|count(\"\")",       "0");                            // explicitly empty parameter (still strange, counts 0-byte before string-terminator; always 0)
    TEST_CI               ("b a",    "|count(\" \")",      "1");                            // space in quotes
    TEST_CI               ("b\\a",   "|count(\\a)",        "2");                            // count '\\' and 'a' (ok)
    TEST_CI__BROKEN       ("b\\a",   "|count(\"\\a\")",    "1", "2");                       // should only count 'a' (which is escaped in param)
    TEST_CI               ("b\\a",   "|count(\"\a\")",     "0");                            // does not contain '\a'
    TEST_CI               ("b\a",    "|count(\"\a\")",     "1");                            // counts '\a'

    // escaping (behavior is unexpected or weird and not documented very well)
    TEST_CI("b\\a\a", "|count(\\a)",         "2"); // counts '\\' and 'a' (but not '\a')
    TEST_CI("b\\a",   "|contains(\"\\\\\")", "0"); // searches for 2 backslashes (not in input)
    TEST_CI("b\\a",   "|contains(\"\")",     "0"); // search for empty string never succeeds
    TEST_CI("b\\a",   "|contains(\\)",       "2"); // finds backslash at position 1
    TEST_CI("b\\\\a", "|contains(\"\\\\\")", "2"); // finds two backslashes at position 2

    TEST_CI_ERROR_CONTAINS("b\\a",   "|contains(\"\\\")",   "ARB ERROR: unbalanced '\"' in '|contains(\"\\\")'"); // FIX: raises error (should search for 1 backslash)

    // test binary ops
    {
        // LocallyModify<int> traceHere(traceACI, 1);
        TEST_CI("", "\"5\";\"7\"|minus",                        "-2");
        TEST_CI("", "\"5\"|minus(\"7\")",                       "-2");
        // TEST_CI("", "minus(5,7)",               "-2"); // @@@ this fails (stating command '5' fails). fix how?
        TEST_CI("", "minus(\"\"5\"\",\"\"7\"\")",               "-2"); // @@@ syntax needed here 'minus(""5"", ""7"")' should be documented more in-depth
        TEST_CI("", "minus(\"\"5\";\"2\"|mult\",\"\"7\"\")",    "3"); // (5*2)-7
        TEST_CI("", "minus(\"\"5\";\"2\\,\"|mult\",\"\"7\"\")", "3"); // comma has to be escaped

        TEST_CI_ERROR_CONTAINS("", "minus(\"\"5\";\"2,\"|mult\",\"\"7\"\")", "Invalid parameter syntax for '\"\"5\";\"2'");
    }

    TEST_CI_NOOP("ab,bcb,abac", WITH_SPLITTED(""));
    TEST_CI     ("ab,bcb,abac", WITH_SPLITTED("|len"),                       "2,3,4");
    TEST_CI     ("ab,bcb,abac", WITH_SPLITTED("|count(a)"),                  "1,0,2");
    TEST_CI     ("ab,bcb,abac", WITH_SPLITTED("|minus(len,count(a))"),       "1,3,2");
    TEST_CI     ("ab,bcb,abac", WITH_SPLITTED("|minus(\"\"5\"\",count(a))"), "4,5,3");

    // test other recursive uses of GB_command_interpreter
    TEST_CI("one",   "|dd;\"two\";dd|command(\"dd;\"_\";dd;\"-\"\")",                          "one_one-two_two-one_one-");
    TEST_CI("",      "|sequence|command(\"/^([\\\\.-]*)[A-Z].*/\\\\1/\")|len",                 "9"); // count gaps at start of sequence
    TEST_CI("one",   "|dd;dd|eval(\"\"up\";\"per\"|merge\")",                                  "ONEONE");
    TEST_CI("1,2,3", WITH_SPLITTED("|select(\"\",  \"\"one\"\", \"\"two\"\", \"\"three\"\")"), "one,two,three");
    TEST_CI_ERROR_CONTAINS("1,4", WITH_SPLITTED("|select(\"\",  \"\"one\"\", \"\"two\"\", \"\"three\"\")"), "Illegal param number '4' (allowed [0..3])");

    // test define and do
    TEST_CI("ignored", "define(d4, \"dd;dd;dd;dd\")",        "");
    TEST_CI("ignored", "define(d16, \"do(d4)|do(d4)\")",     "");
    TEST_CI("ignored", "define(d64, \"do(d4)|do(d16)\")",    "");
    TEST_CI("ignored", "define(d4096, \"do(d64)|do(d64)\")", "");

    TEST_CI("x",  "do(d4)",           "xxxx");
    TEST_CI("xy", "do(d4)",           "xyxyxyxy");
    TEST_CI("x",  "do(d16)",          "xxxxxxxxxxxxxxxx");
    TEST_CI("x",  "do(d64)|len",      "64");
    TEST_CI("xy", "do(d4)|len",       "8");
    TEST_CI("xy", "do(d4)|len(\"\")", "8");
    TEST_CI("xy", "do(d4)|len(x)",    "4");
    TEST_CI("x",  "do(d4096)|len",    "4096");

    // create 4096 streams (disable trace; logs to much):
    TEST_CI("x",  "trace(0)|dd;dd|dd;dd|dd;dd|dd;dd|dd;dd|dd;dd|dd;dd|dd;dd|dd;dd|dd;dd|dd;dd|dd;dd|streams", "4096");
}

__ATTR__REDUCED_OPTIMIZE__NO_GCSE void TEST_GB_command_interpreter_1b() {
    ACI_test_env E;
    GBL_env      base_env(E.gbmain(), NULp);

    // execute ACI on species container (=GB_DB) in this section ------------------------------
    GBDATA * const gb_data = E.gbspecies();
    GBL_call_env   callEnv(gb_data, base_env);

    // streams
    TEST_CI("x", "dd;dd|streams",             "2");
    TEST_CI("x", "dd;dd|dd;dd|streams",       "4");
    TEST_CI("x", "dd;dd|dd;dd|dd;dd|streams", "8");
    TEST_CI("x", "do(d4)|streams",            "1"); // stream is merged when do() returns

    TEST_CI("", "ali_name", "ali_16s");  // ask for default-alignment name
    TEST_CI("", "sequence_type", "rna"); // ask for sequence_type of default-alignment

    // format
    TEST_CI("acgt", "format", "          acgt");
    TEST_CI("acgt", "format(firsttab=1)", " acgt");
    TEST_CI("acgt", "format(firsttab=1, width=2)",
            " ac\n"
            "          gt");
    TEST_CI("acgt", "format(firsttab=1,tab=1,width=2)",
            " ac\n"
            " gt");
    TEST_CI("acgt", "format(firsttab=0,tab=0,width=2)",
            "ac\n"
            "gt");
    TEST_CI("acgt", "format(firsttab=0,tab=0,width=1)",
            "a\n"
            "c\n"
            "g\n"
            "t");

    TEST_CI_ERROR_CONTAINS("acgt", "format(gap=0)",   "Unknown Parameter 'gap=0' in command 'format'");
    TEST_CI_ERROR_CONTAINS("acgt", "format(numleft)", "Unknown Parameter 'numleft' in command 'format'");

    // format_sequence
    TEST_CI_ERROR_CONTAINS("acgt", "format_sequence(numright=5, numleft)", "You may only specify 'numleft' OR 'numright',  not both");

    TEST_CI("acgtacgtacgtacg", "format_sequence(firsttab=5,tab=5,width=4,numleft=1)",
            "1    acgt\n"
            "5    acgt\n"
            "9    acgt\n"
            "13   acg");

    TEST_CI("acgtacgtacgtacg", "format_sequence(firsttab=5,tab=5,width=4,numright=9)", // test EMBL sequence formatting
            "     acgt         4\n"
            "     acgt         8\n"
            "     acgt        12\n"
            "     acg         15");

    TEST_CI("acgtacgtacgtac", "format_sequence(firsttab=5,tab=5,width=4,gap=2,numright=-1)", // autodetect width for 'numright'
            "     ac gt  4\n"
            "     ac gt  8\n"
            "     ac gt 12\n"
            "     ac    14");

    TEST_CI("acgt", "format_sequence(firsttab=0,tab=0,width=2,gap=1)",
            "a c\n"
            "g t");
    TEST_CI("acgt",     "format_sequence(firsttab=0,tab=0,width=4,gap=1)", "a c g t");
    TEST_CI("acgt",     "format_sequence(firsttab=0,tab=0,width=4,gap=2)", "ac gt");
    TEST_CI("acgtacgt", "format_sequence(firsttab=0,width=10,gap=4)",      "acgt acgt");
    TEST_CI("acgtacgt", "format_sequence(firsttab=1,width=10,gap=4)",      " acgt acgt");

    TEST_CI("acgt", "format_sequence(firsttab=0,tab=0,gap=0)",   "acgt");
    TEST_CI("acgt", "format_sequence(firsttab=0,tab=0,gap=-1)",  "acgt"); // no big alloc
    TEST_CI("acgt", "format_sequence(firsttab=0,tab=-1,gap=-1)", "acgt"); // no big alloc
    TEST_CI("acgt", "format(firsttab=0,tab=0,width=-1)",         "acgt"); // no big alloc for(!)format

    TEST_CI("acgt", "format(firsttab=-1,tab=0)",           "acgt"); // did a 4Gb-alloc!
    TEST_CI("acgt", "format(firsttab=-1,tab=-1)",          "acgt"); // did a 4Gb-alloc!
    TEST_CI("acgt", "format(firsttab=-1,tab=-1,width=-1)", "acgt"); // did a 4Gb-alloc!

    TEST_CI("acgt", "format_sequence(firsttab=0,tab=0,gap=0,width=-1)",    "acgt"); // did a 4Gb-alloc!
    TEST_CI("acgt", "format_sequence(firsttab=-1,tab=0,gap=-1)",           "acgt"); // did a 4Gb-alloc!
    TEST_CI("acgt", "format_sequence(firsttab=-1,tab=-1,gap=-1)",          "acgt"); // did a 4Gb-alloc!
    TEST_CI("acgt", "format_sequence(firsttab=-1,tab=-1,gap=-1,width=-1)", "acgt"); // did a 4Gb-alloc!

    TEST_CI_ERROR_CONTAINS("acgt", "format_sequence(nl=c)",     "Unknown Parameter 'nl=c' in command 'format_sequence'");
    TEST_CI_ERROR_CONTAINS("acgt", "format_sequence(forcenl=)", "Unknown Parameter 'forcenl=' in command 'format_sequence'");

    TEST_CI_ERROR_CONTAINS("acgt", "format(width=0)",          "Illegal zero width");
    TEST_CI_ERROR_CONTAINS("acgt", "format_sequence(width=0)", "Illegal zero width");

    // remove + keep
    TEST_CI_NOOP("acgtacgt",         "remove(-.)");
    TEST_CI     ("..acg--ta-cgt...", "remove(-.)", "acgtacgt");
    TEST_CI     ("..acg--ta-cgt...", "remove(acgt)", "..---...");

    TEST_CI_NOOP("acgtacgt",         "keep(acgt)");
    TEST_CI     ("..acg--ta-cgt...", "keep(-.)", "..---...");
    TEST_CI     ("..acg--ta-cgt...", "keep(acgt)", "acgtacgt");

    // compare + icompare
    TEST_CI("x,z,y,y,z,x,x,Z,y,Y,Z,x", WITH_SPLITTED("|compare"),  "-1,0,1,1,1,-1");
    TEST_CI("x,z,y,y,z,x,x,Z,y,Y,Z,x", WITH_SPLITTED("|icompare"), "-1,0,1,-1,0,1");

    TEST_CI("x,y,z", WITH_SPLITTED("|compare(\"y\")"), "-1,0,1");

    // equals + iequals
    TEST_CI("a,b,a,a,a,A", WITH_SPLITTED("|equals"),  "0,1,0");
    TEST_CI("a,b,a,a,a,A", WITH_SPLITTED("|iequals"), "0,1,1");

    // contains + icontains
    TEST_CI("abc,bcd,BCD", WITH_SPLITTED("|contains(\"bc\")"),   "2,1,0");
    TEST_CI("abc,bcd,BCD", WITH_SPLITTED("|icontains(\"bc\")"),  "2,1,1");
    TEST_CI("abc,bcd,BCD", WITH_SPLITTED("|icontains(\"d\")"),   "0,3,3");

    // partof + ipartof
    TEST_CI("abc,BCD,def,deg", WITH_SPLITTED("|partof(\"abcdefg\")"),   "1,0,4,0");
    TEST_CI("abc,BCD,def,deg", WITH_SPLITTED("|ipartof(\"abcdefg\")"),  "1,2,4,0");

    TEST_CI(", ,  ,x", WITH_SPLITTED("|isempty"),             "1,0,0,0");
    TEST_CI(", ,  ,x", WITH_SPLITTED("|crop(\" \")|isempty"), "1,1,1,0");

    // translate
    TEST_CI("abcdefgh", "translate(abc,cba)",     "cbadefgh");
    TEST_CI("abcdefgh", "translate(cba,abc)",     "cbadefgh");
    TEST_CI("abcdefgh", "translate(hcba,abch,-)", "hcb----a");
    TEST_CI("abcdefgh", "translate(aceg,aceg,-)", "a-c-e-g-");
    TEST_CI("abbaabba", "translate(ab,ba,-)",     "baabbaab");
    TEST_CI("abbaabba", "translate(a,x,-)",       "x--xx--x");
    TEST_CI("abbaabba", "translate(,,-)",         "--------");

    // echo
    TEST_CI("", "echo", "");
    TEST_CI("", "echo(x,y,z)", "xyz");
    TEST_CI("", "echo(x;y,z)", "xyz"); // check ';' as param-separator
    TEST_CI("", "echo(x;y;z)", "xyz");
    TEST_CI("", "echo(x,y,z)|streams", "3");

    // upper, lower + caps
    TEST_CI("the QUICK brOwn Fox", "lower", "the quick brown fox");
    TEST_CI("the QUICK brOwn Fox", "upper", "THE QUICK BROWN FOX");
    TEST_CI("the QUICK brOwn FoX", "caps",  "The Quick Brown Fox");
}

__ATTR__REDUCED_OPTIMIZE__NO_GCSE void TEST_GB_command_interpreter_2a() {
    ACI_test_env E;
    GBL_env      base_env(E.gbmain(), NULp);

    // execute ACI on species container (=GB_DB) in this section ------------------------------
    GBDATA * const gb_data = E.gbspecies();
    GBL_call_env   callEnv(gb_data, base_env);

    TEST_CI_ERROR_CONTAINS("a;b;c", "split(;)|merge(-)",     "Invalid separator (cannot be empty");
    TEST_CI               ("a;b;c", "split(\";\")|merge(-)", "a-b-c");

    // head, tail + mid/mid0
    TEST_CI     ("1234567890", "head(3)", "123");
    TEST_CI     ("1234567890", "head(9)", "123456789");
    TEST_CI_NOOP("1234567890", "head(10)");
    TEST_CI_NOOP("1234567890", "head(20)");

    TEST_CI     ("1234567890", "tail(4)", "7890");
    TEST_CI     ("1234567890", "tail(9)", "234567890");
    TEST_CI_NOOP("1234567890", "tail(10)");
    TEST_CI_NOOP("1234567890", "tail(20)");

    TEST_CI("1234567890", "tail(0)", "");
    TEST_CI("1234567890", "head(0)", "");
    TEST_CI("1234567890", "tail(-2)", "");
    TEST_CI("1234567890", "head(-2)", "");

    TEST_CI("1234567890", "mid(3,5)", "345");
    TEST_CI("1234567890", "mid(2,2)", "2");

    TEST_CI("1234567890", "mid0(3,5)", "456");

    TEST_CI("1234567890", "mid(9,20)", "90");
    TEST_CI("1234567890", "mid(20,20)", "");

    TEST_CI("1234567890", "tail(3)",     "890"); // example from ../HELP_SOURCE/source/aci.hlp@mid0
    TEST_CI("1234567890", "mid(-2,0)",   "890");
    TEST_CI("1234567890", "mid0(-3,-1)", "890");

    // tab + pretab
    TEST_CI("x,xx,xxx", WITH_SPLITTED("|tab(2)"),    "x ,xx,xxx");
    TEST_CI("x,xx,xxx", WITH_SPLITTED("|tab(3)"),    "x  ,xx ,xxx");
    TEST_CI("x,xx,xxx", WITH_SPLITTED("|tab(4)"),    "x   ,xx  ,xxx ");
    TEST_CI("x,xx,xxx", WITH_SPLITTED("|pretab(2)"), " x,xx,xxx");
    TEST_CI("x,xx,xxx", WITH_SPLITTED("|pretab(3)"), "  x, xx,xxx");
    TEST_CI("x,xx,xxx", WITH_SPLITTED("|pretab(4)"), "   x,  xx, xxx");

    // crop
    TEST_CI("   x  x  ",         "crop(\" \")",     "x  x");
    TEST_CI("\n \t  x  x \n \t", "crop(\"\t\n \")", "x  x");

    // cut, drop, dropempty and dropzero
    TEST_CI("one,two,three,four,five,six", WITH_SPLITTED("|cut(2,3,5)"),        "two,three,five");
    TEST_CI("one,two,three,four,five,six", WITH_SPLITTED("|drop(2,3,5)"),       "one,four,six");

    TEST_CI_ERROR_CONTAINS("a", "drop(2)",   "Illegal stream number '2' (allowed [1..1])");
    TEST_CI_ERROR_CONTAINS("a", "drop(0)",   "Illegal stream number '0' (allowed [1..1])");
    TEST_CI_ERROR_CONTAINS("a", "drop",      "syntax: drop(streamnumber[,streamnumber]+)");
    TEST_CI_ERROR_CONTAINS("a", "cut(2)",    "Illegal stream number '2' (allowed [1..1])");
    TEST_CI_ERROR_CONTAINS("a", "cut(0)",    "Illegal stream number '0' (allowed [1..1])");
    TEST_CI_ERROR_CONTAINS("a", "cut",       "syntax: cut(streamnumber[,streamnumber]+)");
    TEST_CI_ERROR_CONTAINS("a", "cut()",     "Invalid empty parameter list '()'");
    TEST_CI_ERROR_CONTAINS("a", "cut(\"\")", "Illegal stream number '0' (allowed [1..1])"); // still strange (atoi("")->0)

    TEST_CI("one,two,three,four,five,six", WITH_SPLITTED("|dropempty|streams"), "6");
    TEST_CI("one,two,,,five,six",          WITH_SPLITTED("|dropempty|streams"), "4");
    TEST_CI(",,,,,",                       WITH_SPLITTED("|dropempty"),         "");
    TEST_CI(",,,,,",                       WITH_SPLITTED("|dropempty|streams"), "0");

    TEST_CI("1,0,0,2,3,0",                 WITH_SPLITTED("|dropzero"),          "1,2,3");
    TEST_CI("0,0,0,0,0,0",                 WITH_SPLITTED("|dropzero"),          "");
    TEST_CI("0,0,0,0,0,0",                 WITH_SPLITTED("|dropzero|streams"),  "0");

    TEST_CI("12345",        "|colsplit|streams",           "5");
    TEST_CI("12345",        "|colsplit"    ACI_MERGE,      "1,2,3,4,5");
    TEST_CI("12345",        "|colsplit(3)" ACI_MERGE,      "123,45");
    TEST_CI("12345,678,90", WITH_SPLITTED("|colsplit(2)"), "12,34,5,67,8,90");
    TEST_CI_NOOP("12345,678,90", WITH_SPLITTED("|colsplit(5)"));

    // swap
    TEST_CI("1,2,3,four,five,six", WITH_SPLITTED("|swap"),                "1,2,3,four,six,five");
    TEST_CI("1,2,3,four,five,six", WITH_SPLITTED("|swap(2,3)"),           "1,3,2,four,five,six");
    TEST_CI("1,2,3,four,five,six", WITH_SPLITTED("|swap(2,3)|swap(4,3)"), "1,3,four,2,five,six");
    TEST_CI_NOOP("1,2,3,four,five,six", WITH_SPLITTED("|swap(3,3)"));
    TEST_CI_NOOP("1,2,3,four,five,six", WITH_SPLITTED("|swap(3,2)|swap(2,3)"));
    TEST_CI_NOOP("1,2,3,four,five,six", WITH_SPLITTED("|swap(3,2)|swap(3,1)|swap(2,1)|swap(1,3)"));

    TEST_CI_ERROR_CONTAINS("a",   "swap",                        "need at least two input streams");
    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|swap(2,3)"),   "Illegal stream number '3' (allowed [1..2])");
    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|swap(3,2)"),   "Illegal stream number '3' (allowed [1..2])");
    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|swap(1)"),     "syntax: swap[(streamnumber,streamnumber)]");
    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|swap(1,2,3)"), "syntax: swap[(streamnumber,streamnumber)]");

    // toback + tofront
    TEST_CI     ("front,mid,back", WITH_SPLITTED("|toback(2)"),  "front,back,mid");
    TEST_CI     ("front,mid,back", WITH_SPLITTED("|tofront(2)"), "mid,front,back");
    TEST_CI_NOOP("front,mid,back", WITH_SPLITTED("|toback(3)"));
    TEST_CI_NOOP("front,mid,back", WITH_SPLITTED("|tofront(1)"));
    TEST_CI_NOOP("a",              WITH_SPLITTED("|tofront(1)"));
    TEST_CI_NOOP("a",              WITH_SPLITTED("|toback(1)"));

    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|tofront(3)"),  "Illegal stream number '3' (allowed [1..2])");
    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|toback(3)"),   "Illegal stream number '3' (allowed [1..2])");
    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|tofront"),     "syntax: tofront(streamnumber)");
    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|toback(1,2)"), "syntax: toback(streamnumber)");
    TEST_CI_ERROR_CONTAINS("a,b", WITH_SPLITTED("|merge(1,2)"),  "syntax: merge[(\"separator\")]");

    // split
    TEST_CI               ("a\nb", "|split"        ACI_MERGE, "a,b");
    TEST_CI               ("a-b",  "|split(-)"     ACI_MERGE, "a,b");
    TEST_CI               ("a-b",  "|split(-,0)"   ACI_MERGE, "a,b");
    TEST_CI               ("a-b",  "|split(-,1)"   ACI_MERGE, "a,-b");
    TEST_CI               ("a-b",  "|split(-,2)"   ACI_MERGE, "a-,b");
    TEST_CI_ERROR_CONTAINS("a-b",  "|split(-,3)"   ACI_MERGE, "Illegal split mode '3' (valid: 0..2)");
    TEST_CI_ERROR_CONTAINS("a\nb", "|split(1,2,3)" ACI_MERGE, "syntax: split[(\"separator\"[,mode])]");

#define C0_9 "0123456789"
#define CA_Z "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define Ca_z "abcdefghijklmnopqrstuvwxyz"

    // extract_words + extract_sequence
    TEST_CI("1,2,3,four,five,six",     "extract_words(\"" C0_9 "\",1)",            "1 2 3");
    TEST_CI("1,2,3,four,five,six",     "extract_words(\"" Ca_z "\", 3)",           "five four six");
    TEST_CI("1,2,3,four,five,six",     "extract_words(\"" CA_Z "\", 3)",           "");                 // extract words works case sensitive
    TEST_CI("1,2,3,four,five,six",     "extract_words(\"" Ca_z "\", 4)",           "five four");
    TEST_CI("1,2,3,four,five,six",     "extract_words(\"" Ca_z "\", 5)",           "");
    TEST_CI("7 3b 12A 1 767 111 1 77", "extract_words(\"" C0_9 CA_Z Ca_z "\", 1)", "1 1 111 12A 3b 7 767 77"); // does sort a list of helix numbers

    TEST_CI     ("1,2,3,four,five,six",    "extract_sequence(\"acgtu\", 1.0)",   "");
    TEST_CI     ("1,2,3,four,five,six",    "extract_sequence(\"acgtu\", 0.5)",   "");
    TEST_CI     ("1,2,3,four,five,six",    "extract_sequence(\"acgtu\", 0.0)",   "four five six");
    TEST_CI     ("..acg--ta-cgt...",       "extract_sequence(\"acgtu\", 1.0)",   "");
    TEST_CI_NOOP("..acg--ta-cgt...",       "extract_sequence(\"acgtu-.\", 1.0)");
    TEST_CI_NOOP("..acg--ta-ygt...",       "extract_sequence(\"acgtu-.\", 0.7)");
    TEST_CI     ("70 ..acg--ta-cgt... 70", "extract_sequence(\"acgtu-.\", 1.0)", "..acg--ta-cgt...");

    // checksum + gcgchecksum
    TEST_CI("", "sequence|checksum",      "4C549A5F");
    TEST_CI("", "sequence | gcgchecksum", "4308");

    // SRT
    TEST_CI("The quick brown fox", "srt(\"quick=lazy:brown fox=dog\")", "The lazy dog"); // no need to escape spaces in quoted ACI parameter
    TEST_CI("The quick brown fox", "srt(quick=lazy:brown\\ fox=dog)",   "The lazy dog"); // spaces need to be escaped in unquoted ACI parameter
    TEST_CI_ERROR_CONTAINS("x", "srt(x=y,z)", "SRT ERROR: no '=' found in command");
    TEST_CI_ERROR_CONTAINS("x", "srt",        "syntax: srt(expr[,expr]+)");

    // REG-replace and -match
    TEST_CI("stars*to*stripes", "/\\*/--/", "stars--to--stripes");

    TEST_CI_ERROR_CONTAINS("xxx", "//--",    "Regular expression format is '/expr/' or '/expr/i', not '//--'");
    TEST_CI_ERROR_CONTAINS("xxx", "/*/bla/",
#if defined(DARWIN)
                           // @@@ RESULT_MODIFIED_OSX: this test depends on library version
                           // should either test for one-of-several results or just test for any error
                           "repetition-operator operand invalid"
#else // !DARWIN
                           "Invalid preceding regular expression"
#endif
        );

    TEST_CI("sImILaRWIllBE,GonEEASIly", WITH_SPLITTED("|command(/[A-Z]//)"),   "small,only");
    TEST_CI("sthBIGinside,FATnotCAP",   WITH_SPLITTED("|command(/([A-Z])+/)"), "BIG,FAT"); // does only do match

    // command-queue vs. command-pipe (vs. both as sub-commands)
    TEST_CI("a,bb,ccc",           WITH_SPLITTED("|\"[\";len;\"]\""),    "[,1,2,3,]");   // queue
    TEST_CI("a,bb,ccc", WITH_SPLITTED("|command(\"\"[\";len;\"]\"\")"), "[1],[2],[3]"); // queue as sub-command

    TEST_CI("a,bb,ccc",              WITH_SPLITTED("|len|minus(1)"),    "0,1,2"); // pipe
    TEST_CI("a,bb,ccc",    WITH_SPLITTED("|command(\"len|minus(1)\")"), "0,1,2"); // pipe as sub-command

    TEST_CI(               "a,bb,ccc,dd",           WITH_SPLITTED("|len|minus"), "-1,1"); // pipe
    TEST_CI_ERROR_CONTAINS("a,bb,ccc,dd", WITH_SPLITTED("|command(\"len|minus\")"), "Expect an even number of input streams"); // pipe as sub-command FAILS
}

__ATTR__REDUCED_OPTIMIZE__NO_GCSE void TEST_GB_command_interpreter_2b() {
    ACI_test_env E;
    GBL_env      base_env(E.gbmain(), NULp);

    // execute ACI on species container (=GB_DB) in this section ------------------------------
    GBDATA * const gb_data = E.gbspecies();
    GBL_call_env   callEnv(gb_data, base_env);

    // calculator
    TEST_CI("", "echo(9.9,3.9) |plus;fplus"   ACI_MERGE, "12,13.8");
    TEST_CI("", "echo(9.1,3.9) |minus;fminus" ACI_MERGE, "6,5.2");
    TEST_CI("", "echo(9,3.5)   |mult;fmult"   ACI_MERGE, "27,31.5");
    TEST_CI("", "echo(9,0.1)   |mult;fmult"   ACI_MERGE, "0,0.9");
    TEST_CI("", "echo(9,3)     |div;fdiv"     ACI_MERGE, "3,3");
    TEST_CI("", "echo(10,3)    |div;fdiv"     ACI_MERGE, "3,3.33333");

    TEST_CI("", "echo(9,3)|rest", "0");
    TEST_CI("", "echo(9,5)|rest", "4");

    TEST_CI("", "echo(9,3)  |per_cent;fper_cent" ACI_MERGE, "300,300");
    TEST_CI("", "echo(3,9)  |per_cent;fper_cent" ACI_MERGE, "33,33.3333");
    TEST_CI("", "echo(1,8)  |per_cent;fper_cent" ACI_MERGE, "12,12.5");
    TEST_CI("", "echo(15,16)|per_cent;fper_cent" ACI_MERGE, "93,93.75");
    TEST_CI("", "echo(1,8)  |fper_cent|round(0)", "13");
    TEST_CI("", "echo(15,16)|fper_cent|round(0);round(1)" ACI_MERGE, "94,93.8");

    TEST_CI("", "echo(1,2,3)|plus(1)"     ACI_MERGE, "2,3,4");
    TEST_CI("", "echo(1,2,3)|minus(2)"    ACI_MERGE, "-1,0,1");
    TEST_CI("", "echo(1,2,3)|mult(42)"    ACI_MERGE, "42,84,126");
    TEST_CI("", "echo(1,2,3)|div(2)"      ACI_MERGE, "0,1,1");
    TEST_CI("", "echo(1,2,3)|rest(2)"     ACI_MERGE, "1,0,1");
    TEST_CI("", "echo(1,2,3)|per_cent(3)" ACI_MERGE, "33,66,100");

    // rounding
#define ROUND_FLOATS(dig) "echo(0.3826,0.50849,12.58,77.2,700.099,0.9472e-4,0.175e+7)|round(" #dig ")" ACI_MERGE

    TEST_CI("", ROUND_FLOATS(4),  "0.3826,0.5085,12.58,77.2,700.099,0.0001,1.75e+06");
    TEST_CI("", ROUND_FLOATS(3),  "0.383,0.508,12.58,77.2,700.099,0,1.75e+06");
    TEST_CI("", ROUND_FLOATS(2),  "0.38,0.51,12.58,77.2,700.1,0,1.75e+06");
    TEST_CI("", ROUND_FLOATS(1),  "0.4,0.5,12.6,77.2,700.1,0,1.75e+06");
    TEST_CI("", ROUND_FLOATS(0),  "0,1,13,77,700,0,1.75e+06");
    TEST_CI("", ROUND_FLOATS(-1), "0,0,10,80,700,0,1.75e+06");
    TEST_CI("", ROUND_FLOATS(-2), "0,0,0,100,700,0,1.75e+06");
    TEST_CI("", ROUND_FLOATS(-3), "0,0,0,0,1000,0,1.75e+06");
    TEST_CI("", ROUND_FLOATS(-5), "0,0,0,0,0,0,1.8e+06");
    TEST_CI("", ROUND_FLOATS(-6), "0,0,0,0,0,0,2e+06");


    // compare (integers)
    TEST_CI("", "echo(9,3)|isBelow;isAbove;isEqual", "010");
    TEST_CI("", "echo(3,9)|isBelow;isAbove;isEqual", "100");
    TEST_CI("", "echo(5,5)|isBelow;isAbove;isEqual", "001");

    TEST_CI("", "echo(1,2,3)|isBelow(2)", "100");
    TEST_CI("", "echo(1,2,3)|isAbove(2)", "001");
    TEST_CI("", "echo(1,2,3)|isEqual(2)", "010");

    TEST_CI("", "echo(1,2,3,4,5)|inRange(2,4)",        "01110");
    TEST_CI("", "echo(-1,-2,-3,-4,-5)|inRange(-2,-4)", "00000"); // empty range
    TEST_CI("", "echo(-1,-2,-3,-4,-5)|inRange(-4,-2)", "01110");

    // compare (floats)
    TEST_CI("", "echo(1.7,1.4)  |isBelow;isAbove;isEqual", "010");
    TEST_CI("", "echo(-0.7,0.1) |isBelow;isAbove;isEqual", "100");
    TEST_CI("", "echo(5.10,5.1) |isBelow;isAbove;isEqual", "001");
    TEST_CI("", "echo(0.10,.11) |isBelow;isAbove;isEqual", "100");
    TEST_CI("", "echo(-7.1,-6.9)|isBelow;isAbove;isEqual", "100");
    TEST_CI("", "echo(1e+5,1e+6)|isBelow;isAbove;isEqual", "100");
    TEST_CI("", "echo(2e+5,1e+6)|isBelow;isAbove;isEqual", "100");
    TEST_CI("", "echo(2e+5,1e-6)|isBelow;isAbove;isEqual", "010");
    TEST_CI("", "echo(2e-5,1e+6)|isBelow;isAbove;isEqual", "100");

    TEST_CI("", "echo(.1,.2,.3,.4,.5)   |inRange(.2,.4)",  "01110");
    TEST_CI("", "echo(.8,.9,1.0,1.1,1.2)|inRange(.9,1.1)", "01110");
    TEST_CI("", "echo(-.2,-.1,0.0,.1,.2)|inRange(-.1,.1)", "01110");

    // boolean operators
    TEST_CI("0", "Not", "1");
    TEST_CI("1", "Not", "0");

    TEST_CI("",     "Not", "1");
    TEST_CI("text", "Not", "1");

    TEST_CI("", "echo(0,1)|Not", "10");
    TEST_CI("", "echo(0,0)|Or;And",  "00");
    TEST_CI("", "echo(0,1)|Or;And",  "10");
    TEST_CI("", "echo(1,0)|Or;And",  "10");
    TEST_CI("", "echo(1,1)|Or;And",  "11");

    TEST_CI("", "command(echo(1\\,0)|Or);command(echo(0\\,1)|Or)|And",  "1");

    // readdb
    TEST_CI("", "readdb(name)",     "LcbReu40");
    TEST_CI("", "readdb(acc)",      "X76328");
    TEST_CI("", "readdb(acc,name)", "X76328LcbReu40");

    TEST_CI_ERROR_CONTAINS("", "readdb()",     "Invalid empty parameter list '()'");
    TEST_CI_ERROR_CONTAINS("", "readdb",       "syntax: readdb(fieldname[,fieldname]+)");
    TEST_CI               ("", "readdb(\"\")", ""); // still weird (want field error?)

    // taxonomy
    TEST_CI("", "taxonomy(1)",           "No default tree");
    TEST_CI("", "taxonomy(tree_nuc, 1)", "group1");
    TEST_CI("", "taxonomy(tree_nuc, 5)", "lower-red/group1");
}

__ATTR__REDUCED_OPTIMIZE__NO_GCSE void TEST_GB_command_interpreter_2c() {
    ACI_test_env E;
    GBL_env      base_env(E.gbmain(), NULp);

    GBDATA * const gb_data = E.gbspecies();
    GBL_call_env   callEnv(gb_data, base_env);

    GBL_env      env_tree_nuc(E.gbmain(), "tree_nuc");
    GBL_call_env callEnv_tree_nuc(gb_data, env_tree_nuc);

    TEST_CI_ERROR_CONTAINS("", "taxonomy",        "syntax: taxonomy([tree_name,]count)");
    TEST_CI_ERROR_CONTAINS("", "taxonomy(1,2,3)", "syntax: taxonomy([tree_name,]count)");
    TEST_CI_WITH_ENV("", callEnv_tree_nuc, "taxonomy(1)", "group1");

    // diff, filter + change
    TEST_CI("..acg--ta-cgt..." ","
            "..acg--ta-cgt...", WITH_SPLITTED("|diff(pairwise=1)"),
            "................");
    TEST_CI("..acg--ta-cgt..." ","
            "..cgt--ta-acg...", WITH_SPLITTED("|diff(pairwise=1,equal==)"),
            "==cgt=====acg===");
    TEST_CI("..acg--ta-cgt..." ","
            "..cgt--ta-acg...", WITH_SPLITTED("|diff(pairwise=1,differ=X)"),
            "..XXX.....XXX...");
    TEST_CI("", "sequence|diff(species=LcbFruct)|checksum", "645E3107");

    TEST_CI("..XXX.....XXX..." ","
            "..acg--ta-cgt...", WITH_SPLITTED("|filter(pairwise=1,exclude=X)"),
            "..--ta-...");
    TEST_CI("..XXX.....XXX..." ","
            "..acg--ta-cgt...", WITH_SPLITTED("|filter(pairwise=1,include=X)"),
            "acgcgt");
    TEST_CI("", "sequence|filter(species=LcbFruct,include=.-)", "-----------T----T-------G----------C-----T----T...");

    TEST_CI("...XXX....XXX..." ","
            "..acg--ta-cgt...", WITH_SPLITTED("|change(pairwise=1,include=X,to=C,change=100)"),
            "..aCC--ta-CCC...");
    TEST_CI("...XXXXXXXXX...." ","
            "..acg--ta-cgt...", WITH_SPLITTED("|change(pairwise=1,include=X,to=-,change=100)"),
            "..a---------t...");

    // test environment forwarding
    TEST_CI("x", ":*=*,*(acc)",                                 "x,X76328");          // test DB-item is forwarded to direct SRT-command
    TEST_CI("x", "srt(\"*=*,*(acc)\")",                         "x,X76328");          // test DB-item is forwarded to ACI-command 'srt'
    TEST_CI("x", ":*=*,*(acc|dd;\",\";readdb(name))",           "x,X76328,LcbReu40"); // test DB-item is forwarded to ACI-subexpression inside SRT-command
    TEST_CI("x", "srt(\"*=*,*(acc|dd;\"\\,\";readdb(name))\")", "x,X76328,LcbReu40"); // test DB-item is forwarded to ACI-subexpression inside ACI-command 'srt'
    TEST_CI("x", "command(\"dd;\\\",\\\";readdb(name)\")",      "x,LcbReu40");        // test DB-item is forwarded to ACI-subexpression inside ACI-command 'command'

    // test treename is forwarded to sub-expressions
    TEST_CI_WITH_ENV("x", callEnv_tree_nuc, ":*=*,*(acc|dd;\\\",\\\";taxonomy(1))",                   "x,X76328,group1");
    TEST_CI_WITH_ENV("",  callEnv_tree_nuc, "taxonomy(5)|srt(*=*\\,*(acc|dd;\\\",\\\";taxonomy(1)))", "lower-red/group1,X76328,group1");
    TEST_CI_WITH_ENV("",  callEnv_tree_nuc, "taxonomy(5)|command(\"dd;\\\",\\\";taxonomy(1)\")",      "lower-red/group1,group1");

    // test database root is forwarded to sub-expressions (used by commands 'ali_name', 'sequence_type', ...)
    TEST_CI("x", ":*=*,*(acc|dd;\\\",\\\";ali_name;\\\",\\\";sequence_type)",         "x,X76328,ali_16s,rna");
    TEST_CI("x", "srt(\"*=*,*(acc|dd;\\\",\\\";ali_name;\\\",\\\";sequence_type)\")", "x,X76328,ali_16s,rna");
    TEST_CI("x", "command(\"dd;\\\",\\\";ali_name;\\\",\\\";sequence_type\")",        "x,ali_16s,rna");

    // exec
    TEST_CI("c,b,c,b,a,a", WITH_SPLITTED("|exec(\"(sort|uniq)\")|split|dropempty"),              "a,b,c");
    TEST_CI("a,aba,cac",   WITH_SPLITTED("|exec(\"perl\",-pe,s/([bc])/$1$1/g)|split|dropempty"), "a,abba,ccacc");

    // error cases
    TEST_CI_ERROR_CONTAINS("", "nocmd",            "Unknown command 'nocmd'");
    TEST_CI_ERROR_CONTAINS("", "|nocmd",           "Unknown command 'nocmd'");
    TEST_CI_ERROR_CONTAINS("", "caps(x)",          "syntax: caps (no parameters)");
    TEST_CI_ERROR_CONTAINS("", "trace",            "syntax: trace(0|1)");
    TEST_CI_ERROR_CONTAINS("", "count",            "syntax: count(\"characters to count\")");
    TEST_CI_ERROR_CONTAINS("", "count(a,b)",       "syntax: count(\"characters to count\")");
    TEST_CI_ERROR_CONTAINS("", "len(a,b)",         "syntax: len[(\"characters not to count\")]");
    TEST_CI_ERROR_CONTAINS("", "plus(a,b,c)",      "syntax: plus[(Expr1[,Expr2])]");
    TEST_CI_ERROR_CONTAINS("", "count(a,b",        "Reason: Missing ')'");
    TEST_CI_ERROR_CONTAINS("", "count(a,\"b)",     "unbalanced '\"' in 'count(a,\"b)'");
    TEST_CI_ERROR_CONTAINS("", "count(a,\"b)\"",   "Reason: Missing ')'");
    TEST_CI_ERROR_CONTAINS("", "dd;dd|count",      "syntax: count(\"characters to count\")");
    TEST_CI_ERROR_CONTAINS("", "|count(\"a\"x)",   "Invalid parameter syntax for '\"a\"x'");
    TEST_CI_ERROR_CONTAINS("", "|count(\"a\"x\")", "unbalanced '\"' in '|count(\"a\"x\")'");
    TEST_CI_ERROR_CONTAINS("", "|count(\"a)",      "unbalanced '\"' in '|count(\"a)'");

    TEST_CI_ERROR_CONTAINS__BROKEN("", "|\"xx\"bla", "bla", "xx"); // @@@ should report some error referring to unseparated + unknown command 'bla'

    TEST_CI_ERROR_CONTAINS("", "translate(a)",       "syntax: translate(old,new[,other])");
    TEST_CI_ERROR_CONTAINS("", "translate(a,b,c,d)", "syntax: translate(old,new[,other])");
    TEST_CI_ERROR_CONTAINS("", "translate(a,b,xx)",  "has to be one character");
    TEST_CI_ERROR_CONTAINS("", "translate(a,b,)",    "has to be one character");

    TEST_CI_ERROR_CONTAINS(NULp, "whatever", "ARB ERROR: Can't read this DB entry as string"); // here gb_data is the species container

    TEST_CI("hello",   ":??""=(?-?)",   "(h-e)(l-l)o");
    TEST_CI("hello",   ":??""=(?-?)?",  "(h-e)?(l-l)?o");
    TEST_CI("hello",   ":??""=(?-?0)?", "(h-e0)?(l-l0)?o");
    TEST_CI("hello",   ":??""=(?-?3)?", "(h-?)e(l-?)lo");

    // show linefeed is handled identical for encoded and escaped linefeeds:
    TEST_CI("abc",   ":?=?\\n", "a\nb\nc\n");
    TEST_CI("abc",   ":?=?\n",  "a\nb\nc\n");

    // same for string-terminator:
    TEST_CI("abc",   ":?=?.\\0 ignored:b=d", "a.b.c.");
    TEST_CI("abc",   ":?=?.\0  ignored:b=d", "a.b.c.");

    TEST_CI("",   ":*=X*Y*(full_name|len)", "XY21");
    TEST_CI("",   ":*=*(full_name\\:reuteri=xxx)", "Lactobacillus xxx");
    TEST_CI("",   ":*=*(abc\\:a=A)", ""); // non-existing field -> empty input -> empty output
    TEST_CI("hello world",   ":* =*(\\:*=hi)-", "hi-world"); // srt subexpressions also work w/o key
    TEST_CI_ERROR_CONTAINS("",   ":*=*(full_name\\:reuteri)", "no '=' found"); // test handling of errors from invalid srt-subexpression

    TEST_CI("", ":*=*(acc#have no acc)", "X76328");
    TEST_CI("", ":*=*(abc#have no abc)", "have no abc");
    TEST_CI("", ":*=*(#no field)",       "no field");

    TEST_CI_ERROR_CONTAINS("", ":*=*(unbalanced",     "Unbalanced parenthesis in '(unbalanced'");
    TEST_CI_ERROR_CONTAINS("", ":*=*(unba(lan)ced",   "Unbalanced parenthesis in '(unba(lan)ced'");
    TEST_CI_ERROR_CONTAINS("", ":*=*(unba(lan)c)ed)", "Invalid char '(' in key 'unba(lan)c'");      // invalid key name
    TEST_CI               ("", ":*=*(unbalanc)ed)",   "ed)");
}

__ATTR__REDUCED_OPTIMIZE void TEST_GB_command_interpreter_3() {
    ACI_test_env E;
    GBL_env      base_env(E.gbmain(), NULp);

    {
        // execute ACI on 'full_name' (=GB_STRING) in this section ------------------------------
        GBDATA * const gb_data = GB_entry(E.gbspecies(), "full_name");
        GBL_call_env   callEnv(gb_data, base_env);

        TEST_CI(NULp, "",                            "Lactobacillus reuteri");     // noop
        TEST_CI(NULp, "|len",                        "21");
        TEST_CI(NULp, ":tobac=",                     "Lacillus reuteri");
        TEST_CI(NULp, "/ba.*us/B/",                  "LactoB reuteri");
        TEST_CI(NULp, ":::*=hello:::hell=heaven:::", "heaveno");                   // test superfluous ':'s
        TEST_CI(NULp, ":* *=;*2,*1;",                ";reuteri,Lactobacillus;");   // tests multiple successful matches of '*'
        TEST_CI(NULp, ":* ??*=;?2,?1,*2,*1;",        ";e,r,uteri,Lactobacillus;"); // tests multiple successful matches of '*' and '?' (also tests working multi-wildcards "??" and "?*")
        TEST_CI(NULp, ":Lacto*eutei=*1",             "Lactobacillus reuteri");     // test match failing after '*' (=> skips replace)
        TEST_CI(NULp, ":Lact?bac?lls=?1?2",          "Lactobacillus reuteri");     // test match failing after 2nd '?' (=> skips replace)
        TEST_CI(NULp, ":*reuteri?=?1",               "Lactobacillus reuteri");     // test match failing on '?' behind EOS (=> skips replace)

        // tests for (unwanted) multi-wildcards:
        TEST_CI__BROKEN(NULp, ":Lacto*?lus=(*1,?1)", "(baci,l)", "Lactobacillus reuteri"); // @@@ diffcult to achieve (alternative: forbid "*?" and report error)
        TEST_CI__BROKEN("Lactobaci\4lus reuteri", ":Lacto*?lus=(*1,?1)", "<want error instead>", "(baci,?) reuteri"); // @@@ pathological case forcing a match for above situation (ASCII 4 is code for '?' wildcard)
        TEST_CI_ERROR_CONTAINS__BROKEN(NULp, ":Lacto**lus=(*1,*2)", "invalid", "Lactobacillus reuteri"); // @@@ impossible: (forbid "**" and report error)
        TEST_CI_ERROR_CONTAINS__BROKEN("Lactobac\3lus reuteri", ":Lacto**lus=(*1,*2)", "invalid", "(bac,*) reuteri"); // @@@ pathological case forcing a match for above situation (ASCII 3 is code for '*' wildcard)

        TEST_CI_ERROR_CONTAINS(NULp, ":*=*(|wot)",    "Unknown command 'wot'"); // provoke error in gbs_build_replace_string [coverage]
        TEST_CI_ERROR_CONTAINS("",   ":*=X*Y*(|wot)", "Unknown command 'wot'"); // dito (other caller)

        TEST_CI_ERROR_CONTAINS("", ":*=X*Y*(full_name|len)",    "can't read key 'full_name' (DB item is no container)");
        TEST_CI               ("", ":*=X*Y*(../full_name|len)", "XY21"); // searches entry via parent-entry (from non-container)

        TEST_CI(NULp, "|taxonomy(1)", "No default tree");
        TEST_CI_ERROR_CONTAINS(NULp, "|taxonomy(tree_nuc,2)", "Container has neither 'name' nor 'group_name' entry - can't detect container type");
    }
    {
        // execute ACI on 'ARB_color' (=GB_INT) in this section ------------------------------
        GBDATA * const gb_data = GB_entry(E.gbspecies(), "ARB_color");
        GBL_call_env   callEnv(gb_data, base_env);

        TEST_CI(NULp, "", "1"); // noop
        TEST_CI("", "ali_name;\",\";sequence_type", "ali_16s,rna"); // test global database access works when specific database element is specified
    }
    {
        // execute ACI without database element in this section ------------------------------
        GBDATA * const gb_data = NULp;
        GBL_call_env   callEnv(gb_data, base_env);

        TEST_CI_ERROR_CONTAINS(NULp, "", "no input streams found");
        TEST_CI("", ":*=\\tA*1Z\t", "\tAZ\t"); // special case (match empty input using '*'); test TAB conversion

        TEST_CI_ERROR_CONTAINS("", ":*=X*Y*(|wot)",      "Unknown command 'wot'");
        TEST_CI_ERROR_CONTAINS("", ":*=X*Y*(nokey|len)", "can't read key 'nokey' (called w/o database item)");
        TEST_CI_ERROR_CONTAINS("", ":*=X*Y*(nokey)",     "can't read key 'nokey' (called w/o database item)");

        // test global database access also works w/o specific database element
        TEST_CI("", "ali_name;\",\";sequence_type", "ali_16s,rna");
        TEST_CI("", "command(\"ali_name;\\\",\\\";sequence_type\")", "ali_16s,rna");

        // empty+NULp commands:
        TEST_CI("in", NULp, "in");
        TEST_CI("in", "", "in");
        TEST_CI("in", ":", "in");
        TEST_CI("in", "::", "in");

        // empty+NULp commands:
        TEST_CI("in", NULp, "in");
        TEST_CI("in", "", "in");
        TEST_CI("in", ":", "in");
        TEST_CI("in", "::", "in");
    }

    // register custom ACI commands
    {
        const GBL_command_lookup_table& stdCmds = ACI_get_standard_commands();

        GBL_command_definition custom_cmds[] = {
            { "custom", gbx_custom },              // new command 'custom'
            { "upper",  stdCmds.lookup("lower") }, // change meaning of lower ..
            { "lower",  stdCmds.lookup("upper") }, // .. and upper

            {NULp, NULp}
        };

        GBL_custom_command_lookup_table custom(custom_cmds, ARRAY_ELEMS(custom_cmds)-1, stdCmds, PERMIT_SUBSTITUTION);

        GBDATA * const gb_data = E.gbspecies();

        GBL_env      custom_env(E.gbmain(), NULp, custom);
        GBL_call_env customCallEnv(gb_data, custom_env);
        GBL_call_env callEnv(gb_data, base_env);

        // lookup overwritten commands:
        TEST_EXPECT(custom.lookup("upper") == stdCmds.lookup("lower"));
        TEST_EXPECT(custom.lookup("lower") == stdCmds.lookup("upper"));

        // test new commands:
        TEST_CI_WITH_ENV      ("abc", customCallEnv,  "dd;custom;dd", "abc4711abc");
        TEST_CI_ERROR_CONTAINS("abc", "dd;custom;dd", "Unknown command 'custom'"); // unknown in standard environment

        // test overwritten commands:
        TEST_CI_WITH_ENV("abcDEF,", customCallEnv, "dd;lower;upper", "abcDEF,ABCDEF,abcdef,");
        TEST_CI         ("abcDEF,",                "dd;lower;upper", "abcDEF,abcdef,ABCDEF,");
    }
}

void TEST_GB_command_interpreter_4() {
    ACI_test_env E;
    GBL_env      base_env(E.gbmain(), NULp);

    // execute ACI on species container (=GB_DB) in this section ------------------------------
    GBDATA * const gb_data = E.gbspecies();
    GBL_call_env   callEnv(gb_data, base_env);

    TEST_CI("LcbReu40", "findspec(\"readdb             (acc)\")", "X76328");
    TEST_CI("LcbFruct", "findspec(\"readdb             (acc)\")", "X76330");
    TEST_CI("",         "readdb(name)|findspec(\"readdb(acc)\")", "X76328");

    TEST_CI("LcbReu40;lcbfruct", "split(\";\")|findspec(\"readdb(acc)\")|merge(\";\")", "X76328;X76330");     // usecase ("bring next-relatives info into name-independent format")
    TEST_CI("X76328;x76330",     "split(\";\")|findacc(\"readdb(name)\")|merge(\";\")", "LcbReu40;LcbFruct"); // perform opposite (tests 'findacc')

    TEST_CI               ("",         "findspec(\"invalid\")", "");                                           // does not execute command for unnamed item
    TEST_CI_ERROR_CONTAINS("LcbReu40", "findspec(\"invalid\")", "Unknown command 'invalid'");
    TEST_CI_ERROR_CONTAINS("unknown",  "findspec(\"invalid\")", "No species with name 'unknown' found");
    TEST_CI_ERROR_CONTAINS("unknown",  "findacc(\"invalid\")",  "No species with acc 'unknown' found");
}

#endif // UNIT_TESTS

