hush: replace flag bytes in struct o_string with bit flags
[oweals/busybox.git] / shell / hush.c
index 7640bd6ba72205061af51885b16c57e3a9330c86..05ac4096fe9fd9bd90b065ac8c4f277aca4b91eb 100644 (file)
@@ -50,7 +50,6 @@
  *
  * Bash compat TODO:
  *      redirection of stdout+stderr: &> and >&
- *      subst operator: ${var/[/]expr/expr}
  *      brace expansion: one/{two,three,four}
  *      reserved words: function select
  *      advanced test: [[ ]]
@@ -79,7 +78,7 @@
  *              $ "export" i=`echo 'aaa  bbb'`; echo "$i"
  *              aaa
  *
- * Licensed under the GPL v2 or later, see the file LICENSE in this tarball.
+ * Licensed under GPLv2 or later, see file LICENSE in this source tree.
  */
 #include "busybox.h"  /* for APPLET_IS_NOFORK/NOEXEC */
 #include <malloc.h>   /* for malloc_trim */
 # define PIPE_BUF 4096  /* amount of buffering in a pipe */
 #endif
 
+//applet:IF_HUSH(APPLET(hush, _BB_DIR_BIN, _BB_SUID_DROP))
+//applet:IF_MSH(APPLET(msh, _BB_DIR_BIN, _BB_SUID_DROP))
+//applet:IF_FEATURE_SH_IS_HUSH(APPLET_ODDNAME(sh, hush, _BB_DIR_BIN, _BB_SUID_DROP, sh))
+//applet:IF_FEATURE_BASH_IS_HUSH(APPLET_ODDNAME(bash, hush, _BB_DIR_BIN, _BB_SUID_DROP, bash))
+
+//kbuild:lib-$(CONFIG_HUSH) += hush.o match.o shell_common.o
+//kbuild:lib-$(CONFIG_HUSH_RANDOM_SUPPORT) += random.o
+
+//config:config HUSH
+//config:      bool "hush"
+//config:      default y
+//config:      help
+//config:        hush is a small shell (25k). It handles the normal flow control
+//config:        constructs such as if/then/elif/else/fi, for/in/do/done, while loops,
+//config:        case/esac. Redirections, here documents, $((arithmetic))
+//config:        and functions are supported.
+//config:
+//config:        It will compile and work on no-mmu systems.
+//config:
+//config:        It does not handle select, aliases, brace expansion,
+//config:        tilde expansion, &>file and >&file redirection of stdout+stderr.
+//config:
+//config:config HUSH_BASH_COMPAT
+//config:      bool "bash-compatible extensions"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable bash-compatible extensions.
+//config:
+//config:config HUSH_HELP
+//config:      bool "help builtin"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable help builtin in hush. Code size + ~1 kbyte.
+//config:
+//config:config HUSH_INTERACTIVE
+//config:      bool "Interactive mode"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable interactive mode (prompt and command editing).
+//config:        Without this, hush simply reads and executes commands
+//config:        from stdin just like a shell script from a file.
+//config:        No prompt, no PS1/PS2 magic shell variables.
+//config:
+//config:config HUSH_JOB
+//config:      bool "Job control"
+//config:      default y
+//config:      depends on HUSH_INTERACTIVE
+//config:      help
+//config:        Enable job control: Ctrl-Z backgrounds, Ctrl-C interrupts current
+//config:        command (not entire shell), fg/bg builtins work. Without this option,
+//config:        "cmd &" still works by simply spawning a process and immediately
+//config:        prompting for next command (or executing next command in a script),
+//config:        but no separate process group is formed.
+//config:
+//config:config HUSH_TICK
+//config:      bool "Process substitution"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable process substitution `command` and $(command) in hush.
+//config:
+//config:config HUSH_IF
+//config:      bool "Support if/then/elif/else/fi"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable if/then/elif/else/fi in hush.
+//config:
+//config:config HUSH_LOOPS
+//config:      bool "Support for, while and until loops"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable for, while and until loops in hush.
+//config:
+//config:config HUSH_CASE
+//config:      bool "Support case ... esac statement"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable case ... esac statement in hush. +400 bytes.
+//config:
+//config:config HUSH_FUNCTIONS
+//config:      bool "Support funcname() { commands; } syntax"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable support for shell functions in hush. +800 bytes.
+//config:
+//config:config HUSH_LOCAL
+//config:      bool "Support local builtin"
+//config:      default y
+//config:      depends on HUSH_FUNCTIONS
+//config:      help
+//config:        Enable support for local variables in functions.
+//config:
+//config:config HUSH_RANDOM_SUPPORT
+//config:      bool "Pseudorandom generator and $RANDOM variable"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        Enable pseudorandom generator and dynamic variable "$RANDOM".
+//config:        Each read of "$RANDOM" will generate a new pseudorandom value.
+//config:
+//config:config HUSH_EXPORT_N
+//config:      bool "Support 'export -n' option"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        export -n unexports variables. It is a bash extension.
+//config:
+//config:config HUSH_MODE_X
+//config:      bool "Support 'hush -x' option and 'set -x' command"
+//config:      default y
+//config:      depends on HUSH
+//config:      help
+//config:        This instructs hush to print commands before execution.
+//config:        Adds ~300 bytes.
+//config:
+//config:config MSH
+//config:      bool "msh (deprecated: aliased to hush)"
+//config:      default n
+//config:      select HUSH
+//config:      help
+//config:        msh is deprecated and will be removed, please migrate to hush.
+//config:
+
+//usage:#define hush_trivial_usage NOUSAGE_STR
+//usage:#define hush_full_usage ""
+//usage:#define msh_trivial_usage NOUSAGE_STR
+//usage:#define msh_full_usage ""
+
 
 /* Build knobs */
 #define LEAK_HUNTING 0
 #define _SPECIAL_VARS_STR     "_*@$!?#"
 #define SPECIAL_VARS_STR     ("_*@$!?#" + 1)
 #define NUMERIC_SPECVARS_STR ("_*@$!?#" + 3)
+#if ENABLE_HUSH_BASH_COMPAT
+/* Support / and // replace ops */
+/* Note that // is stored as \ in "encoded" string representation */
+# define VAR_ENCODED_SUBST_OPS      "\\/%#:-=+?"
+# define VAR_SUBST_OPS             ("\\/%#:-=+?" + 1)
+# define MINUS_PLUS_EQUAL_QUESTION ("\\/%#:-=+?" + 5)
+#else
+# define VAR_ENCODED_SUBST_OPS      "%#:-=+?"
+# define VAR_SUBST_OPS              "%#:-=+?"
+# define MINUS_PLUS_EQUAL_QUESTION ("%#:-=+?" + 3)
+#endif
 
 #define SPECIAL_VAR_SYMBOL   3
 
@@ -204,7 +349,7 @@ typedef struct nommu_save_t {
 } nommu_save_t;
 #endif
 
-typedef enum reserved_style {
+enum {
        RES_NONE  = 0,
 #if ENABLE_HUSH_IF
        RES_IF    ,
@@ -233,7 +378,13 @@ typedef enum reserved_style {
 #endif
        RES_XXXX  ,
        RES_SNTX
-} reserved_style;
+};
+
+enum {
+       EXP_FLAG_GLOB = 0x200,
+       EXP_FLAG_ESC_GLOB_CHARS = 0x100,
+       EXP_FLAG_SINGLEWORD = 0x80, /* must be 0x80 */
+};
 
 typedef struct o_string {
        char *data;
@@ -241,11 +392,10 @@ typedef struct o_string {
        int maxlen;
        /* Protect newly added chars against globbing
         * (by prepending \ to *, ?, [, \) */
-       smallint o_escape;
-       smallint o_glob;
+       int o_expflags;
        /* At least some part of the string was inside '' or "",
         * possibly empty one: word"", wo''rd etc. */
-       smallint o_quoted;
+       smallint has_quoted_part;
        smallint has_empty_slot;
        smallint o_assignment; /* 0:maybe, 1:yes, 2:no */
 } o_string;
@@ -330,20 +480,10 @@ struct command {
        smallint cmd_type;          /* CMD_xxx */
 #define CMD_NORMAL   0
 #define CMD_SUBSHELL 1
-
-/* used for "[[ EXPR ]]" */
 #if ENABLE_HUSH_BASH_COMPAT
+/* used for "[[ EXPR ]]" */
 # define CMD_SINGLEWORD_NOGLOB 2
 #endif
-
-/* used for "export noglob=* glob* a=`echo a b`" */
-//#define CMD_SINGLEWORD_NOGLOB_COND 3
-// It is hard to implement correctly, it adds significant amounts of tricky code,
-// and all this is only useful for really obscure export statements
-// almost nobody would use anyway. #ifdef CMD_SINGLEWORD_NOGLOB_COND
-// guards the code which implements it, but I have doubts it works
-// in all cases (especially with mixed globbed/non-globbed arguments)
-
 #if ENABLE_HUSH_FUNCTIONS
 # define CMD_FUNCDEF 3
 #endif
@@ -531,8 +671,13 @@ struct globals {
         */
        smallint flag_return_in_progress;
 #endif
-       smallint fake_mode;
+       smallint n_mode;
+#if ENABLE_HUSH_MODE_X
        smallint x_mode;
+# define G_x_mode (G.x_mode)
+#else
+# define G_x_mode 0
+#endif
        smallint exiting; /* used to prevent EXIT trap recursion */
        /* These four support $?, $#, and $1 */
        smalluint last_exitcode;
@@ -553,6 +698,7 @@ struct globals {
        const char *cwd;
        struct variable *top_var; /* = &G.shell_ver (set in main()) */
        struct variable shell_ver;
+       char **expanded_assignments;
 #if ENABLE_HUSH_FUNCTIONS
        struct function *top_func;
 # if ENABLE_HUSH_LOCAL
@@ -1203,11 +1349,14 @@ static void hush_exit(int exitcode)
                /* Prevent recursion:
                 * trap "echo Hi; exit" EXIT; exit
                 */
-               char *argv[] = { NULL, G.traps[0], NULL };
+               char *argv[3];
+               /* argv[0] is unused */
+               argv[1] = G.traps[0];
+               argv[2] = NULL;
                G.traps[0] = NULL;
                G.exiting = 1;
                builtin_eval(argv);
-               free(argv[1]);
+               /* free(argv[1]); - why bother */
        }
 
 #if ENABLE_HUSH_JOB
@@ -1235,10 +1384,12 @@ static int check_and_run_traps(int sig)
                if (G.traps && G.traps[sig]) {
                        if (G.traps[sig][0]) {
                                /* We have user-defined handler */
-                               char *argv[] = { NULL, xstrdup(G.traps[sig]), NULL };
+                               char *argv[3];
+                               /* argv[0] is unused */
+                               argv[1] = G.traps[sig];
+                               argv[2] = NULL;
                                save_rcode = G.last_exitcode;
                                builtin_eval(argv);
-                               free(argv[1]);
                                G.last_exitcode = save_rcode;
                        } /* else: "" trap, ignoring signal */
                        continue;
@@ -1298,13 +1449,11 @@ static const char *get_cwd(int force)
 /*
  * Shell and environment variable support
  */
-static struct variable **get_ptr_to_local_var(const char *name)
+static struct variable **get_ptr_to_local_var(const char *name, unsigned len)
 {
        struct variable **pp;
        struct variable *cur;
-       int len;
 
-       len = strlen(name);
        pp = &G.top_var;
        while ((cur = *pp) != NULL) {
                if (strncmp(cur->varstr, name, len) == 0 && cur->varstr[len] == '=')
@@ -1314,26 +1463,31 @@ static struct variable **get_ptr_to_local_var(const char *name)
        return NULL;
 }
 
-static struct variable *get_local_var(const char *name)
-{
-       struct variable **pp = get_ptr_to_local_var(name);
-       if (pp)
-               return *pp;
-       return NULL;
-}
-
 static const char* FAST_FUNC get_local_var_value(const char *name)
 {
-       struct variable **pp = get_ptr_to_local_var(name);
-       if (pp)
-               return strchr((*pp)->varstr, '=') + 1;
+       struct variable **vpp;
+       unsigned len = strlen(name);
+
+       if (G.expanded_assignments) {
+               char **cpp = G.expanded_assignments;
+               while (*cpp) {
+                       char *cp = *cpp;
+                       if (strncmp(cp, name, len) == 0 && cp[len] == '=')
+                               return cp + len + 1;
+                       cpp++;
+               }
+       }
+
+       vpp = get_ptr_to_local_var(name, len);
+       if (vpp)
+               return (*vpp)->varstr + len + 1;
+
        if (strcmp(name, "PPID") == 0)
                return utoa(G.root_ppid);
        // bash compat: UID? EUID?
 #if ENABLE_HUSH_RANDOM_SUPPORT
-       if (strcmp(name, "RANDOM") == 0) {
+       if (strcmp(name, "RANDOM") == 0)
                return utoa(next_random(&G.random_gen));
-       }
 #endif
        return NULL;
 }
@@ -1522,24 +1676,6 @@ static void unset_vars(char **strings)
        free(strings);
 }
 
-#if ENABLE_SH_MATH_SUPPORT
-# define is_name(c)      ((c) == '_' || isalpha((unsigned char)(c)))
-# define is_in_name(c)   ((c) == '_' || isalnum((unsigned char)(c)))
-static char* FAST_FUNC endofname(const char *name)
-{
-       char *p;
-
-       p = (char *) name;
-       if (!is_name(*p))
-               return p;
-       while (*++p) {
-               if (!is_in_name(*p))
-                       break;
-       }
-       return p;
-}
-#endif
-
 static void FAST_FUNC set_local_var_from_halves(const char *name, const char *val)
 {
        char *var = xasprintf("%s=%s", name, val);
@@ -1583,9 +1719,7 @@ static struct variable *set_vars_and_save_old(char **strings)
 
                eq = strchr(*s, '=');
                if (eq) {
-                       *eq = '\0';
-                       var_pp = get_ptr_to_local_var(*s);
-                       *eq = '=';
+                       var_pp = get_ptr_to_local_var(*s, eq - *s);
                        if (var_pp) {
                                /* Remove variable from global linked list */
                                var_p = *var_pp;
@@ -1637,7 +1771,7 @@ static void cmdedit_update_prompt(void)
                G.PS2 = "> ";
 }
 
-static const charsetup_prompt_string(int promptmode)
+static const char *setup_prompt_string(int promptmode)
 {
        const char *prompt_str;
        debug_printf("setup_prompt_string %d ", promptmode);
@@ -1788,7 +1922,7 @@ static void setup_string_in_str(struct in_str *i, const char *s)
 static void o_reset_to_empty_unquoted(o_string *o)
 {
        o->length = 0;
-       o->o_quoted = 0;
+       o->has_quoted_part = 0;
        if (o->data)
                o->data[0] = '\0';
 }
@@ -1852,13 +1986,17 @@ static void o_addstr_with_NUL(o_string *o, const char *str)
 static void o_addblock_duplicate_backslash(o_string *o, const char *str, int len)
 {
        while (len) {
+               len--;
                o_addchr(o, *str);
-               if (*str++ == '\\'
-                && (*str != '*' && *str != '?' && *str != '[')
-               ) {
+               if (*str++ == '\\') {
+                       /* \z -> \\\z; \<eol> -> \\<eol> */
                        o_addchr(o, '\\');
+                       if (len) {
+                               len--;
+                               o_addchr(o, '\\');
+                               o_addchr(o, *str++);
+                       }
                }
-               len--;
        }
 }
 
@@ -1902,7 +2040,9 @@ static void o_addqchr(o_string *o, int ch)
 static void o_addQchr(o_string *o, int ch)
 {
        int sz = 1;
-       if (o->o_escape && strchr("*?[\\" MAYBE_BRACES, ch)) {
+       if ((o->o_expflags & EXP_FLAG_ESC_GLOB_CHARS)
+        && strchr("*?[\\" MAYBE_BRACES, ch)
+       ) {
                sz++;
                o->data[o->length] = '\\';
                o->length++;
@@ -1913,12 +2053,8 @@ static void o_addQchr(o_string *o, int ch)
        o->data[o->length] = '\0';
 }
 
-static void o_addQstr(o_string *o, const char *str, int len)
+static void o_addqblock(o_string *o, const char *str, int len)
 {
-       if (!o->o_escape) {
-               o_addblock(o, str, len);
-               return;
-       }
        while (len) {
                char ch;
                int sz;
@@ -1945,6 +2081,20 @@ static void o_addQstr(o_string *o, const char *str, int len)
        }
 }
 
+static void o_addQblock(o_string *o, const char *str, int len)
+{
+       if (!(o->o_expflags & EXP_FLAG_ESC_GLOB_CHARS)) {
+               o_addblock(o, str, len);
+               return;
+       }
+       o_addqblock(o, str, len);
+}
+
+static void o_addQstr(o_string *o, const char *str)
+{
+       o_addQblock(o, str, strlen(str));
+}
+
 /* A special kind of o_string for $VAR and `cmd` expansion.
  * It contains char* list[] at the beginning, which is grown in 16 element
  * increments. Actual string data starts at the next multiple of 16 * (char*).
@@ -1963,8 +2113,11 @@ static void debug_print_list(const char *prefix, o_string *o, int n)
        int i = 0;
 
        indent();
-       fprintf(stderr, "%s: list:%p n:%d string_start:%d length:%d maxlen:%d\n",
-                       prefix, list, n, string_start, o->length, o->maxlen);
+       fprintf(stderr, "%s: list:%p n:%d string_start:%d length:%d maxlen:%d glob:%d quoted:%d escape:%d\n",
+                       prefix, list, n, string_start, o->length, o->maxlen,
+                       !!(o->o_expflags & EXP_FLAG_GLOB),
+                       o->has_quoted_part,
+                       !!(o->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
        while (i < n) {
                indent();
                fprintf(stderr, " list[%d]=%d '%s' %p\n", i, (int)list[i],
@@ -2014,7 +2167,7 @@ static int o_save_ptr_helper(o_string *o, int n)
                                n, string_len, string_start);
                o->has_empty_slot = 0;
        }
-       list[n] = (char*)(ptrdiff_t)string_len;
+       list[n] = (char*)(uintptr_t)string_len;
        return n + 1;
 }
 
@@ -2024,7 +2177,7 @@ static int o_get_last_ptr(o_string *o, int n)
        char **list = (char**)o->data;
        int string_start = ((n + 0xf) & ~0xf) * sizeof(list[0]);
 
-       return ((int)(ptrdiff_t)list[n-1]) + string_start;
+       return ((int)(uintptr_t)list[n-1]) + string_start;
 }
 
 #ifdef HUSH_BRACE_EXP
@@ -2062,9 +2215,9 @@ static const char *next_brace_sub(const char *cp)
                        cp++;
                        continue;
                }
-                /*{*/ if ((*cp == '}' && depth-- == 0) || (*cp == ',' && depth == 0))
+               if ((*cp == '}' && depth-- == 0) || (*cp == ',' && depth == 0))
                        break;
-               if (*cp++ == '{') /*}*/
+               if (*cp++ == '{')
                        depth++;
        }
 
@@ -2086,7 +2239,7 @@ static int glob_brace(char *pattern, o_string *o, int n)
        while (1) {
                if (*begin == '\0')
                        goto simple_glob;
-               if (*begin == '{') /*}*/ {
+               if (*begin == '{') {
                        /* Find the first sub-pattern and at the same time
                         * find the rest after the closing brace */
                        next = next_brace_sub(begin);
@@ -2094,7 +2247,7 @@ static int glob_brace(char *pattern, o_string *o, int n)
                                /* An illegal expression */
                                goto simple_glob;
                        }
-                       /*{*/ if (*next == '}') {
+                       if (*next == '}') {
                                /* "{abc}" with no commas - illegal
                                 * brace expr, disregard and skip it */
                                begin = next + 1;
@@ -2111,7 +2264,7 @@ static int glob_brace(char *pattern, o_string *o, int n)
 
        /* Now find the end of the whole brace expression */
        rest = next;
-       /*{*/ while (*rest != '}') {
+       while (*rest != '}') {
                rest = next_brace_sub(rest);
                if (rest == NULL) {
                        /* An illegal expression */
@@ -2147,7 +2300,7 @@ static int glob_brace(char *pattern, o_string *o, int n)
                 * That's why we re-copy prefix every time (1st memcpy above).
                 */
                n = glob_brace(new_pattern_buf, o, n);
-               /*{*/ if (*next == '}') {
+               if (*next == '}') {
                        /* We saw the last entry */
                        break;
                }
@@ -2197,11 +2350,11 @@ static int glob_brace(char *pattern, o_string *o, int n)
 /* Performs globbing on last list[],
  * saving each result as a new list[].
  */
-static int o_glob(o_string *o, int n)
+static int perform_glob(o_string *o, int n)
 {
        char *pattern, *copy;
 
-       debug_printf_glob("start o_glob: n:%d o->data:%p\n", n, o->data);
+       debug_printf_glob("start perform_glob: n:%d o->data:%p\n", n, o->data);
        if (!o->data)
                return o_save_ptr_helper(o, n);
        pattern = o->data + o_get_last_ptr(o, n);
@@ -2219,7 +2372,7 @@ static int o_glob(o_string *o, int n)
        n = glob_brace(copy, o, n);
        free(copy);
        if (DEBUG_GLOB)
-               debug_print_list("o_glob returning", o, n);
+               debug_print_list("perform_glob returning", o, n);
        return n;
 }
 
@@ -2244,13 +2397,13 @@ static int glob_needed(const char *s)
 /* Performs globbing on last list[],
  * saving each result as a new list[].
  */
-static int o_glob(o_string *o, int n)
+static int perform_glob(o_string *o, int n)
 {
        glob_t globdata;
        int gr;
        char *pattern;
 
-       debug_printf_glob("start o_glob: n:%d o->data:%p\n", n, o->data);
+       debug_printf_glob("start perform_glob: n:%d o->data:%p\n", n, o->data);
        if (!o->data)
                return o_save_ptr_helper(o, n);
        pattern = o->data + o_get_last_ptr(o, n);
@@ -2296,22 +2449,22 @@ static int o_glob(o_string *o, int n)
        }
        globfree(&globdata);
        if (DEBUG_GLOB)
-               debug_print_list("o_glob returning", o, n);
+               debug_print_list("perform_glob returning", o, n);
        return n;
 }
 
 #endif /* !HUSH_BRACE_EXP */
 
-/* If o->o_glob == 1, glob the string so far remembered.
+/* If o->o_expflags & EXP_FLAG_GLOB, glob the string so far remembered.
  * Otherwise, just finish current list[] and start new */
 static int o_save_ptr(o_string *o, int n)
 {
-       if (o->o_glob) { /* if globbing is requested */
+       if (o->o_expflags & EXP_FLAG_GLOB) {
                /* If o->has_empty_slot, list[n] was already globbed
                 * (if it was requested back then when it was filled)
                 * so don't do that again! */
                if (!o->has_empty_slot)
-                       return o_glob(o, n); /* o_save_ptr_helper is inside */
+                       return perform_glob(o, n); /* o_save_ptr_helper is inside */
        }
        return o_save_ptr_helper(o, n);
 }
@@ -2331,1079 +2484,762 @@ static char **o_finalize_list(o_string *o, int n)
        list[--n] = NULL;
        while (n) {
                n--;
-               list[n] = o->data + (int)(ptrdiff_t)list[n] + string_start;
+               list[n] = o->data + (int)(uintptr_t)list[n] + string_start;
        }
        return list;
 }
 
+static void free_pipe_list(struct pipe *pi);
 
-/* Expansion can recurse */
-#if ENABLE_HUSH_TICK
-static int process_command_subs(o_string *dest, const char *s);
-#endif
-static char *expand_string_to_string(const char *str);
-#if BB_MMU
-#define parse_stream_dquoted(as_string, dest, input, dquote_end) \
-       parse_stream_dquoted(dest, input, dquote_end)
-#endif
-static int parse_stream_dquoted(o_string *as_string,
-               o_string *dest,
-               struct in_str *input,
-               int dquote_end);
+/* Returns pi->next - next pipe in the list */
+static struct pipe *free_pipe(struct pipe *pi)
+{
+       struct pipe *next;
+       int i;
 
-/* expand_strvec_to_strvec() takes a list of strings, expands
- * all variable references within and returns a pointer to
- * a list of expanded strings, possibly with larger number
- * of strings. (Think VAR="a b"; echo $VAR).
- * This new list is allocated as a single malloc block.
- * NULL-terminated list of char* pointers is at the beginning of it,
- * followed by strings themself.
- * Caller can deallocate entire list by single free(list). */
+       debug_printf_clean("free_pipe (pid %d)\n", getpid());
+       for (i = 0; i < pi->num_cmds; i++) {
+               struct command *command;
+               struct redir_struct *r, *rnext;
 
-/* Store given string, finalizing the word and starting new one whenever
- * we encounter IFS char(s). This is used for expanding variable values.
- * End-of-string does NOT finalize word: think about 'echo -$VAR-' */
-static int expand_on_ifs(o_string *output, int n, const char *str)
-{
-       while (1) {
-               int word_len = strcspn(str, G.ifs);
-               if (word_len) {
-                       if (output->o_escape || !output->o_glob)
-                               o_addQstr(output, str, word_len);
-                       else /* protect backslashes against globbing up :) */
-                               o_addblock_duplicate_backslash(output, str, word_len);
-                       str += word_len;
+               command = &pi->cmds[i];
+               debug_printf_clean("  command %d:\n", i);
+               if (command->argv) {
+                       if (DEBUG_CLEAN) {
+                               int a;
+                               char **p;
+                               for (a = 0, p = command->argv; *p; a++, p++) {
+                                       debug_printf_clean("   argv[%d] = %s\n", a, *p);
+                               }
+                       }
+                       free_strings(command->argv);
+                       //command->argv = NULL;
                }
-               if (!*str)  /* EOL - do not finalize word */
-                       break;
-               o_addchr(output, '\0');
-               debug_print_list("expand_on_ifs", output, n);
-               n = o_save_ptr(output, n);
-               str += strspn(str, G.ifs); /* skip ifs chars */
+               /* not "else if": on syntax error, we may have both! */
+               if (command->group) {
+                       debug_printf_clean("   begin group (cmd_type:%d)\n",
+                                       command->cmd_type);
+                       free_pipe_list(command->group);
+                       debug_printf_clean("   end group\n");
+                       //command->group = NULL;
+               }
+               /* else is crucial here.
+                * If group != NULL, child_func is meaningless */
+#if ENABLE_HUSH_FUNCTIONS
+               else if (command->child_func) {
+                       debug_printf_exec("cmd %p releases child func at %p\n", command, command->child_func);
+                       command->child_func->parent_cmd = NULL;
+               }
+#endif
+#if !BB_MMU
+               free(command->group_as_string);
+               //command->group_as_string = NULL;
+#endif
+               for (r = command->redirects; r; r = rnext) {
+                       debug_printf_clean("   redirect %d%s",
+                                       r->rd_fd, redir_table[r->rd_type].descrip);
+                       /* guard against the case >$FOO, where foo is unset or blank */
+                       if (r->rd_filename) {
+                               debug_printf_clean(" fname:'%s'\n", r->rd_filename);
+                               free(r->rd_filename);
+                               //r->rd_filename = NULL;
+                       }
+                       debug_printf_clean(" rd_dup:%d\n", r->rd_dup);
+                       rnext = r->next;
+                       free(r);
+               }
+               //command->redirects = NULL;
        }
-       debug_print_list("expand_on_ifs[1]", output, n);
-       return n;
+       free(pi->cmds);   /* children are an array, they get freed all at once */
+       //pi->cmds = NULL;
+#if ENABLE_HUSH_JOB
+       free(pi->cmdtext);
+       //pi->cmdtext = NULL;
+#endif
+
+       next = pi->next;
+       free(pi);
+       return next;
 }
 
-/* Helper to expand $((...)) and heredoc body. These act as if
- * they are in double quotes, with the exception that they are not :).
- * Just the rules are similar: "expand only $var and `cmd`"
- *
- * Returns malloced string.
- * As an optimization, we return NULL if expansion is not needed.
- */
-static char *expand_pseudo_dquoted(const char *str)
+static void free_pipe_list(struct pipe *pi)
 {
-       char *exp_str;
-       struct in_str input;
-       o_string dest = NULL_O_STRING;
-
-       if (strchr(str, '$') == NULL
-#if ENABLE_HUSH_TICK
-        && strchr(str, '`') == NULL
+       while (pi) {
+#if HAS_KEYWORDS
+               debug_printf_clean("pipe reserved word %d\n", pi->res_word);
 #endif
-       ) {
-               return NULL;
+               debug_printf_clean("pipe followup code %d\n", pi->followup);
+               pi = free_pipe(pi);
        }
+}
 
-       /* We need to expand. Example:
-        * echo $(($a + `echo 1`)) $((1 + $((2)) ))
-        */
-       setup_string_in_str(&input, str);
-       parse_stream_dquoted(NULL, &dest, &input, EOF);
-       //bb_error_msg("'%s' -> '%s'", str, dest.data);
-       exp_str = expand_string_to_string(dest.data);
-       //bb_error_msg("'%s' -> '%s'", dest.data, exp_str);
-       o_free_unsafe(&dest);
-       return exp_str;
+
+/*** Parsing routines ***/
+
+static struct pipe *new_pipe(void)
+{
+       struct pipe *pi;
+       pi = xzalloc(sizeof(struct pipe));
+       /*pi->followup = 0; - deliberately invalid value */
+       /*pi->res_word = RES_NONE; - RES_NONE is 0 anyway */
+       return pi;
 }
 
-#if ENABLE_SH_MATH_SUPPORT
-static arith_t expand_and_evaluate_arith(const char *arg, int *errcode_p)
+/* Command (member of a pipe) is complete, or we start a new pipe
+ * if ctx->command is NULL.
+ * No errors possible here.
+ */
+static int done_command(struct parse_context *ctx)
 {
-       arith_eval_hooks_t hooks;
-       arith_t res;
-       char *exp_str;
+       /* The command is really already in the pipe structure, so
+        * advance the pipe counter and make a new, null command. */
+       struct pipe *pi = ctx->pipe;
+       struct command *command = ctx->command;
 
-       hooks.lookupvar = get_local_var_value;
-       hooks.setvar = set_local_var_from_halves;
-       hooks.endofname = endofname;
-       exp_str = expand_pseudo_dquoted(arg);
-       res = arith(exp_str ? exp_str : arg, errcode_p, &hooks);
-       free(exp_str);
-       return res;
+       if (command) {
+               if (IS_NULL_CMD(command)) {
+                       debug_printf_parse("done_command: skipping null cmd, num_cmds=%d\n", pi->num_cmds);
+                       goto clear_and_ret;
+               }
+               pi->num_cmds++;
+               debug_printf_parse("done_command: ++num_cmds=%d\n", pi->num_cmds);
+               //debug_print_tree(ctx->list_head, 20);
+       } else {
+               debug_printf_parse("done_command: initializing, num_cmds=%d\n", pi->num_cmds);
+       }
+
+       /* Only real trickiness here is that the uncommitted
+        * command structure is not counted in pi->num_cmds. */
+       pi->cmds = xrealloc(pi->cmds, sizeof(*pi->cmds) * (pi->num_cmds+1));
+       ctx->command = command = &pi->cmds[pi->num_cmds];
+ clear_and_ret:
+       memset(command, 0, sizeof(*command));
+       return pi->num_cmds; /* used only for 0/nonzero check */
 }
-#endif
 
-/* Expand all variable references in given string, adding words to list[]
- * at n, n+1,... positions. Return updated n (so that list[n] is next one
- * to be filled). This routine is extremely tricky: has to deal with
- * variables/parameters with whitespace, $* and $@, and constructs like
- * 'echo -$*-'. If you play here, you must run testsuite afterwards! */
-static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char or_mask)
+static void done_pipe(struct parse_context *ctx, pipe_style type)
 {
-       /* or_mask is either 0 (normal case) or 0x80 -
-        * expansion of right-hand side of assignment == 1-element expand.
-        * It will also do no globbing, and thus we must not backslash-quote!
-        */
-       char ored_ch;
-       char *p;
-
-       ored_ch = 0;
+       int not_null;
 
-       debug_printf_expand("expand_vars_to_list: arg:'%s' or_mask:%x\n", arg, or_mask);
-       debug_print_list("expand_vars_to_list", output, n);
-       n = o_save_ptr(output, n);
-       debug_print_list("expand_vars_to_list[0]", output, n);
+       debug_printf_parse("done_pipe entered, followup %d\n", type);
+       /* Close previous command */
+       not_null = done_command(ctx);
+       ctx->pipe->followup = type;
+#if HAS_KEYWORDS
+       ctx->pipe->pi_inverted = ctx->ctx_inverted;
+       ctx->ctx_inverted = 0;
+       ctx->pipe->res_word = ctx->ctx_res_w;
+#endif
 
-       while ((p = strchr(arg, SPECIAL_VAR_SYMBOL)) != NULL) {
-               char first_ch;
-               int i;
-               char *to_be_freed = NULL;
-               const char *val = NULL;
-#if ENABLE_HUSH_TICK
-               o_string subst_result = NULL_O_STRING;
+       /* Without this check, even just <enter> on command line generates
+        * tree of three NOPs (!). Which is harmless but annoying.
+        * IOW: it is safe to do it unconditionally. */
+       if (not_null
+#if ENABLE_HUSH_IF
+        || ctx->ctx_res_w == RES_FI
 #endif
-#if ENABLE_SH_MATH_SUPPORT
-               char arith_buf[sizeof(arith_t)*3 + 2];
-#endif
-               o_addblock(output, arg, p - arg);
-               debug_print_list("expand_vars_to_list[1]", output, n);
-               arg = ++p;
-               p = strchr(p, SPECIAL_VAR_SYMBOL);
-
-               first_ch = arg[0] | or_mask; /* forced to "quoted" if or_mask = 0x80 */
-               /* "$@" is special. Even if quoted, it can still
-                * expand to nothing (not even an empty string) */
-               if ((first_ch & 0x7f) != '@')
-                       ored_ch |= first_ch;
-
-               switch (first_ch & 0x7f) {
-               /* Highest bit in first_ch indicates that var is double-quoted */
-               case '*':
-               case '@':
-                       i = 1;
-                       if (!G.global_argv[i])
-                               break;
-                       ored_ch |= first_ch; /* do it for "$@" _now_, when we know it's not empty */
-                       if (!(first_ch & 0x80)) { /* unquoted $* or $@ */
-                               smallint sv = output->o_escape;
-                               /* unquoted var's contents should be globbed, so don't escape */
-                               output->o_escape = 0;
-                               while (G.global_argv[i]) {
-                                       n = expand_on_ifs(output, n, G.global_argv[i]);
-                                       debug_printf_expand("expand_vars_to_list: argv %d (last %d)\n", i, G.global_argc - 1);
-                                       if (G.global_argv[i++][0] && G.global_argv[i]) {
-                                               /* this argv[] is not empty and not last:
-                                                * put terminating NUL, start new word */
-                                               o_addchr(output, '\0');
-                                               debug_print_list("expand_vars_to_list[2]", output, n);
-                                               n = o_save_ptr(output, n);
-                                               debug_print_list("expand_vars_to_list[3]", output, n);
-                                       }
-                               }
-                               output->o_escape = sv;
-                       } else
-                       /* If or_mask is nonzero, we handle assignment 'a=....$@.....'
-                        * and in this case should treat it like '$*' - see 'else...' below */
-                       if (first_ch == ('@'|0x80) && !or_mask) { /* quoted $@ */
-                               while (1) {
-                                       o_addQstr(output, G.global_argv[i], strlen(G.global_argv[i]));
-                                       if (++i >= G.global_argc)
-                                               break;
-                                       o_addchr(output, '\0');
-                                       debug_print_list("expand_vars_to_list[4]", output, n);
-                                       n = o_save_ptr(output, n);
-                               }
-                       } else { /* quoted $*: add as one word */
-                               while (1) {
-                                       o_addQstr(output, G.global_argv[i], strlen(G.global_argv[i]));
-                                       if (!G.global_argv[++i])
-                                               break;
-                                       if (G.ifs[0])
-                                               o_addchr(output, G.ifs[0]);
-                               }
-                       }
-                       break;
-               case SPECIAL_VAR_SYMBOL: /* <SPECIAL_VAR_SYMBOL><SPECIAL_VAR_SYMBOL> */
-                       /* "Empty variable", used to make "" etc to not disappear */
-                       arg++;
-                       ored_ch = 0x80;
-                       break;
-#if ENABLE_HUSH_TICK
-               case '`': /* <SPECIAL_VAR_SYMBOL>`cmd<SPECIAL_VAR_SYMBOL> */
-                       *p = '\0';
-                       arg++;
-                       /* Can't just stuff it into output o_string,
-                        * expanded result may need to be globbed
-                        * and $IFS-splitted */
-                       debug_printf_subst("SUBST '%s' first_ch %x\n", arg, first_ch);
-                       G.last_exitcode = process_command_subs(&subst_result, arg);
-                       debug_printf_subst("SUBST RES:%d '%s'\n", G.last_exitcode, subst_result.data);
-                       val = subst_result.data;
-                       goto store_val;
-#endif
-#if ENABLE_SH_MATH_SUPPORT
-               case '+': { /* <SPECIAL_VAR_SYMBOL>+cmd<SPECIAL_VAR_SYMBOL> */
-                       arith_t res;
-                       int errcode;
-
-                       arg++; /* skip '+' */
-                       *p = '\0'; /* replace trailing <SPECIAL_VAR_SYMBOL> */
-                       debug_printf_subst("ARITH '%s' first_ch %x\n", arg, first_ch);
-                       res = expand_and_evaluate_arith(arg, &errcode);
-
-                       if (errcode < 0) {
-                               const char *msg = "error in arithmetic";
-                               switch (errcode) {
-                               case -3:
-                                       msg = "exponent less than 0";
-                                       break;
-                               case -2:
-                                       msg = "divide by 0";
-                                       break;
-                               case -5:
-                                       msg = "expression recursion loop detected";
-                                       break;
-                               }
-                               die_if_script(msg);
-                       }
-                       debug_printf_subst("ARITH RES '"arith_t_fmt"'\n", res);
-                       sprintf(arith_buf, arith_t_fmt, res);
-                       val = arith_buf;
-                       break;
-               }
+#if ENABLE_HUSH_LOOPS
+        || ctx->ctx_res_w == RES_DONE
+        || ctx->ctx_res_w == RES_FOR
+        || ctx->ctx_res_w == RES_IN
 #endif
-               default: { /* <SPECIAL_VAR_SYMBOL>varname<SPECIAL_VAR_SYMBOL> */
-                       char *var;
-                       char first_char;
-                       char exp_op;
-                       char exp_save = exp_save; /* for compiler */
-                       char *exp_saveptr; /* points to expansion operator */
-                       char *exp_word = exp_word; /* for compiler */
-
-                       var = arg;
-                       *p = '\0';
-                       exp_saveptr = arg[1] ? strchr("%#:-=+?", arg[1]) : NULL;
-                       first_char = arg[0] = first_ch & 0x7f;
-                       exp_op = 0;
-
-                       if (first_char == '#' && arg[1] && !exp_saveptr) {
-                               /* handle length expansion ${#var} */
-                               var++;
-                               exp_op = 'L';
-                       } else {
-                               /* maybe handle parameter expansion */
-                               if (exp_saveptr /* if 2nd char is one of expansion operators */
-                                && strchr(NUMERIC_SPECVARS_STR, first_char) /* 1st char is special variable */
-                               ) {
-                                       /* ${?:0}, ${#[:]%0} etc */
-                                       exp_saveptr = var + 1;
-                               } else {
-                                       /* ${?}, ${var}, ${var:0}, ${var[:]%0} etc */
-                                       exp_saveptr = var+1 + strcspn(var+1, "%#:-=+?");
-                               }
-                               exp_op = exp_save = *exp_saveptr;
-                               if (exp_op) {
-                                       exp_word = exp_saveptr + 1;
-                                       if (exp_op == ':') {
-                                               exp_op = *exp_word++;
-                                               if (ENABLE_HUSH_BASH_COMPAT
-                                                && (exp_op == '\0' || !strchr("%#:-=+?"+3, exp_op))
-                                               ) {
-                                                       /* oops... it's ${var:N[:M]}, not ${var:?xxx} or some such */
-                                                       exp_op = ':';
-                                                       exp_word--;
-                                               }
-                                       }
-                                       *exp_saveptr = '\0';
-                               } /* else: it's not an expansion op, but bare ${var} */
-                       }
-
-                       /* lookup the variable in question */
-                       if (isdigit(var[0])) {
-                               /* parse_dollar() should have vetted var for us */
-                               i = xatoi_u(var);
-                               if (i < G.global_argc)
-                                       val = G.global_argv[i];
-                               /* else val remains NULL: $N with too big N */
-                       } else {
-                               switch (var[0]) {
-                               case '$': /* pid */
-                                       val = utoa(G.root_pid);
-                                       break;
-                               case '!': /* bg pid */
-                                       val = G.last_bg_pid ? utoa(G.last_bg_pid) : (char*)"";
-                                       break;
-                               case '?': /* exitcode */
-                                       val = utoa(G.last_exitcode);
-                                       break;
-                               case '#': /* argc */
-                                       val = utoa(G.global_argc ? G.global_argc-1 : 0);
-                                       break;
-                               default:
-                                       val = get_local_var_value(var);
-                               }
-                       }
-
-                       /* handle any expansions */
-                       if (exp_op == 'L') {
-                               debug_printf_expand("expand: length(%s)=", val);
-                               val = utoa(val ? strlen(val) : 0);
-                               debug_printf_expand("%s\n", val);
-                       } else if (exp_op) {
-                               if (exp_op == '%' || exp_op == '#') {
-       /* Standard-mandated substring removal ops:
-        * ${parameter%word} - remove smallest suffix pattern
-        * ${parameter%%word} - remove largest suffix pattern
-        * ${parameter#word} - remove smallest prefix pattern
-        * ${parameter##word} - remove largest prefix pattern
-        *
-        * Word is expanded to produce a glob pattern.
-        * Then var's value is matched to it and matching part removed.
-        */
-                                       if (val) {
-                                               bool match_at_left;
-                                               char *loc;
-                                               scan_t scan = pick_scan(exp_op, *exp_word, &match_at_left);
-                                               if (exp_op == *exp_word)        /* ## or %% */
-                                                       exp_word++;
-                                               val = to_be_freed = xstrdup(val);
-                                               {
-                                                       char *exp_exp_word = expand_pseudo_dquoted(exp_word);
-                                                       if (exp_exp_word)
-                                                               exp_word = exp_exp_word;
-                                                       loc = scan(to_be_freed, exp_word, match_at_left);
-                                                       //bb_error_msg("op:%c str:'%s' pat:'%s' res:'%s'",
-                                                       //              exp_op, to_be_freed, exp_word, loc);
-                                                       free(exp_exp_word);
-                                               }
-                                               if (loc) { /* match was found */
-                                                       if (match_at_left) /* # or ## */
-                                                               val = loc;
-                                                       else /* % or %% */
-                                                               *loc = '\0';
-                                               }
-                                       }
-                               } else if (exp_op == ':') {
-#if ENABLE_HUSH_BASH_COMPAT && ENABLE_SH_MATH_SUPPORT
-       /* It's ${var:N[:M]} bashism.
-        * Note that in encoded form it has TWO parts:
-        * var:N<SPECIAL_VAR_SYMBOL>M<SPECIAL_VAR_SYMBOL>
-        */
-                                       arith_t beg, len;
-                                       int errcode = 0;
-
-                                       beg = expand_and_evaluate_arith(exp_word, &errcode);
-                                       debug_printf_varexp("beg:'%s'=%lld\n", exp_word, (long long)beg);
-                                       *p++ = SPECIAL_VAR_SYMBOL;
-                                       exp_word = p;
-                                       p = strchr(p, SPECIAL_VAR_SYMBOL);
-                                       *p = '\0';
-                                       len = expand_and_evaluate_arith(exp_word, &errcode);
-                                       debug_printf_varexp("len:'%s'=%lld\n", exp_word, (long long)len);
-
-                                       if (errcode >= 0 && len >= 0) { /* bash compat: len < 0 is illegal */
-                                               if (beg < 0) /* bash compat */
-                                                       beg = 0;
-                                               debug_printf_varexp("from val:'%s'\n", val);
-                                               if (len == 0 || !val || beg >= strlen(val))
-                                                       val = "";
-                                               else {
-                                                       /* Paranoia. What if user entered 9999999999999
-                                                        * which fits in arith_t but not int? */
-                                                       if (len >= INT_MAX)
-                                                               len = INT_MAX;
-                                                       val = to_be_freed = xstrndup(val + beg, len);
-                                               }
-                                               debug_printf_varexp("val:'%s'\n", val);
-                                       } else
+#if ENABLE_HUSH_CASE
+        || ctx->ctx_res_w == RES_ESAC
 #endif
-                                       {
-                                               die_if_script("malformed ${%s:...}", var);
-                                               val = "";
-                                       }
-                               } else { /* one of "-=+?" */
-       /* Standard-mandated substitution ops:
-        * ${var?word} - indicate error if unset
-        *      If var is unset, word (or a message indicating it is unset
-        *      if word is null) is written to standard error
-        *      and the shell exits with a non-zero exit status.
-        *      Otherwise, the value of var is substituted.
-        * ${var-word} - use default value
-        *      If var is unset, word is substituted.
-        * ${var=word} - assign and use default value
-        *      If var is unset, word is assigned to var.
-        *      In all cases, final value of var is substituted.
-        * ${var+word} - use alternative value
-        *      If var is unset, null is substituted.
-        *      Otherwise, word is substituted.
-        *
-        * Word is subjected to tilde expansion, parameter expansion,
-        * command substitution, and arithmetic expansion.
-        * If word is not needed, it is not expanded.
-        *
-        * Colon forms (${var:-word}, ${var:=word} etc) do the same,
-        * but also treat null var as if it is unset.
-        */
-                                       int use_word = (!val || ((exp_save == ':') && !val[0]));
-                                       if (exp_op == '+')
-                                               use_word = !use_word;
-                                       debug_printf_expand("expand: op:%c (null:%s) test:%i\n", exp_op,
-                                               (exp_save == ':') ? "true" : "false", use_word);
-                                       if (use_word) {
-                                               to_be_freed = expand_pseudo_dquoted(exp_word);
-                                               if (to_be_freed)
-                                                       exp_word = to_be_freed;
-                                               if (exp_op == '?') {
-                                                       /* mimic bash message */
-                                                       die_if_script("%s: %s",
-                                                               var,
-                                                               exp_word[0] ? exp_word : "parameter null or not set"
-                                                       );
-//TODO: how interactive bash aborts expansion mid-command?
-                                               } else {
-                                                       val = exp_word;
-                                               }
-
-                                               if (exp_op == '=') {
-                                                       /* ${var=[word]} or ${var:=[word]} */
-                                                       if (isdigit(var[0]) || var[0] == '#') {
-                                                               /* mimic bash message */
-                                                               die_if_script("$%s: cannot assign in this way", var);
-                                                               val = NULL;
-                                                       } else {
-                                                               char *new_var = xasprintf("%s=%s", var, val);
-                                                               set_local_var(new_var, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
-                                                       }
-                                               }
-                                       }
-                               } /* one of "-=+?" */
-
-                               *exp_saveptr = exp_save;
-                       } /* if (exp_op) */
-
-                       arg[0] = first_ch;
-#if ENABLE_HUSH_TICK
- store_val:
+       ) {
+               struct pipe *new_p;
+               debug_printf_parse("done_pipe: adding new pipe: "
+                               "not_null:%d ctx->ctx_res_w:%d\n",
+                               not_null, ctx->ctx_res_w);
+               new_p = new_pipe();
+               ctx->pipe->next = new_p;
+               ctx->pipe = new_p;
+               /* RES_THEN, RES_DO etc are "sticky" -
+                * they remain set for pipes inside if/while.
+                * This is used to control execution.
+                * RES_FOR and RES_IN are NOT sticky (needed to support
+                * cases where variable or value happens to match a keyword):
+                */
+#if ENABLE_HUSH_LOOPS
+               if (ctx->ctx_res_w == RES_FOR
+                || ctx->ctx_res_w == RES_IN)
+                       ctx->ctx_res_w = RES_NONE;
 #endif
-                       if (!(first_ch & 0x80)) { /* unquoted $VAR */
-                               debug_printf_expand("unquoted '%s', output->o_escape:%d\n", val, output->o_escape);
-                               if (val) {
-                                       /* unquoted var's contents should be globbed, so don't escape */
-                                       smallint sv = output->o_escape;
-                                       output->o_escape = 0;
-                                       n = expand_on_ifs(output, n, val);
-                                       val = NULL;
-                                       output->o_escape = sv;
-                               }
-                       } else { /* quoted $VAR, val will be appended below */
-                               debug_printf_expand("quoted '%s', output->o_escape:%d\n", val, output->o_escape);
-                       }
-               } /* default: */
-               } /* switch (char after <SPECIAL_VAR_SYMBOL>) */
-
-               if (val) {
-                       o_addQstr(output, val, strlen(val));
-               }
-               free(to_be_freed);
-               /* Do the check to avoid writing to a const string */
-               if (*p != SPECIAL_VAR_SYMBOL)
-                       *p = SPECIAL_VAR_SYMBOL;
-
-#if ENABLE_HUSH_TICK
-               o_free(&subst_result);
+#if ENABLE_HUSH_CASE
+               if (ctx->ctx_res_w == RES_MATCH)
+                       ctx->ctx_res_w = RES_CASE_BODY;
+               if (ctx->ctx_res_w == RES_CASE)
+                       ctx->ctx_res_w = RES_CASE_IN;
 #endif
-               arg = ++p;
-       } /* end of "while (SPECIAL_VAR_SYMBOL is found) ..." */
-
-       if (arg[0]) {
-               debug_print_list("expand_vars_to_list[a]", output, n);
-               /* this part is literal, and it was already pre-quoted
-                * if needed (much earlier), do not use o_addQstr here! */
-               o_addstr_with_NUL(output, arg);
-               debug_print_list("expand_vars_to_list[b]", output, n);
-       } else if (output->length == o_get_last_ptr(output, n) /* expansion is empty */
-        && !(ored_ch & 0x80) /* and all vars were not quoted. */
-       ) {
-               n--;
-               /* allow to reuse list[n] later without re-growth */
-               output->has_empty_slot = 1;
-       } else {
-               o_addchr(output, '\0');
-       }
-       return n;
-}
-
-static char **expand_variables(char **argv, int or_mask)
-{
-       int n;
-       char **list;
-       char **v;
-       o_string output = NULL_O_STRING;
-
-       if (or_mask & 0x100) {
-               output.o_escape = 1; /* protect against globbing for "$var" */
-               /* (unquoted $var will temporarily switch it off) */
-               output.o_glob = 1;
-       }
-
-       n = 0;
-       v = argv;
-       while (*v) {
-               n = expand_vars_to_list(&output, n, *v, (unsigned char)or_mask);
-               v++;
+               ctx->command = NULL; /* trick done_command below */
+               /* Create the memory for command, roughly:
+                * ctx->pipe->cmds = new struct command;
+                * ctx->command = &ctx->pipe->cmds[0];
+                */
+               done_command(ctx);
+               //debug_print_tree(ctx->list_head, 10);
        }
-       debug_print_list("expand_variables", &output, n);
-
-       /* output.data (malloced in one block) gets returned in "list" */
-       list = o_finalize_list(&output, n);
-       debug_print_strings("expand_variables[1]", list);
-       return list;
-}
-
-static char **expand_strvec_to_strvec(char **argv)
-{
-       return expand_variables(argv, 0x100);
+       debug_printf_parse("done_pipe return\n");
 }
 
-#if ENABLE_HUSH_BASH_COMPAT
-static char **expand_strvec_to_strvec_singleword_noglob(char **argv)
+static void initialize_context(struct parse_context *ctx)
 {
-       return expand_variables(argv, 0x80);
+       memset(ctx, 0, sizeof(*ctx));
+       ctx->pipe = ctx->list_head = new_pipe();
+       /* Create the memory for command, roughly:
+        * ctx->pipe->cmds = new struct command;
+        * ctx->command = &ctx->pipe->cmds[0];
+        */
+       done_command(ctx);
 }
-#endif
-
-#ifdef CMD_SINGLEWORD_NOGLOB_COND
-static char **expand_strvec_to_strvec_singleword_noglob_cond(char **argv)
-{
-       int n;
-       char **list;
-       char **v;
-       o_string output = NULL_O_STRING;
-
-       n = 0;
-       v = argv;
-       while (*v) {
-               int is_var = is_well_formed_var_name(*v, '=');
-               /* is_var * 0x80: singleword expansion for vars */
-               n = expand_vars_to_list(&output, n, *v, is_var * 0x80);
 
-               /* Subtle! expand_vars_to_list did not glob last word yet.
-                * It does this only when fed with further data.
-                * Therefore we set globbing flags AFTER it, not before:
-                */
-
-               /* if it is not recognizably abc=...; then: */
-               output.o_escape = !is_var; /* protect against globbing for "$var" */
-               /* (unquoted $var will temporarily switch it off) */
-               output.o_glob = !is_var; /* and indeed do globbing */
-               v++;
-       }
-       debug_print_list("expand_cond", &output, n);
-
-       /* output.data (malloced in one block) gets returned in "list" */
-       list = o_finalize_list(&output, n);
-       debug_print_strings("expand_cond[1]", list);
-       return list;
-}
-#endif
+/* If a reserved word is found and processed, parse context is modified
+ * and 1 is returned.
+ */
+#if HAS_KEYWORDS
+struct reserved_combo {
+       char literal[6];
+       unsigned char res;
+       unsigned char assignment_flag;
+       int flag;
+};
+enum {
+       FLAG_END   = (1 << RES_NONE ),
+# if ENABLE_HUSH_IF
+       FLAG_IF    = (1 << RES_IF   ),
+       FLAG_THEN  = (1 << RES_THEN ),
+       FLAG_ELIF  = (1 << RES_ELIF ),
+       FLAG_ELSE  = (1 << RES_ELSE ),
+       FLAG_FI    = (1 << RES_FI   ),
+# endif
+# if ENABLE_HUSH_LOOPS
+       FLAG_FOR   = (1 << RES_FOR  ),
+       FLAG_WHILE = (1 << RES_WHILE),
+       FLAG_UNTIL = (1 << RES_UNTIL),
+       FLAG_DO    = (1 << RES_DO   ),
+       FLAG_DONE  = (1 << RES_DONE ),
+       FLAG_IN    = (1 << RES_IN   ),
+# endif
+# if ENABLE_HUSH_CASE
+       FLAG_MATCH = (1 << RES_MATCH),
+       FLAG_ESAC  = (1 << RES_ESAC ),
+# endif
+       FLAG_START = (1 << RES_XXXX ),
+};
 
-/* Used for expansion of right hand of assignments */
-/* NB: should NOT do globbing! "export v=/bin/c*; env | grep ^v=" outputs
- * "v=/bin/c*" */
-static char *expand_string_to_string(const char *str)
+static const struct reserved_combo* match_reserved_word(o_string *word)
 {
-       char *argv[2], **list;
+       /* Mostly a list of accepted follow-up reserved words.
+        * FLAG_END means we are done with the sequence, and are ready
+        * to turn the compound list into a command.
+        * FLAG_START means the word must start a new compound list.
+        */
+       static const struct reserved_combo reserved_list[] = {
+# if ENABLE_HUSH_IF
+               { "!",     RES_NONE,  NOT_ASSIGNMENT , 0 },
+               { "if",    RES_IF,    WORD_IS_KEYWORD, FLAG_THEN | FLAG_START },
+               { "then",  RES_THEN,  WORD_IS_KEYWORD, FLAG_ELIF | FLAG_ELSE | FLAG_FI },
+               { "elif",  RES_ELIF,  WORD_IS_KEYWORD, FLAG_THEN },
+               { "else",  RES_ELSE,  WORD_IS_KEYWORD, FLAG_FI   },
+               { "fi",    RES_FI,    NOT_ASSIGNMENT , FLAG_END  },
+# endif
+# if ENABLE_HUSH_LOOPS
+               { "for",   RES_FOR,   NOT_ASSIGNMENT , FLAG_IN | FLAG_DO | FLAG_START },
+               { "while", RES_WHILE, WORD_IS_KEYWORD, FLAG_DO | FLAG_START },
+               { "until", RES_UNTIL, WORD_IS_KEYWORD, FLAG_DO | FLAG_START },
+               { "in",    RES_IN,    NOT_ASSIGNMENT , FLAG_DO   },
+               { "do",    RES_DO,    WORD_IS_KEYWORD, FLAG_DONE },
+               { "done",  RES_DONE,  NOT_ASSIGNMENT , FLAG_END  },
+# endif
+# if ENABLE_HUSH_CASE
+               { "case",  RES_CASE,  NOT_ASSIGNMENT , FLAG_MATCH | FLAG_START },
+               { "esac",  RES_ESAC,  NOT_ASSIGNMENT , FLAG_END  },
+# endif
+       };
+       const struct reserved_combo *r;
 
-       argv[0] = (char*)str;
-       argv[1] = NULL;
-       list = expand_variables(argv, 0x80); /* 0x80: singleword expansion */
-       if (HUSH_DEBUG)
-               if (!list[0] || list[1])
-                       bb_error_msg_and_die("BUG in varexp2");
-       /* actually, just move string 2*sizeof(char*) bytes back */
-       overlapping_strcpy((char*)list, list[0]);
-       unbackslash((char*)list);
-       debug_printf_expand("string_to_string='%s'\n", (char*)list);
-       return (char*)list;
+       for (r = reserved_list; r < reserved_list + ARRAY_SIZE(reserved_list); r++) {
+               if (strcmp(word->data, r->literal) == 0)
+                       return r;
+       }
+       return NULL;
 }
-
-/* Used for "eval" builtin */
-static char* expand_strvec_to_string(char **argv)
+/* Return 0: not a keyword, 1: keyword
+ */
+static int reserved_word(o_string *word, struct parse_context *ctx)
 {
-       char **list;
+# if ENABLE_HUSH_CASE
+       static const struct reserved_combo reserved_match = {
+               "",        RES_MATCH, NOT_ASSIGNMENT , FLAG_MATCH | FLAG_ESAC
+       };
+# endif
+       const struct reserved_combo *r;
 
-       list = expand_variables(argv, 0x80);
-       /* Convert all NULs to spaces */
-       if (list[0]) {
-               int n = 1;
-               while (list[n]) {
-                       if (HUSH_DEBUG)
-                               if (list[n-1] + strlen(list[n-1]) + 1 != list[n])
-                                       bb_error_msg_and_die("BUG in varexp3");
-                       /* bash uses ' ' regardless of $IFS contents */
-                       list[n][-1] = ' ';
-                       n++;
+       if (word->has_quoted_part)
+               return 0;
+       r = match_reserved_word(word);
+       if (!r)
+               return 0;
+
+       debug_printf("found reserved word %s, res %d\n", r->literal, r->res);
+# if ENABLE_HUSH_CASE
+       if (r->res == RES_IN && ctx->ctx_res_w == RES_CASE_IN) {
+               /* "case word IN ..." - IN part starts first MATCH part */
+               r = &reserved_match;
+       } else
+# endif
+       if (r->flag == 0) { /* '!' */
+               if (ctx->ctx_inverted) { /* bash doesn't accept '! ! true' */
+                       syntax_error("! ! command");
+                       ctx->ctx_res_w = RES_SNTX;
                }
+               ctx->ctx_inverted = 1;
+               return 1;
        }
-       overlapping_strcpy((char*)list, list[0]);
-       debug_printf_expand("strvec_to_string='%s'\n", (char*)list);
-       return (char*)list;
-}
+       if (r->flag & FLAG_START) {
+               struct parse_context *old;
 
-static char **expand_assignments(char **argv, int count)
-{
-       int i;
-       char **p = NULL;
-       /* Expand assignments into one string each */
-       for (i = 0; i < count; i++) {
-               p = add_string_to_strings(p, expand_string_to_string(argv[i]));
+               old = xmalloc(sizeof(*old));
+               debug_printf_parse("push stack %p\n", old);
+               *old = *ctx;   /* physical copy */
+               initialize_context(ctx);
+               ctx->stack = old;
+       } else if (/*ctx->ctx_res_w == RES_NONE ||*/ !(ctx->old_flag & (1 << r->res))) {
+               syntax_error_at(word->data);
+               ctx->ctx_res_w = RES_SNTX;
+               return 1;
+       } else {
+               /* "{...} fi" is ok. "{...} if" is not
+                * Example:
+                * if { echo foo; } then { echo bar; } fi */
+               if (ctx->command->group)
+                       done_pipe(ctx, PIPE_SEQ);
        }
-       return p;
-}
-
-
-#if BB_MMU
-/* never called */
-void re_execute_shell(char ***to_free, const char *s,
-               char *g_argv0, char **g_argv,
-               char **builtin_argv) NORETURN;
-
-static void reset_traps_to_defaults(void)
-{
-       /* This function is always called in a child shell
-        * after fork (not vfork, NOMMU doesn't use this function).
-        */
-       unsigned sig;
-       unsigned mask;
 
-       /* Child shells are not interactive.
-        * SIGTTIN/SIGTTOU/SIGTSTP should not have special handling.
-        * Testcase: (while :; do :; done) + ^Z should background.
-        * Same goes for SIGTERM, SIGHUP, SIGINT.
-        */
-       if (!G.traps && !(G.non_DFL_mask & SPECIAL_INTERACTIVE_SIGS))
-               return; /* already no traps and no SPECIAL_INTERACTIVE_SIGS */
+       ctx->ctx_res_w = r->res;
+       ctx->old_flag = r->flag;
+       word->o_assignment = r->assignment_flag;
 
-       /* Switching off SPECIAL_INTERACTIVE_SIGS.
-        * Stupid. It can be done with *single* &= op, but we can't use
-        * the fact that G.blocked_set is implemented as a bitmask
-        * in libc... */
-       mask = (SPECIAL_INTERACTIVE_SIGS >> 1);
-       sig = 1;
-       while (1) {
-               if (mask & 1) {
-                       /* Careful. Only if no trap or trap is not "" */
-                       if (!G.traps || !G.traps[sig] || G.traps[sig][0])
-                               sigdelset(&G.blocked_set, sig);
-               }
-               mask >>= 1;
-               if (!mask)
-                       break;
-               sig++;
-       }
-       /* Our homegrown sig mask is saner to work with :) */
-       G.non_DFL_mask &= ~SPECIAL_INTERACTIVE_SIGS;
+       if (ctx->old_flag & FLAG_END) {
+               struct parse_context *old;
 
-       /* Resetting all traps to default except empty ones */
-       mask = G.non_DFL_mask;
-       if (G.traps) for (sig = 0; sig < NSIG; sig++, mask >>= 1) {
-               if (!G.traps[sig] || !G.traps[sig][0])
-                       continue;
-               free(G.traps[sig]);
-               G.traps[sig] = NULL;
-               /* There is no signal for 0 (EXIT) */
-               if (sig == 0)
-                       continue;
-               /* There was a trap handler, we just removed it.
-                * But if sig still has non-DFL handling,
-                * we should not unblock the sig. */
-               if (mask & 1)
-                       continue;
-               sigdelset(&G.blocked_set, sig);
+               done_pipe(ctx, PIPE_SEQ);
+               debug_printf_parse("pop stack %p\n", ctx->stack);
+               old = ctx->stack;
+               old->command->group = ctx->list_head;
+               old->command->cmd_type = CMD_NORMAL;
+# if !BB_MMU
+               o_addstr(&old->as_string, ctx->as_string.data);
+               o_free_unsafe(&ctx->as_string);
+               old->command->group_as_string = xstrdup(old->as_string.data);
+               debug_printf_parse("pop, remembering as:'%s'\n",
+                               old->command->group_as_string);
+# endif
+               *ctx = *old;   /* physical copy */
+               free(old);
        }
-       sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
+       return 1;
 }
+#endif /* HAS_KEYWORDS */
 
-#else /* !BB_MMU */
-
-static void re_execute_shell(char ***to_free, const char *s,
-               char *g_argv0, char **g_argv,
-               char **builtin_argv) NORETURN;
-static void re_execute_shell(char ***to_free, const char *s,
-               char *g_argv0, char **g_argv,
-               char **builtin_argv)
+/* Word is complete, look at it and update parsing context.
+ * Normal return is 0. Syntax errors return 1.
+ * Note: on return, word is reset, but not o_free'd!
+ */
+static int done_word(o_string *word, struct parse_context *ctx)
 {
-# define NOMMU_HACK_FMT ("-$%x:%x:%x:%x:%x:%llx" IF_HUSH_LOOPS(":%x"))
-       /* delims + 2 * (number of bytes in printed hex numbers) */
-       char param_buf[sizeof(NOMMU_HACK_FMT) + 2 * (sizeof(int)*6 + sizeof(long long)*1)];
-       char *heredoc_argv[4];
-       struct variable *cur;
-# if ENABLE_HUSH_FUNCTIONS
-       struct function *funcp;
-# endif
-       char **argv, **pp;
-       unsigned cnt;
-       unsigned long long empty_trap_mask;
+       struct command *command = ctx->command;
 
-       if (!g_argv0) { /* heredoc */
-               argv = heredoc_argv;
-               argv[0] = (char *) G.argv0_for_re_execing;
-               argv[1] = (char *) "-<";
-               argv[2] = (char *) s;
-               argv[3] = NULL;
-               pp = &argv[3]; /* used as pointer to empty environment */
-               goto do_exec;
+       debug_printf_parse("done_word entered: '%s' %p\n", word->data, command);
+       if (word->length == 0 && !word->has_quoted_part) {
+               debug_printf_parse("done_word return 0: true null, ignored\n");
+               return 0;
        }
 
-       cnt = 0;
-       pp = builtin_argv;
-       if (pp) while (*pp++)
-               cnt++;
-
-       empty_trap_mask = 0;
-       if (G.traps) {
-               int sig;
-               for (sig = 1; sig < NSIG; sig++) {
-                       if (G.traps[sig] && !G.traps[sig][0])
-                               empty_trap_mask |= 1LL << sig;
+       if (ctx->pending_redirect) {
+               /* We do not glob in e.g. >*.tmp case. bash seems to glob here
+                * only if run as "bash", not "sh" */
+               /* http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html
+                * "2.7 Redirection
+                * ...the word that follows the redirection operator
+                * shall be subjected to tilde expansion, parameter expansion,
+                * command substitution, arithmetic expansion, and quote
+                * removal. Pathname expansion shall not be performed
+                * on the word by a non-interactive shell; an interactive
+                * shell may perform it, but shall do so only when
+                * the expansion would result in one word."
+                */
+               ctx->pending_redirect->rd_filename = xstrdup(word->data);
+               /* Cater for >\file case:
+                * >\a creates file a; >\\a, >"\a", >"\\a" create file \a
+                * Same with heredocs:
+                * for <<\H delim is H; <<\\H, <<"\H", <<"\\H" - \H
+                */
+               if (ctx->pending_redirect->rd_type == REDIRECT_HEREDOC) {
+                       unbackslash(ctx->pending_redirect->rd_filename);
+                       /* Is it <<"HEREDOC"? */
+                       if (word->has_quoted_part) {
+                               ctx->pending_redirect->rd_dup |= HEREDOC_QUOTED;
+                       }
+               }
+               debug_printf_parse("word stored in rd_filename: '%s'\n", word->data);
+               ctx->pending_redirect = NULL;
+       } else {
+               /* If this word wasn't an assignment, next ones definitely
+                * can't be assignments. Even if they look like ones. */
+               if (word->o_assignment != DEFINITELY_ASSIGNMENT
+                && word->o_assignment != WORD_IS_KEYWORD
+               ) {
+                       word->o_assignment = NOT_ASSIGNMENT;
+               } else {
+                       if (word->o_assignment == DEFINITELY_ASSIGNMENT)
+                               command->assignment_cnt++;
+                       word->o_assignment = MAYBE_ASSIGNMENT;
                }
-       }
 
-       sprintf(param_buf, NOMMU_HACK_FMT
-                       , (unsigned) G.root_pid
-                       , (unsigned) G.root_ppid
-                       , (unsigned) G.last_bg_pid
-                       , (unsigned) G.last_exitcode
-                       , cnt
-                       , empty_trap_mask
-                       IF_HUSH_LOOPS(, G.depth_of_loop)
-                       );
-# undef NOMMU_HACK_FMT
-       /* 1:hush 2:-$<pid>:<pid>:<exitcode>:<etc...> <vars...> <funcs...>
-        * 3:-c 4:<cmd> 5:<arg0> <argN...> 6:NULL
-        */
-       cnt += 6;
-       for (cur = G.top_var; cur; cur = cur->next) {
-               if (!cur->flg_export || cur->flg_read_only)
-                       cnt += 2;
-       }
-# if ENABLE_HUSH_FUNCTIONS
-       for (funcp = G.top_func; funcp; funcp = funcp->next)
-               cnt += 3;
+#if HAS_KEYWORDS
+# if ENABLE_HUSH_CASE
+               if (ctx->ctx_dsemicolon
+                && strcmp(word->data, "esac") != 0 /* not "... pattern) cmd;; esac" */
+               ) {
+                       /* already done when ctx_dsemicolon was set to 1: */
+                       /* ctx->ctx_res_w = RES_MATCH; */
+                       ctx->ctx_dsemicolon = 0;
+               } else
 # endif
-       pp = g_argv;
-       while (*pp++)
-               cnt++;
-       *to_free = argv = pp = xzalloc(sizeof(argv[0]) * cnt);
-       *pp++ = (char *) G.argv0_for_re_execing;
-       *pp++ = param_buf;
-       for (cur = G.top_var; cur; cur = cur->next) {
-               if (cur->varstr == hush_version_str)
-                       continue;
-               if (cur->flg_read_only) {
-                       *pp++ = (char *) "-R";
-                       *pp++ = cur->varstr;
-               } else if (!cur->flg_export) {
-                       *pp++ = (char *) "-V";
-                       *pp++ = cur->varstr;
+               if (!command->argv /* if it's the first word... */
+# if ENABLE_HUSH_LOOPS
+                && ctx->ctx_res_w != RES_FOR /* ...not after FOR or IN */
+                && ctx->ctx_res_w != RES_IN
+# endif
+# if ENABLE_HUSH_CASE
+                && ctx->ctx_res_w != RES_CASE
+# endif
+               ) {
+                       debug_printf_parse("checking '%s' for reserved-ness\n", word->data);
+                       if (reserved_word(word, ctx)) {
+                               o_reset_to_empty_unquoted(word);
+                               debug_printf_parse("done_word return %d\n",
+                                               (ctx->ctx_res_w == RES_SNTX));
+                               return (ctx->ctx_res_w == RES_SNTX);
+                       }
+# if ENABLE_HUSH_BASH_COMPAT
+                       if (strcmp(word->data, "[[") == 0) {
+                               command->cmd_type = CMD_SINGLEWORD_NOGLOB;
+                       }
+                       /* fall through */
+# endif
+               }
+#endif
+               if (command->group) {
+                       /* "{ echo foo; } echo bar" - bad */
+                       syntax_error_at(word->data);
+                       debug_printf_parse("done_word return 1: syntax error, "
+                                       "groups and arglists don't mix\n");
+                       return 1;
+               }
+               if (word->has_quoted_part
+                /* optimization: and if it's ("" or '') or ($v... or `cmd`...): */
+                && (word->data[0] == '\0' || word->data[0] == SPECIAL_VAR_SYMBOL)
+                /* (otherwise it's known to be not empty and is already safe) */
+               ) {
+                       /* exclude "$@" - it can expand to no word despite "" */
+                       char *p = word->data;
+                       while (p[0] == SPECIAL_VAR_SYMBOL
+                           && (p[1] & 0x7f) == '@'
+                           && p[2] == SPECIAL_VAR_SYMBOL
+                       ) {
+                               p += 3;
+                       }
+                       if (p == word->data || p[0] != '\0') {
+                               /* saw no "$@", or not only "$@" but some
+                                * real text is there too */
+                               /* insert "empty variable" reference, this makes
+                                * e.g. "", $empty"" etc to not disappear */
+                               o_addchr(word, SPECIAL_VAR_SYMBOL);
+                               o_addchr(word, SPECIAL_VAR_SYMBOL);
+                       }
                }
+               command->argv = add_string_to_strings(command->argv, xstrdup(word->data));
+               debug_print_strings("word appended to argv", command->argv);
        }
-# if ENABLE_HUSH_FUNCTIONS
-       for (funcp = G.top_func; funcp; funcp = funcp->next) {
-               *pp++ = (char *) "-F";
-               *pp++ = funcp->name;
-               *pp++ = funcp->body_as_string;
+
+#if ENABLE_HUSH_LOOPS
+       if (ctx->ctx_res_w == RES_FOR) {
+               if (word->has_quoted_part
+                || !is_well_formed_var_name(command->argv[0], '\0')
+               ) {
+                       /* bash says just "not a valid identifier" */
+                       syntax_error("not a valid identifier in for");
+                       return 1;
+               }
+               /* Force FOR to have just one word (variable name) */
+               /* NB: basically, this makes hush see "for v in ..."
+                * syntax as if it is "for v; in ...". FOR and IN become
+                * two pipe structs in parse tree. */
+               done_pipe(ctx, PIPE_SEQ);
        }
-# endif
-       /* We can pass activated traps here. Say, -Tnn:trap_string
-        *
-        * However, POSIX says that subshells reset signals with traps
-        * to SIG_DFL.
-        * I tested bash-3.2 and it not only does that with true subshells
-        * of the form ( list ), but with any forked children shells.
-        * I set trap "echo W" WINCH; and then tried:
-        *
-        * { echo 1; sleep 20; echo 2; } &
-        * while true; do echo 1; sleep 20; echo 2; break; done &
-        * true | { echo 1; sleep 20; echo 2; } | cat
-        *
-        * In all these cases sending SIGWINCH to the child shell
-        * did not run the trap. If I add trap "echo V" WINCH;
-        * _inside_ group (just before echo 1), it works.
-        *
-        * I conclude it means we don't need to pass active traps here.
-        * Even if we would use signal handlers instead of signal masking
-        * in order to implement trap handling,
-        * exec syscall below resets signals to SIG_DFL for us.
-        */
-       *pp++ = (char *) "-c";
-       *pp++ = (char *) s;
-       if (builtin_argv) {
-               while (*++builtin_argv)
-                       *pp++ = *builtin_argv;
-               *pp++ = (char *) "";
+#endif
+#if ENABLE_HUSH_CASE
+       /* Force CASE to have just one word */
+       if (ctx->ctx_res_w == RES_CASE) {
+               done_pipe(ctx, PIPE_SEQ);
        }
-       *pp++ = g_argv0;
-       while (*g_argv)
-               *pp++ = *g_argv++;
-       /* *pp = NULL; - is already there */
-       pp = environ;
+#endif
 
- do_exec:
-       debug_printf_exec("re_execute_shell pid:%d cmd:'%s'\n", getpid(), s);
-       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
-       execve(bb_busybox_exec_path, argv, pp);
-       /* Fallback. Useful for init=/bin/hush usage etc */
-       if (argv[0][0] == '/')
-               execve(argv[0], argv, pp);
-       xfunc_error_retval = 127;
-       bb_error_msg_and_die("can't re-execute the shell");
+       o_reset_to_empty_unquoted(word);
+
+       debug_printf_parse("done_word return 0\n");
+       return 0;
 }
-#endif  /* !BB_MMU */
 
 
-static void setup_heredoc(struct redir_struct *redir)
+/* Peek ahead in the input to find out if we have a "&n" construct,
+ * as in "2>&1", that represents duplicating a file descriptor.
+ * Return:
+ * REDIRFD_CLOSE if >&- "close fd" construct is seen,
+ * REDIRFD_SYNTAX_ERR if syntax error,
+ * REDIRFD_TO_FILE if no & was seen,
+ * or the number found.
+ */
+#if BB_MMU
+#define parse_redir_right_fd(as_string, input) \
+       parse_redir_right_fd(input)
+#endif
+static int parse_redir_right_fd(o_string *as_string, struct in_str *input)
 {
-       struct fd_pair pair;
-       pid_t pid;
-       int len, written;
-       /* the _body_ of heredoc (misleading field name) */
-       const char *heredoc = redir->rd_filename;
-       char *expanded;
-#if !BB_MMU
-       char **to_free;
-#endif
-
-       expanded = NULL;
-       if (!(redir->rd_dup & HEREDOC_QUOTED)) {
-               expanded = expand_pseudo_dquoted(heredoc);
-               if (expanded)
-                       heredoc = expanded;
-       }
-       len = strlen(heredoc);
+       int ch, d, ok;
 
-       close(redir->rd_fd); /* often saves dup2+close in xmove_fd */
-       xpiped_pair(pair);
-       xmove_fd(pair.rd, redir->rd_fd);
+       ch = i_peek(input);
+       if (ch != '&')
+               return REDIRFD_TO_FILE;
 
-       /* Try writing without forking. Newer kernels have
-        * dynamically growing pipes. Must use non-blocking write! */
-       ndelay_on(pair.wr);
-       while (1) {
-               written = write(pair.wr, heredoc, len);
-               if (written <= 0)
-                       break;
-               len -= written;
-               if (len == 0) {
-                       close(pair.wr);
-                       free(expanded);
-                       return;
-               }
-               heredoc += written;
+       ch = i_getch(input);  /* get the & */
+       nommu_addchr(as_string, ch);
+       ch = i_peek(input);
+       if (ch == '-') {
+               ch = i_getch(input);
+               nommu_addchr(as_string, ch);
+               return REDIRFD_CLOSE;
        }
-       ndelay_off(pair.wr);
-
-       /* Okay, pipe buffer was not big enough */
-       /* Note: we must not create a stray child (bastard? :)
-        * for the unsuspecting parent process. Child creates a grandchild
-        * and exits before parent execs the process which consumes heredoc
-        * (that exec happens after we return from this function) */
-#if !BB_MMU
-       to_free = NULL;
-#endif
-       pid = xvfork();
-       if (pid == 0) {
-               /* child */
-               disable_restore_tty_pgrp_on_exit();
-               pid = BB_MMU ? xfork() : xvfork();
-               if (pid != 0)
-                       _exit(0);
-               /* grandchild */
-               close(redir->rd_fd); /* read side of the pipe */
-#if BB_MMU
-               full_write(pair.wr, heredoc, len); /* may loop or block */
-               _exit(0);
-#else
-               /* Delegate blocking writes to another process */
-               xmove_fd(pair.wr, STDOUT_FILENO);
-               re_execute_shell(&to_free, heredoc, NULL, NULL, NULL);
-#endif
+       d = 0;
+       ok = 0;
+       while (ch != EOF && isdigit(ch)) {
+               d = d*10 + (ch-'0');
+               ok = 1;
+               ch = i_getch(input);
+               nommu_addchr(as_string, ch);
+               ch = i_peek(input);
        }
-       /* parent */
-#if ENABLE_HUSH_FAST
-       G.count_SIGCHLD++;
-//bb_error_msg("[%d] fork in setup_heredoc: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
-#endif
-       enable_restore_tty_pgrp_on_exit();
-#if !BB_MMU
-       free(to_free);
-#endif
-       close(pair.wr);
-       free(expanded);
-       wait(NULL); /* wait till child has died */
+       if (ok) return d;
+
+//TODO: this is the place to catch ">&file" bashism (redirect both fd 1 and 2)
+
+       bb_error_msg("ambiguous redirect");
+       return REDIRFD_SYNTAX_ERR;
 }
 
-/* squirrel != NULL means we squirrel away copies of stdin, stdout,
- * and stderr if they are redirected. */
-static int setup_redirects(struct command *prog, int squirrel[])
+/* Return code is 0 normal, 1 if a syntax error is detected
+ */
+static int parse_redirect(struct parse_context *ctx,
+               int fd,
+               redir_type style,
+               struct in_str *input)
 {
-       int openfd, mode;
+       struct command *command = ctx->command;
        struct redir_struct *redir;
+       struct redir_struct **redirp;
+       int dup_num;
 
-       for (redir = prog->redirects; redir; redir = redir->next) {
-               if (redir->rd_type == REDIRECT_HEREDOC2) {
-                       /* rd_fd<<HERE case */
-                       if (squirrel && redir->rd_fd < 3
-                        && squirrel[redir->rd_fd] < 0
-                       ) {
-                               squirrel[redir->rd_fd] = dup(redir->rd_fd);
-                       }
-                       /* for REDIRECT_HEREDOC2, rd_filename holds _contents_
-                        * of the heredoc */
-                       debug_printf_parse("set heredoc '%s'\n",
-                                       redir->rd_filename);
-                       setup_heredoc(redir);
-                       continue;
+       dup_num = REDIRFD_TO_FILE;
+       if (style != REDIRECT_HEREDOC) {
+               /* Check for a '>&1' type redirect */
+               dup_num = parse_redir_right_fd(&ctx->as_string, input);
+               if (dup_num == REDIRFD_SYNTAX_ERR)
+                       return 1;
+       } else {
+               int ch = i_peek(input);
+               dup_num = (ch == '-'); /* HEREDOC_SKIPTABS bit is 1 */
+               if (dup_num) { /* <<-... */
+                       ch = i_getch(input);
+                       nommu_addchr(&ctx->as_string, ch);
+                       ch = i_peek(input);
                }
+       }
 
-               if (redir->rd_dup == REDIRFD_TO_FILE) {
-                       /* rd_fd<*>file case (<*> is <,>,>>,<>) */
-                       char *p;
-                       if (redir->rd_filename == NULL) {
-                               /* Something went wrong in the parse.
-                                * Pretend it didn't happen */
-                               bb_error_msg("bug in redirect parse");
-                               continue;
-                       }
-                       mode = redir_table[redir->rd_type].mode;
-                       p = expand_string_to_string(redir->rd_filename);
-                       openfd = open_or_warn(p, mode);
-                       free(p);
-                       if (openfd < 0) {
-                       /* this could get lost if stderr has been redirected, but
-                        * bash and ash both lose it as well (though zsh doesn't!) */
-//what the above comment tries to say?
-                               return 1;
-                       }
-               } else {
-                       /* rd_fd<*>rd_dup or rd_fd<*>- cases */
-                       openfd = redir->rd_dup;
+       if (style == REDIRECT_OVERWRITE && dup_num == REDIRFD_TO_FILE) {
+               int ch = i_peek(input);
+               if (ch == '|') {
+                       /* >|FILE redirect ("clobbering" >).
+                        * Since we do not support "set -o noclobber" yet,
+                        * >| and > are the same for now. Just eat |.
+                        */
+                       ch = i_getch(input);
+                       nommu_addchr(&ctx->as_string, ch);
                }
+       }
 
-               if (openfd != redir->rd_fd) {
-                       if (squirrel && redir->rd_fd < 3
-                        && squirrel[redir->rd_fd] < 0
-                       ) {
-                               squirrel[redir->rd_fd] = dup(redir->rd_fd);
-                       }
-                       if (openfd == REDIRFD_CLOSE) {
-                               /* "n>-" means "close me" */
-                               close(redir->rd_fd);
-                       } else {
-                               xdup2(openfd, redir->rd_fd);
-                               if (redir->rd_dup == REDIRFD_TO_FILE)
-                                       close(openfd);
-                       }
-               }
+       /* Create a new redir_struct and append it to the linked list */
+       redirp = &command->redirects;
+       while ((redir = *redirp) != NULL) {
+               redirp = &(redir->next);
        }
-       return 0;
-}
+       *redirp = redir = xzalloc(sizeof(*redir));
+       /* redir->next = NULL; */
+       /* redir->rd_filename = NULL; */
+       redir->rd_type = style;
+       redir->rd_fd = (fd == -1) ? redir_table[style].default_fd : fd;
 
-static void restore_redirects(int squirrel[])
-{
-       int i, fd;
-       for (i = 0; i < 3; i++) {
-               fd = squirrel[i];
-               if (fd != -1) {
-                       /* We simply die on error */
-                       xmove_fd(fd, i);
-               }
+       debug_printf_parse("redirect type %d %s\n", redir->rd_fd,
+                               redir_table[style].descrip);
+
+       redir->rd_dup = dup_num;
+       if (style != REDIRECT_HEREDOC && dup_num != REDIRFD_TO_FILE) {
+               /* Erik had a check here that the file descriptor in question
+                * is legit; I postpone that to "run time"
+                * A "-" representation of "close me" shows up as a -3 here */
+               debug_printf_parse("duplicating redirect '%d>&%d'\n",
+                               redir->rd_fd, redir->rd_dup);
+       } else {
+               /* Set ctx->pending_redirect, so we know what to do at the
+                * end of the next parsed word. */
+               ctx->pending_redirect = redir;
        }
+       return 0;
 }
 
+/* If a redirect is immediately preceded by a number, that number is
+ * supposed to tell which file descriptor to redirect.  This routine
+ * looks for such preceding numbers.  In an ideal world this routine
+ * needs to handle all the following classes of redirects...
+ *     echo 2>foo     # redirects fd  2 to file "foo", nothing passed to echo
+ *     echo 49>foo    # redirects fd 49 to file "foo", nothing passed to echo
+ *     echo -2>foo    # redirects fd  1 to file "foo",    "-2" passed to echo
+ *     echo 49x>foo   # redirects fd  1 to file "foo",   "49x" passed to echo
+ *
+ * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html
+ * "2.7 Redirection
+ * ... If n is quoted, the number shall not be recognized as part of
+ * the redirection expression. For example:
+ * echo \2>a
+ * writes the character 2 into file a"
+ * We are getting it right by setting ->has_quoted_part on any \<char>
+ *
+ * A -1 return means no valid number was found,
+ * the caller should use the appropriate default for this redirection.
+ */
+static int redirect_opt_num(o_string *o)
+{
+       int num;
 
-static void free_pipe_list(struct pipe *head);
+       if (o->data == NULL)
+               return -1;
+       num = bb_strtou(o->data, NULL, 10);
+       if (errno || num < 0)
+               return -1;
+       o_reset_to_empty_unquoted(o);
+       return num;
+}
 
-/* Return code is the exit status of the pipe */
-static void free_pipe(struct pipe *pi)
+#if BB_MMU
+#define fetch_till_str(as_string, input, word, skip_tabs) \
+       fetch_till_str(input, word, skip_tabs)
+#endif
+static char *fetch_till_str(o_string *as_string,
+               struct in_str *input,
+               const char *word,
+               int heredoc_flags)
 {
-       char **p;
-       struct command *command;
-       struct redir_struct *r, *rnext;
-       int a, i;
-
-       if (pi->stopped_cmds > 0) /* why? */
-               return;
-       debug_printf_clean("run pipe: (pid %d)\n", getpid());
-       for (i = 0; i < pi->num_cmds; i++) {
-               command = &pi->cmds[i];
-               debug_printf_clean("  command %d:\n", i);
-               if (command->argv) {
-                       for (a = 0, p = command->argv; *p; a++, p++) {
-                               debug_printf_clean("   argv[%d] = %s\n", a, *p);
+       o_string heredoc = NULL_O_STRING;
+       int past_EOL = 0;
+       int prev = 0; /* not \ */
+       int ch;
+
+       goto jump_in;
+       while (1) {
+               ch = i_getch(input);
+               nommu_addchr(as_string, ch);
+               if (ch == '\n'
+               /* TODO: or EOF? (heredoc delimiter may end with <eof>, not only <eol>) */
+                && ((heredoc_flags & HEREDOC_QUOTED) || prev != '\\')
+               ) {
+                       if (strcmp(heredoc.data + past_EOL, word) == 0) {
+                               heredoc.data[past_EOL] = '\0';
+                               debug_printf_parse("parsed heredoc '%s'\n", heredoc.data);
+                               return heredoc.data;
                        }
-                       free_strings(command->argv);
-                       command->argv = NULL;
-               }
-               /* not "else if": on syntax error, we may have both! */
-               if (command->group) {
-                       debug_printf_clean("   begin group (cmd_type:%d)\n",
-                                       command->cmd_type);
-                       free_pipe_list(command->group);
-                       debug_printf_clean("   end group\n");
-                       command->group = NULL;
-               }
-               /* else is crucial here.
-                * If group != NULL, child_func is meaningless */
-#if ENABLE_HUSH_FUNCTIONS
-               else if (command->child_func) {
-                       debug_printf_exec("cmd %p releases child func at %p\n", command, command->child_func);
-                       command->child_func->parent_cmd = NULL;
+                       do {
+                               o_addchr(&heredoc, '\n');
+                               prev = 0; /* not \ */
+                               past_EOL = heredoc.length;
+ jump_in:
+                               do {
+                                       ch = i_getch(input);
+                                       nommu_addchr(as_string, ch);
+                               } while ((heredoc_flags & HEREDOC_SKIPTABS) && ch == '\t');
+                       } while (ch == '\n');
                }
-#endif
-#if !BB_MMU
-               free(command->group_as_string);
-               command->group_as_string = NULL;
-#endif
-               for (r = command->redirects; r; r = rnext) {
-                       debug_printf_clean("   redirect %d%s",
-                                       r->rd_fd, redir_table[r->rd_type].descrip);
-                       /* guard against the case >$FOO, where foo is unset or blank */
-                       if (r->rd_filename) {
-                               debug_printf_clean(" fname:'%s'\n", r->rd_filename);
-                               free(r->rd_filename);
-                               r->rd_filename = NULL;
-                       }
-                       debug_printf_clean(" rd_dup:%d\n", r->rd_dup);
-                       rnext = r->next;
-                       free(r);
+               if (ch == EOF) {
+                       o_free_unsafe(&heredoc);
+                       return NULL;
                }
-               command->redirects = NULL;
+               o_addchr(&heredoc, ch);
+               if (prev == '\\' && ch == '\\')
+                       /* Correctly handle foo\\<eol> (not a line cont.) */
+                       prev = 0; /* not \ */
+               else
+                       prev = ch;
+               nommu_addchr(as_string, ch);
        }
-       free(pi->cmds);   /* children are an array, they get freed all at once */
-       pi->cmds = NULL;
-#if ENABLE_HUSH_JOB
-       free(pi->cmdtext);
-       pi->cmdtext = NULL;
-#endif
 }
 
-static void free_pipe_list(struct pipe *head)
+/* Look at entire parse tree for not-yet-loaded REDIRECT_HEREDOCs
+ * and load them all. There should be exactly heredoc_cnt of them.
+ */
+static int fetch_heredocs(int heredoc_cnt, struct parse_context *ctx, struct in_str *input)
 {
-       struct pipe *pi, *next;
+       struct pipe *pi = ctx->list_head;
 
-       for (pi = head; pi; pi = next) {
-#if HAS_KEYWORDS
-               debug_printf_clean(" pipe reserved word %d\n", pi->res_word);
-#endif
-               free_pipe(pi);
-               debug_printf_clean("pipe followup code %d\n", pi->followup);
-               next = pi->next;
-               /*pi->next = NULL;*/
-               free(pi);
+       while (pi && heredoc_cnt) {
+               int i;
+               struct command *cmd = pi->cmds;
+
+               debug_printf_parse("fetch_heredocs: num_cmds:%d cmd argv0:'%s'\n",
+                               pi->num_cmds,
+                               cmd->argv ? cmd->argv[0] : "NONE");
+               for (i = 0; i < pi->num_cmds; i++) {
+                       struct redir_struct *redir = cmd->redirects;
+
+                       debug_printf_parse("fetch_heredocs: %d cmd argv0:'%s'\n",
+                                       i, cmd->argv ? cmd->argv[0] : "NONE");
+                       while (redir) {
+                               if (redir->rd_type == REDIRECT_HEREDOC) {
+                                       char *p;
+
+                                       redir->rd_type = REDIRECT_HEREDOC2;
+                                       /* redir->rd_dup is (ab)used to indicate <<- */
+                                       p = fetch_till_str(&ctx->as_string, input,
+                                                       redir->rd_filename, redir->rd_dup);
+                                       if (!p) {
+                                               syntax_error("unexpected EOF in here document");
+                                               return 1;
+                                       }
+                                       free(redir->rd_filename);
+                                       redir->rd_filename = p;
+                                       heredoc_cnt--;
+                               }
+                               redir = redir->next;
+                       }
+                       cmd++;
+               }
+               pi = pi->next;
        }
+#if 0
+       /* Should be 0. If it isn't, it's a parse error */
+       if (heredoc_cnt)
+               bb_error_msg_and_die("heredoc BUG 2");
+#endif
+       return 0;
 }
 
 
@@ -3415,3470 +3251,3955 @@ static int run_list(struct pipe *pi);
 static struct pipe *parse_stream(char **pstring,
                struct in_str *input,
                int end_trigger);
-static void parse_and_run_string(const char *s);
 
 
-static char *find_in_path(const char *arg)
+#if !ENABLE_HUSH_FUNCTIONS
+#define parse_group(dest, ctx, input, ch) \
+       parse_group(ctx, input, ch)
+#endif
+static int parse_group(o_string *dest, struct parse_context *ctx,
+       struct in_str *input, int ch)
 {
-       char *ret = NULL;
-       const char *PATH = get_local_var_value("PATH");
+       /* dest contains characters seen prior to ( or {.
+        * Typically it's empty, but for function defs,
+        * it contains function name (without '()'). */
+       struct pipe *pipe_list;
+       int endch;
+       struct command *command = ctx->command;
 
-       if (!PATH)
-               return NULL;
+       debug_printf_parse("parse_group entered\n");
+#if ENABLE_HUSH_FUNCTIONS
+       if (ch == '(' && !dest->has_quoted_part) {
+               if (dest->length)
+                       if (done_word(dest, ctx))
+                               return 1;
+               if (!command->argv)
+                       goto skip; /* (... */
+               if (command->argv[1]) { /* word word ... (... */
+                       syntax_error_unexpected_ch('(');
+                       return 1;
+               }
+               /* it is "word(..." or "word (..." */
+               do
+                       ch = i_getch(input);
+               while (ch == ' ' || ch == '\t');
+               if (ch != ')') {
+                       syntax_error_unexpected_ch(ch);
+                       return 1;
+               }
+               nommu_addchr(&ctx->as_string, ch);
+               do
+                       ch = i_getch(input);
+               while (ch == ' ' || ch == '\t' || ch == '\n');
+               if (ch != '{') {
+                       syntax_error_unexpected_ch(ch);
+                       return 1;
+               }
+               nommu_addchr(&ctx->as_string, ch);
+               command->cmd_type = CMD_FUNCDEF;
+               goto skip;
+       }
+#endif
 
-       while (1) {
-               const char *end = strchrnul(PATH, ':');
-               int sz = end - PATH; /* must be int! */
+#if 0 /* Prevented by caller */
+       if (command->argv /* word [word]{... */
+        || dest->length /* word{... */
+        || dest->has_quoted_part /* ""{... */
+       ) {
+               syntax_error(NULL);
+               debug_printf_parse("parse_group return 1: "
+                       "syntax error, groups and arglists don't mix\n");
+               return 1;
+       }
+#endif
 
-               free(ret);
-               if (sz != 0) {
-                       ret = xasprintf("%.*s/%s", sz, PATH, arg);
-               } else {
-                       /* We have xxx::yyyy in $PATH,
-                        * it means "use current dir" */
-                       ret = xstrdup(arg);
+#if ENABLE_HUSH_FUNCTIONS
+ skip:
+#endif
+       endch = '}';
+       if (ch == '(') {
+               endch = ')';
+               command->cmd_type = CMD_SUBSHELL;
+       } else {
+               /* bash does not allow "{echo...", requires whitespace */
+               ch = i_getch(input);
+               if (ch != ' ' && ch != '\t' && ch != '\n') {
+                       syntax_error_unexpected_ch(ch);
+                       return 1;
                }
-               if (access(ret, F_OK) == 0)
-                       break;
+               nommu_addchr(&ctx->as_string, ch);
+       }
 
-               if (*end == '\0') {
-                       free(ret);
-                       return NULL;
+       {
+#if BB_MMU
+# define as_string NULL
+#else
+               char *as_string = NULL;
+#endif
+               pipe_list = parse_stream(&as_string, input, endch);
+#if !BB_MMU
+               if (as_string)
+                       o_addstr(&ctx->as_string, as_string);
+#endif
+               /* empty ()/{} or parse error? */
+               if (!pipe_list || pipe_list == ERR_PTR) {
+                       /* parse_stream already emitted error msg */
+                       if (!BB_MMU)
+                               free(as_string);
+                       debug_printf_parse("parse_group return 1: "
+                               "parse_stream returned %p\n", pipe_list);
+                       return 1;
                }
-               PATH = end + 1;
+               command->group = pipe_list;
+#if !BB_MMU
+               as_string[strlen(as_string) - 1] = '\0'; /* plink ')' or '}' */
+               command->group_as_string = as_string;
+               debug_printf_parse("end of group, remembering as:'%s'\n",
+                               command->group_as_string);
+#endif
+#undef as_string
        }
-
-       return ret;
+       debug_printf_parse("parse_group return 0\n");
+       return 0;
+       /* command remains "open", available for possible redirects */
 }
 
-static const struct built_in_command* find_builtin_helper(const char *name,
-               const struct built_in_command *x,
-               const struct built_in_command *end)
+#if ENABLE_HUSH_TICK || ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_DOLLAR_OPS
+/* Subroutines for copying $(...) and `...` things */
+static void add_till_backquote(o_string *dest, struct in_str *input);
+/* '...' */
+static void add_till_single_quote(o_string *dest, struct in_str *input)
 {
-       while (x != end) {
-               if (strcmp(name, x->b_cmd) != 0) {
-                       x++;
-                       continue;
+       while (1) {
+               int ch = i_getch(input);
+               if (ch == EOF) {
+                       syntax_error_unterm_ch('\'');
+                       /*xfunc_die(); - redundant */
                }
-               debug_printf_exec("found builtin '%s'\n", name);
-               return x;
+               if (ch == '\'')
+                       return;
+               o_addchr(dest, ch);
        }
-       return NULL;
 }
-static const struct built_in_command* find_builtin1(const char *name)
-{
-       return find_builtin_helper(name, bltins1, &bltins1[ARRAY_SIZE(bltins1)]);
-}
-static const struct built_in_command* find_builtin(const char *name)
-{
-       const struct built_in_command *x = find_builtin1(name);
-       if (x)
-               return x;
-       return find_builtin_helper(name, bltins2, &bltins2[ARRAY_SIZE(bltins2)]);
-}
-
-#if ENABLE_HUSH_FUNCTIONS
-static struct function **find_function_slot(const char *name)
+/* "...\"...`..`...." - do we need to handle "...$(..)..." too? */
+static void add_till_double_quote(o_string *dest, struct in_str *input)
 {
-       struct function **funcpp = &G.top_func;
-       while (*funcpp) {
-               if (strcmp(name, (*funcpp)->name) == 0) {
-                       break;
+       while (1) {
+               int ch = i_getch(input);
+               if (ch == EOF) {
+                       syntax_error_unterm_ch('"');
+                       /*xfunc_die(); - redundant */
                }
-               funcpp = &(*funcpp)->next;
+               if (ch == '"')
+                       return;
+               if (ch == '\\') {  /* \x. Copy both chars. */
+                       o_addchr(dest, ch);
+                       ch = i_getch(input);
+               }
+               o_addchr(dest, ch);
+               if (ch == '`') {
+                       add_till_backquote(dest, input);
+                       o_addchr(dest, ch);
+                       continue;
+               }
+               //if (ch == '$') ...
        }
-       return funcpp;
-}
-
-static const struct function *find_function(const char *name)
-{
-       const struct function *funcp = *find_function_slot(name);
-       if (funcp)
-               debug_printf_exec("found function '%s'\n", name);
-       return funcp;
 }
-
-/* Note: takes ownership on name ptr */
-static struct function *new_function(char *name)
+/* Process `cmd` - copy contents until "`" is seen. Complicated by
+ * \` quoting.
+ * "Within the backquoted style of command substitution, backslash
+ * shall retain its literal meaning, except when followed by: '$', '`', or '\'.
+ * The search for the matching backquote shall be satisfied by the first
+ * backquote found without a preceding backslash; during this search,
+ * if a non-escaped backquote is encountered within a shell comment,
+ * a here-document, an embedded command substitution of the $(command)
+ * form, or a quoted string, undefined results occur. A single-quoted
+ * or double-quoted string that begins, but does not end, within the
+ * "`...`" sequence produces undefined results."
+ * Example                               Output
+ * echo `echo '\'TEST\`echo ZZ\`BEST`    \TESTZZBEST
+ */
+static void add_till_backquote(o_string *dest, struct in_str *input)
 {
-       struct function **funcpp = find_function_slot(name);
-       struct function *funcp = *funcpp;
-
-       if (funcp != NULL) {
-               struct command *cmd = funcp->parent_cmd;
-               debug_printf_exec("func %p parent_cmd %p\n", funcp, cmd);
-               if (!cmd) {
-                       debug_printf_exec("freeing & replacing function '%s'\n", funcp->name);
-                       free(funcp->name);
-                       /* Note: if !funcp->body, do not free body_as_string!
-                        * This is a special case of "-F name body" function:
-                        * body_as_string was not malloced! */
-                       if (funcp->body) {
-                               free_pipe_list(funcp->body);
-# if !BB_MMU
-                               free(funcp->body_as_string);
-# endif
+       while (1) {
+               int ch = i_getch(input);
+               if (ch == EOF) {
+                       syntax_error_unterm_ch('`');
+                       /*xfunc_die(); - redundant */
+               }
+               if (ch == '`')
+                       return;
+               if (ch == '\\') {
+                       /* \x. Copy both chars unless it is \` */
+                       int ch2 = i_getch(input);
+                       if (ch2 == EOF) {
+                               syntax_error_unterm_ch('`');
+                               /*xfunc_die(); - redundant */
                        }
-               } else {
-                       debug_printf_exec("reinserting in tree & replacing function '%s'\n", funcp->name);
-                       cmd->argv[0] = funcp->name;
-                       cmd->group = funcp->body;
-# if !BB_MMU
-                       cmd->group_as_string = funcp->body_as_string;
-# endif
+                       if (ch2 != '`' && ch2 != '$' && ch2 != '\\')
+                               o_addchr(dest, ch);
+                       ch = ch2;
                }
-       } else {
-               debug_printf_exec("remembering new function '%s'\n", name);
-               funcp = *funcpp = xzalloc(sizeof(*funcp));
-               /*funcp->next = NULL;*/
+               o_addchr(dest, ch);
        }
-
-       funcp->name = name;
-       return funcp;
 }
-
-static void unset_func(const char *name)
+/* Process $(cmd) - copy contents until ")" is seen. Complicated by
+ * quoting and nested ()s.
+ * "With the $(command) style of command substitution, all characters
+ * following the open parenthesis to the matching closing parenthesis
+ * constitute the command. Any valid shell script can be used for command,
+ * except a script consisting solely of redirections which produces
+ * unspecified results."
+ * Example                              Output
+ * echo $(echo '(TEST)' BEST)           (TEST) BEST
+ * echo $(echo 'TEST)' BEST)            TEST) BEST
+ * echo $(echo \(\(TEST\) BEST)         ((TEST) BEST
+ *
+ * Also adapted to eat ${var%...} and $((...)) constructs, since ... part
+ * can contain arbitrary constructs, just like $(cmd).
+ * In bash compat mode, it needs to also be able to stop on ':' or '/'
+ * for ${var:N[:M]} and ${var/P[/R]} parsing.
+ */
+#define DOUBLE_CLOSE_CHAR_FLAG 0x80
+static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsigned end_ch)
 {
-       struct function **funcpp = find_function_slot(name);
-       struct function *funcp = *funcpp;
-
-       if (funcp != NULL) {
-               debug_printf_exec("freeing function '%s'\n", funcp->name);
-               *funcpp = funcp->next;
-               /* funcp is unlinked now, deleting it.
-                * Note: if !funcp->body, the function was created by
-                * "-F name body", do not free ->body_as_string
-                * and ->name as they were not malloced. */
-               if (funcp->body) {
-                       free_pipe_list(funcp->body);
-                       free(funcp->name);
-# if !BB_MMU
-                       free(funcp->body_as_string);
+       int ch;
+       char dbl = end_ch & DOUBLE_CLOSE_CHAR_FLAG;
+# if ENABLE_HUSH_BASH_COMPAT
+       char end_char2 = end_ch >> 8;
 # endif
+       end_ch &= (DOUBLE_CLOSE_CHAR_FLAG - 1);
+
+       while (1) {
+               ch = i_getch(input);
+               if (ch == EOF) {
+                       syntax_error_unterm_ch(end_ch);
+                       /*xfunc_die(); - redundant */
+               }
+               if (ch == end_ch  IF_HUSH_BASH_COMPAT( || ch == end_char2)) {
+                       if (!dbl)
+                               break;
+                       /* we look for closing )) of $((EXPR)) */
+                       if (i_peek(input) == end_ch) {
+                               i_getch(input); /* eat second ')' */
+                               break;
+                       }
+               }
+               o_addchr(dest, ch);
+               if (ch == '(' || ch == '{') {
+                       ch = (ch == '(' ? ')' : '}');
+                       add_till_closing_bracket(dest, input, ch);
+                       o_addchr(dest, ch);
+                       continue;
+               }
+               if (ch == '\'') {
+                       add_till_single_quote(dest, input);
+                       o_addchr(dest, ch);
+                       continue;
+               }
+               if (ch == '"') {
+                       add_till_double_quote(dest, input);
+                       o_addchr(dest, ch);
+                       continue;
+               }
+               if (ch == '`') {
+                       add_till_backquote(dest, input);
+                       o_addchr(dest, ch);
+                       continue;
+               }
+               if (ch == '\\') {
+                       /* \x. Copy verbatim. Important for  \(, \) */
+                       ch = i_getch(input);
+                       if (ch == EOF) {
+                               syntax_error_unterm_ch(')');
+                               /*xfunc_die(); - redundant */
+                       }
+                       o_addchr(dest, ch);
+                       continue;
                }
-               free(funcp);
        }
+       return ch;
 }
+#endif /* ENABLE_HUSH_TICK || ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_DOLLAR_OPS */
 
-# if BB_MMU
-#define exec_function(to_free, funcp, argv) \
-       exec_function(funcp, argv)
-# endif
-static void exec_function(char ***to_free,
-               const struct function *funcp,
-               char **argv) NORETURN;
-static void exec_function(char ***to_free,
-               const struct function *funcp,
-               char **argv)
-{
-# if BB_MMU
-       int n = 1;
-
-       argv[0] = G.global_argv[0];
-       G.global_argv = argv;
-       while (*++argv)
-               n++;
-       G.global_argc = n;
-       /* On MMU, funcp->body is always non-NULL */
-       n = run_list(funcp->body);
-       fflush_all();
-       _exit(n);
-# else
-       re_execute_shell(to_free,
-                       funcp->body_as_string,
-                       G.global_argv[0],
-                       argv + 1,
-                       NULL);
-# endif
-}
-
-static int run_function(const struct function *funcp, char **argv)
+/* Return code: 0 for OK, 1 for syntax error */
+#if BB_MMU
+#define parse_dollar(as_string, dest, input) \
+       parse_dollar(dest, input)
+#define as_string NULL
+#endif
+static int parse_dollar(o_string *as_string,
+               o_string *dest,
+               struct in_str *input)
 {
-       int rc;
-       save_arg_t sv;
-       smallint sv_flg;
-
-       save_and_replace_G_args(&sv, argv);
-
-       /* "we are in function, ok to use return" */
-       sv_flg = G.flag_return_in_progress;
-       G.flag_return_in_progress = -1;
-# if ENABLE_HUSH_LOCAL
-       G.func_nest_level++;
-# endif
+       int ch = i_peek(input);  /* first character after the $ */
+       unsigned char quote_mask = (dest->o_expflags & EXP_FLAG_ESC_GLOB_CHARS) ? 0x80 : 0;
 
-       /* On MMU, funcp->body is always non-NULL */
-# if !BB_MMU
-       if (!funcp->body) {
-               /* Function defined by -F */
-               parse_and_run_string(funcp->body_as_string);
-               rc = G.last_exitcode;
-       } else
-# endif
-       {
-               rc = run_list(funcp->body);
-       }
+       debug_printf_parse("parse_dollar entered: ch='%c'\n", ch);
+       if (isalpha(ch)) {
+               ch = i_getch(input);
+               nommu_addchr(as_string, ch);
+ make_var:
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+               while (1) {
+                       debug_printf_parse(": '%c'\n", ch);
+                       o_addchr(dest, ch | quote_mask);
+                       quote_mask = 0;
+                       ch = i_peek(input);
+                       if (!isalnum(ch) && ch != '_')
+                               break;
+                       ch = i_getch(input);
+                       nommu_addchr(as_string, ch);
+               }
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+       } else if (isdigit(ch)) {
+ make_one_char_var:
+               ch = i_getch(input);
+               nommu_addchr(as_string, ch);
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+               debug_printf_parse(": '%c'\n", ch);
+               o_addchr(dest, ch | quote_mask);
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+       } else switch (ch) {
+       case '$': /* pid */
+       case '!': /* last bg pid */
+       case '?': /* last exit code */
+       case '#': /* number of args */
+       case '*': /* args */
+       case '@': /* args */
+               goto make_one_char_var;
+       case '{': {
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
 
-# if ENABLE_HUSH_LOCAL
-       {
-               struct variable *var;
-               struct variable **var_pp;
+               ch = i_getch(input); /* eat '{' */
+               nommu_addchr(as_string, ch);
 
-               var_pp = &G.top_var;
-               while ((var = *var_pp) != NULL) {
-                       if (var->func_nest_level < G.func_nest_level) {
-                               var_pp = &var->next;
-                               continue;
-                       }
-                       /* Unexport */
-                       if (var->flg_export)
-                               bb_unsetenv(var->varstr);
-                       /* Remove from global list */
-                       *var_pp = var->next;
-                       /* Free */
-                       if (!var->max_len)
-                               free(var->varstr);
-                       free(var);
+               ch = i_getch(input); /* first char after '{' */
+               nommu_addchr(as_string, ch);
+               /* It should be ${?}, or ${#var},
+                * or even ${?+subst} - operator acting on a special variable,
+                * or the beginning of variable name.
+                */
+               if (!strchr(_SPECIAL_VARS_STR, ch) && !isalnum(ch)) { /* not one of those */
+ bad_dollar_syntax:
+                       syntax_error_unterm_str("${name}");
+                       debug_printf_parse("parse_dollar return 1: unterminated ${name}\n");
+                       return 1;
                }
-               G.func_nest_level--;
-       }
-# endif
-       G.flag_return_in_progress = sv_flg;
+               ch |= quote_mask;
 
-       restore_G_args(&sv, argv);
+               /* It's possible to just call add_till_closing_bracket() at this point.
+                * However, this regresses some of our testsuite cases
+                * which check invalid constructs like ${%}.
+                * Oh well... let's check that the var name part is fine... */
 
-       return rc;
-}
-#endif /* ENABLE_HUSH_FUNCTIONS */
+               while (1) {
+                       unsigned pos;
 
+                       o_addchr(dest, ch);
+                       debug_printf_parse(": '%c'\n", ch);
 
-#if BB_MMU
-#define exec_builtin(to_free, x, argv) \
-       exec_builtin(x, argv)
-#else
-#define exec_builtin(to_free, x, argv) \
-       exec_builtin(to_free, argv)
-#endif
-static void exec_builtin(char ***to_free,
-               const struct built_in_command *x,
-               char **argv) NORETURN;
-static void exec_builtin(char ***to_free,
-               const struct built_in_command *x,
-               char **argv)
-{
-#if BB_MMU
-       int rcode = x->b_function(argv);
-       fflush_all();
-       _exit(rcode);
+                       ch = i_getch(input);
+                       nommu_addchr(as_string, ch);
+                       if (ch == '}')
+                               break;
+
+                       if (!isalnum(ch) && ch != '_') {
+                               unsigned end_ch;
+                               unsigned char last_ch;
+                               /* handle parameter expansions
+                                * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02
+                                */
+                               if (!strchr(VAR_SUBST_OPS, ch)) /* ${var<bad_char>... */
+                                       goto bad_dollar_syntax;
+
+                               /* Eat everything until closing '}' (or ':') */
+                               end_ch = '}';
+                               if (ENABLE_HUSH_BASH_COMPAT
+                                && ch == ':'
+                                && !strchr(MINUS_PLUS_EQUAL_QUESTION, i_peek(input))
+                               ) {
+                                       /* It's ${var:N[:M]} thing */
+                                       end_ch = '}' * 0x100 + ':';
+                               }
+                               if (ENABLE_HUSH_BASH_COMPAT
+                                && ch == '/'
+                               ) {
+                                       /* It's ${var/[/]pattern[/repl]} thing */
+                                       if (i_peek(input) == '/') { /* ${var//pattern[/repl]}? */
+                                               i_getch(input);
+                                               nommu_addchr(as_string, '/');
+                                               ch = '\\';
+                                       }
+                                       end_ch = '}' * 0x100 + '/';
+                               }
+                               o_addchr(dest, ch);
+ again:
+                               if (!BB_MMU)
+                                       pos = dest->length;
+#if ENABLE_HUSH_DOLLAR_OPS
+                               last_ch = add_till_closing_bracket(dest, input, end_ch);
 #else
-       /* On NOMMU, we must never block!
-        * Example: { sleep 99 | read line; } & echo Ok
-        */
-       re_execute_shell(to_free,
-                       argv[0],
-                       G.global_argv[0],
-                       G.global_argv + 1,
-                       argv);
+#error Simple code to only allow ${var} is not implemented
 #endif
-}
+                               if (as_string) {
+                                       o_addstr(as_string, dest->data + pos);
+                                       o_addchr(as_string, last_ch);
+                               }
 
+                               if (ENABLE_HUSH_BASH_COMPAT && (end_ch & 0xff00)) {
+                                       /* close the first block: */
+                                       o_addchr(dest, SPECIAL_VAR_SYMBOL);
+                                       /* while parsing N from ${var:N[:M]}
+                                        * or pattern from ${var/[/]pattern[/repl]} */
+                                       if ((end_ch & 0xff) == last_ch) {
+                                               /* got ':' or '/'- parse the rest */
+                                               end_ch = '}';
+                                               goto again;
+                                       }
+                                       /* got '}' */
+                                       if (end_ch == '}' * 0x100 + ':') {
+                                               /* it's ${var:N} - emulate :999999999 */
+                                               o_addstr(dest, "999999999");
+                                       } /* else: it's ${var/[/]pattern} */
+                               }
+                               break;
+                       }
+               }
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+               break;
+       }
+#if ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_TICK
+       case '(': {
+               unsigned pos;
 
-static void execvp_or_die(char **argv) NORETURN;
-static void execvp_or_die(char **argv)
-{
-       debug_printf_exec("execing '%s'\n", argv[0]);
-       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
-       execvp(argv[0], argv);
-       bb_perror_msg("can't execute '%s'", argv[0]);
-       _exit(127); /* bash compat */
+               ch = i_getch(input);
+               nommu_addchr(as_string, ch);
+# if ENABLE_SH_MATH_SUPPORT
+               if (i_peek(input) == '(') {
+                       ch = i_getch(input);
+                       nommu_addchr(as_string, ch);
+                       o_addchr(dest, SPECIAL_VAR_SYMBOL);
+                       o_addchr(dest, /*quote_mask |*/ '+');
+                       if (!BB_MMU)
+                               pos = dest->length;
+                       add_till_closing_bracket(dest, input, ')' | DOUBLE_CLOSE_CHAR_FLAG);
+                       if (as_string) {
+                               o_addstr(as_string, dest->data + pos);
+                               o_addchr(as_string, ')');
+                               o_addchr(as_string, ')');
+                       }
+                       o_addchr(dest, SPECIAL_VAR_SYMBOL);
+                       break;
+               }
+# endif
+# if ENABLE_HUSH_TICK
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+               o_addchr(dest, quote_mask | '`');
+               if (!BB_MMU)
+                       pos = dest->length;
+               add_till_closing_bracket(dest, input, ')');
+               if (as_string) {
+                       o_addstr(as_string, dest->data + pos);
+                       o_addchr(as_string, ')');
+               }
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+# endif
+               break;
+       }
+#endif
+       case '_':
+               ch = i_getch(input);
+               nommu_addchr(as_string, ch);
+               ch = i_peek(input);
+               if (isalnum(ch)) { /* it's $_name or $_123 */
+                       ch = '_';
+                       goto make_var;
+               }
+               /* else: it's $_ */
+       /* TODO: $_ and $-: */
+       /* $_ Shell or shell script name; or last argument of last command
+        * (if last command wasn't a pipe; if it was, bash sets $_ to "");
+        * but in command's env, set to full pathname used to invoke it */
+       /* $- Option flags set by set builtin or shell options (-i etc) */
+       default:
+               o_addQchr(dest, '$');
+       }
+       debug_printf_parse("parse_dollar return 0\n");
+       return 0;
+#undef as_string
 }
 
-static void dump_cmd_in_x_mode(char **argv)
+#if BB_MMU
+#define parse_stream_dquoted(as_string, dest, input, dquote_end) \
+       parse_stream_dquoted(dest, input, dquote_end)
+#define as_string NULL
+#endif
+static int parse_stream_dquoted(o_string *as_string,
+               o_string *dest,
+               struct in_str *input,
+               int dquote_end)
 {
-       if (G.x_mode && argv) {
-               /* We want to output the line in one write op */
-               char *buf, *p;
-               int len;
-               int n;
+       int ch;
+       int next;
 
-               len = 3;
-               n = 0;
-               while (argv[n])
-                       len += strlen(argv[n++]) + 1;
-               buf = xmalloc(len);
-               buf[0] = '+';
-               p = buf + 1;
-               n = 0;
-               while (argv[n])
-                       p += sprintf(p, " %s", argv[n++]);
-               *p++ = '\n';
-               *p = '\0';
-               fputs(buf, stderr);
-               free(buf);
+ again:
+       ch = i_getch(input);
+       if (ch != EOF)
+               nommu_addchr(as_string, ch);
+       if (ch == dquote_end) { /* may be only '"' or EOF */
+               if (dest->o_assignment == NOT_ASSIGNMENT)
+                       dest->o_expflags ^= EXP_FLAG_ESC_GLOB_CHARS;
+               debug_printf_parse("parse_stream_dquoted return 0\n");
+               return 0;
        }
-}
-
-#if BB_MMU
-#define pseudo_exec_argv(nommu_save, argv, assignment_cnt, argv_expanded) \
-       pseudo_exec_argv(argv, assignment_cnt, argv_expanded)
-#define pseudo_exec(nommu_save, command, argv_expanded) \
-       pseudo_exec(command, argv_expanded)
-#endif
-
-/* Called after [v]fork() in run_pipe, or from builtin_exec.
- * Never returns.
- * Don't exit() here.  If you don't exec, use _exit instead.
- * The at_exit handlers apparently confuse the calling process,
- * in particular stdin handling.  Not sure why? -- because of vfork! (vda) */
-static void pseudo_exec_argv(nommu_save_t *nommu_save,
-               char **argv, int assignment_cnt,
-               char **argv_expanded) NORETURN;
-static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
-               char **argv, int assignment_cnt,
-               char **argv_expanded)
-{
-       char **new_env;
-
-       new_env = expand_assignments(argv, assignment_cnt);
-       dump_cmd_in_x_mode(new_env);
-
-       if (!argv[assignment_cnt]) {
-               /* Case when we are here: ... | var=val | ...
-                * (note that we do not exit early, i.e., do not optimize out
-                * expand_assignments(): think about ... | var=`sleep 1` | ...
-                */
-               free_strings(new_env);
-               _exit(EXIT_SUCCESS);
+       /* note: can't move it above ch == dquote_end check! */
+       if (ch == EOF) {
+               syntax_error_unterm_ch('"');
+               /*xfunc_die(); - redundant */
        }
-
-#if BB_MMU
-       set_vars_and_save_old(new_env);
-       free(new_env); /* optional */
-       /* we can also destroy set_vars_and_save_old's return value,
-        * to save memory */
-#else
-       nommu_save->new_env = new_env;
-       nommu_save->old_vars = set_vars_and_save_old(new_env);
-#endif
-
-       if (argv_expanded) {
-               argv = argv_expanded;
-       } else {
-               argv = expand_strvec_to_strvec(argv + assignment_cnt);
-#if !BB_MMU
-               nommu_save->argv = argv;
-#endif
+       next = '\0';
+       if (ch != '\n') {
+               next = i_peek(input);
        }
-       dump_cmd_in_x_mode(argv);
-
-#if ENABLE_FEATURE_SH_STANDALONE || BB_MMU
-       if (strchr(argv[0], '/') != NULL)
-               goto skip;
-#endif
-
-       /* Check if the command matches any of the builtins.
-        * Depending on context, this might be redundant.  But it's
-        * easier to waste a few CPU cycles than it is to figure out
-        * if this is one of those cases.
-        */
-       {
-               /* On NOMMU, it is more expensive to re-execute shell
-                * just in order to run echo or test builtin.
-                * It's better to skip it here and run corresponding
-                * non-builtin later. */
-               const struct built_in_command *x;
-               x = BB_MMU ? find_builtin(argv[0]) : find_builtin1(argv[0]);
-               if (x) {
-                       exec_builtin(&nommu_save->argv_from_re_execing, x, argv);
+       debug_printf_parse("\" ch=%c (%d) escape=%d\n",
+                       ch, ch, !!(dest->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+       if (ch == '\\') {
+               if (next == EOF) {
+                       syntax_error("\\<eof>");
+                       xfunc_die();
                }
-       }
-#if ENABLE_HUSH_FUNCTIONS
-       /* Check if the command matches any functions */
-       {
-               const struct function *funcp = find_function(argv[0]);
-               if (funcp) {
-                       exec_function(&nommu_save->argv_from_re_execing, funcp, argv);
+               /* bash:
+                * "The backslash retains its special meaning [in "..."]
+                * only when followed by one of the following characters:
+                * $, `, ", \, or <newline>.  A double quote may be quoted
+                * within double quotes by preceding it with a backslash."
+                * NB: in (unquoted) heredoc, above does not apply to ".
+                */
+               if (next == dquote_end || strchr("$`\\\n", next) != NULL) {
+                       ch = i_getch(input);
+                       if (ch != '\n') {
+                               o_addqchr(dest, ch);
+                               nommu_addchr(as_string, ch);
+                       }
+               } else {
+                       o_addqchr(dest, '\\');
+                       nommu_addchr(as_string, '\\');
                }
+               goto again;
        }
-#endif
-
-#if ENABLE_FEATURE_SH_STANDALONE
-       /* Check if the command matches any busybox applets */
-       {
-               int a = find_applet_by_name(argv[0]);
-               if (a >= 0) {
-# if BB_MMU /* see above why on NOMMU it is not allowed */
-                       if (APPLET_IS_NOEXEC(a)) {
-                               debug_printf_exec("running applet '%s'\n", argv[0]);
-                               run_applet_no_and_exit(a, argv);
-                       }
-# endif
-                       /* Re-exec ourselves */
-                       debug_printf_exec("re-execing applet '%s'\n", argv[0]);
-                       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
-                       execv(bb_busybox_exec_path, argv);
-                       /* If they called chroot or otherwise made the binary no longer
-                        * executable, fall through */
+       if (ch == '$') {
+               if (parse_dollar(as_string, dest, input) != 0) {
+                       debug_printf_parse("parse_stream_dquoted return 1: "
+                                       "parse_dollar returned non-0\n");
+                       return 1;
                }
+               goto again;
        }
-#endif
-
-#if ENABLE_FEATURE_SH_STANDALONE || BB_MMU
- skip:
-#endif
-       execvp_or_die(argv);
-}
-
-/* Called after [v]fork() in run_pipe
- */
-static void pseudo_exec(nommu_save_t *nommu_save,
-               struct command *command,
-               char **argv_expanded) NORETURN;
-static void pseudo_exec(nommu_save_t *nommu_save,
-               struct command *command,
-               char **argv_expanded)
-{
-       if (command->argv) {
-               pseudo_exec_argv(nommu_save, command->argv,
-                               command->assignment_cnt, argv_expanded);
+#if ENABLE_HUSH_TICK
+       if (ch == '`') {
+               //unsigned pos = dest->length;
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+               o_addchr(dest, 0x80 | '`');
+               add_till_backquote(dest, input);
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+               //debug_printf_subst("SUBST RES3 '%s'\n", dest->data + pos);
+               goto again;
        }
-
-       if (command->group) {
-               /* Cases when we are here:
-                * ( list )
-                * { list } &
-                * ... | ( list ) | ...
-                * ... | { list } | ...
-                */
-#if BB_MMU
-               int rcode;
-               debug_printf_exec("pseudo_exec: run_list\n");
-               reset_traps_to_defaults();
-               rcode = run_list(command->group);
-               /* OK to leak memory by not calling free_pipe_list,
-                * since this process is about to exit */
-               _exit(rcode);
-#else
-               re_execute_shell(&nommu_save->argv_from_re_execing,
-                               command->group_as_string,
-                               G.global_argv[0],
-                               G.global_argv + 1,
-                               NULL);
 #endif
+       o_addQchr(dest, ch);
+       if (ch == '='
+        && (dest->o_assignment == MAYBE_ASSIGNMENT
+           || dest->o_assignment == WORD_IS_KEYWORD)
+        && is_well_formed_var_name(dest->data, '=')
+       ) {
+               dest->o_assignment = DEFINITELY_ASSIGNMENT;
        }
-
-       /* Case when we are here: ... | >file */
-       debug_printf_exec("pseudo_exec'ed null command\n");
-       _exit(EXIT_SUCCESS);
+       goto again;
+#undef as_string
 }
 
-#if ENABLE_HUSH_JOB
-static const char *get_cmdtext(struct pipe *pi)
+/*
+ * Scan input until EOF or end_trigger char.
+ * Return a list of pipes to execute, or NULL on EOF
+ * or if end_trigger character is met.
+ * On syntax error, exit is shell is not interactive,
+ * reset parsing machinery and start parsing anew,
+ * or return ERR_PTR.
+ */
+static struct pipe *parse_stream(char **pstring,
+               struct in_str *input,
+               int end_trigger)
 {
-       char **argv;
-       char *p;
-       int len;
+       struct parse_context ctx;
+       o_string dest = NULL_O_STRING;
+       int is_in_dquote;
+       int heredoc_cnt;
 
-       /* This is subtle. ->cmdtext is created only on first backgrounding.
-        * (Think "cat, <ctrl-z>, fg, <ctrl-z>, fg, <ctrl-z>...." here...)
-        * On subsequent bg argv is trashed, but we won't use it */
-       if (pi->cmdtext)
-               return pi->cmdtext;
-       argv = pi->cmds[0].argv;
-       if (!argv || !argv[0]) {
-               pi->cmdtext = xzalloc(1);
-               return pi->cmdtext;
-       }
+       /* Double-quote state is handled in the state variable is_in_dquote.
+        * A single-quote triggers a bypass of the main loop until its mate is
+        * found.  When recursing, quote state is passed in via dest->o_expflags.
+        */
+       debug_printf_parse("parse_stream entered, end_trigger='%c'\n",
+                       end_trigger ? end_trigger : 'X');
+       debug_enter();
 
-       len = 0;
-       do {
-               len += strlen(*argv) + 1;
-       } while (*++argv);
-       p = xmalloc(len);
-       pi->cmdtext = p;
-       argv = pi->cmds[0].argv;
-       do {
-               len = strlen(*argv);
-               memcpy(p, *argv, len);
-               p += len;
-               *p++ = ' ';
-       } while (*++argv);
-       p[-1] = '\0';
-       return pi->cmdtext;
-}
-
-static void insert_bg_job(struct pipe *pi)
-{
-       struct pipe *job, **jobp;
-       int i;
-
-       /* Linear search for the ID of the job to use */
-       pi->jobid = 1;
-       for (job = G.job_list; job; job = job->next)
-               if (job->jobid >= pi->jobid)
-                       pi->jobid = job->jobid + 1;
-
-       /* Add job to the list of running jobs */
-       jobp = &G.job_list;
-       while ((job = *jobp) != NULL)
-               jobp = &job->next;
-       job = *jobp = xmalloc(sizeof(*job));
-
-       *job = *pi; /* physical copy */
-       job->next = NULL;
-       job->cmds = xzalloc(sizeof(pi->cmds[0]) * pi->num_cmds);
-       /* Cannot copy entire pi->cmds[] vector! This causes double frees */
-       for (i = 0; i < pi->num_cmds; i++) {
-               job->cmds[i].pid = pi->cmds[i].pid;
-               /* all other fields are not used and stay zero */
-       }
-       job->cmdtext = xstrdup(get_cmdtext(pi));
-
-       if (G_interactive_fd)
-               printf("[%d] %d %s\n", job->jobid, job->cmds[0].pid, job->cmdtext);
-       /* Last command's pid goes to $! */
-       G.last_bg_pid = job->cmds[job->num_cmds - 1].pid;
-       G.last_jobid = job->jobid;
-}
+       /* If very first arg is "" or '', dest.data may end up NULL.
+        * Preventing this: */
+       o_addchr(&dest, '\0');
+       dest.length = 0;
 
-static void remove_bg_job(struct pipe *pi)
-{
-       struct pipe *prev_pipe;
+       /* We used to separate words on $IFS here. This was wrong.
+        * $IFS is used only for word splitting when $var is expanded,
+        * here we should use blank chars as separators, not $iFS
+        */
+ reset:
+#if ENABLE_HUSH_INTERACTIVE
+       input->promptmode = 0; /* PS1 */
+#endif
+       /* dest.o_assignment = MAYBE_ASSIGNMENT; - already is */
+       initialize_context(&ctx);
+       is_in_dquote = 0;
+       heredoc_cnt = 0;
+       while (1) {
+               const char *is_blank;
+               const char *is_special;
+               int ch;
+               int next;
+               int redir_fd;
+               redir_type redir_style;
 
-       if (pi == G.job_list) {
-               G.job_list = pi->next;
-       } else {
-               prev_pipe = G.job_list;
-               while (prev_pipe->next != pi)
-                       prev_pipe = prev_pipe->next;
-               prev_pipe->next = pi->next;
-       }
-       if (G.job_list)
-               G.last_jobid = G.job_list->jobid;
-       else
-               G.last_jobid = 0;
-}
+               if (is_in_dquote) {
+                       /* dest.has_quoted_part = 1; - already is (see below) */
+                       if (parse_stream_dquoted(&ctx.as_string, &dest, input, '"')) {
+                               goto parse_error;
+                       }
+                       /* We reached closing '"' */
+                       is_in_dquote = 0;
+               }
+               ch = i_getch(input);
+               debug_printf_parse(": ch=%c (%d) escape=%d\n",
+                               ch, ch, !!(dest.o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+               if (ch == EOF) {
+                       struct pipe *pi;
 
-/* Remove a backgrounded job */
-static void delete_finished_bg_job(struct pipe *pi)
-{
-       remove_bg_job(pi);
-       pi->stopped_cmds = 0;
-       free_pipe(pi);
-       free(pi);
-}
-#endif /* JOB */
+                       if (heredoc_cnt) {
+                               syntax_error_unterm_str("here document");
+                               goto parse_error;
+                       }
+                       /* end_trigger == '}' case errors out earlier,
+                        * checking only ')' */
+                       if (end_trigger == ')') {
+                               syntax_error_unterm_ch('('); /* exits */
+                               /* goto parse_error; */
+                       }
 
-/* Check to see if any processes have exited -- if they
- * have, figure out why and see if a job has completed */
-static int checkjobs(struct pipe* fg_pipe)
-{
-       int attributes;
-       int status;
-#if ENABLE_HUSH_JOB
-       struct pipe *pi;
+                       if (done_word(&dest, &ctx)) {
+                               goto parse_error;
+                       }
+                       o_free(&dest);
+                       done_pipe(&ctx, PIPE_SEQ);
+                       pi = ctx.list_head;
+                       /* If we got nothing... */
+                       /* (this makes bare "&" cmd a no-op.
+                        * bash says: "syntax error near unexpected token '&'") */
+                       if (pi->num_cmds == 0
+                           IF_HAS_KEYWORDS( && pi->res_word == RES_NONE)
+                       ) {
+                               free_pipe_list(pi);
+                               pi = NULL;
+                       }
+#if !BB_MMU
+                       debug_printf_parse("as_string '%s'\n", ctx.as_string.data);
+                       if (pstring)
+                               *pstring = ctx.as_string.data;
+                       else
+                               o_free_unsafe(&ctx.as_string);
 #endif
-       pid_t childpid;
-       int rcode = 0;
-
-       debug_printf_jobs("checkjobs %p\n", fg_pipe);
+                       debug_leave();
+                       debug_printf_parse("parse_stream return %p\n", pi);
+                       return pi;
+               }
+               nommu_addchr(&ctx.as_string, ch);
 
-       attributes = WUNTRACED;
-       if (fg_pipe == NULL)
-               attributes |= WNOHANG;
+               next = '\0';
+               if (ch != '\n')
+                       next = i_peek(input);
 
-       errno = 0;
-#if ENABLE_HUSH_FAST
-       if (G.handled_SIGCHLD == G.count_SIGCHLD) {
-//bb_error_msg("[%d] checkjobs: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d children?:%d fg_pipe:%p",
-//getpid(), G.count_SIGCHLD, G.handled_SIGCHLD, G.we_have_children, fg_pipe);
-               /* There was neither fork nor SIGCHLD since last waitpid */
-               /* Avoid doing waitpid syscall if possible */
-               if (!G.we_have_children) {
-                       errno = ECHILD;
-                       return -1;
-               }
-               if (fg_pipe == NULL) { /* is WNOHANG set? */
-                       /* We have children, but they did not exit
-                        * or stop yet (we saw no SIGCHLD) */
-                       return 0;
+               is_special = "{}<>;&|()#'" /* special outside of "str" */
+                               "\\$\"" IF_HUSH_TICK("`"); /* always special */
+               /* Are { and } special here? */
+               if (ctx.command->argv /* word [word]{... - non-special */
+                || dest.length       /* word{... - non-special */
+                || dest.has_quoted_part     /* ""{... - non-special */
+                || (next != ';'             /* }; - special */
+                   && next != ')'           /* }) - special */
+                   && next != '&'           /* }& and }&& ... - special */
+                   && next != '|'           /* }|| ... - special */
+                   && !strchr(defifs, next) /* {word - non-special */
+                   )
+               ) {
+                       /* They are not special, skip "{}" */
+                       is_special += 2;
                }
-               /* else: !WNOHANG, waitpid will block, can't short-circuit */
-       }
-#endif
+               is_special = strchr(is_special, ch);
+               is_blank = strchr(defifs, ch);
 
-/* Do we do this right?
- * bash-3.00# sleep 20 | false
- * <ctrl-Z pressed>
- * [3]+  Stopped          sleep 20 | false
- * bash-3.00# echo $?
- * 1   <========== bg pipe is not fully done, but exitcode is already known!
- * [hush 1.14.0: yes we do it right]
- */
- wait_more:
-       while (1) {
-               int i;
-               int dead;
+               if (!is_special && !is_blank) { /* ordinary char */
+ ordinary_char:
+                       o_addQchr(&dest, ch);
+                       if ((dest.o_assignment == MAYBE_ASSIGNMENT
+                           || dest.o_assignment == WORD_IS_KEYWORD)
+                        && ch == '='
+                        && is_well_formed_var_name(dest.data, '=')
+                       ) {
+                               dest.o_assignment = DEFINITELY_ASSIGNMENT;
+                       }
+                       continue;
+               }
 
-#if ENABLE_HUSH_FAST
-               i = G.count_SIGCHLD;
-#endif
-               childpid = waitpid(-1, &status, attributes);
-               if (childpid <= 0) {
-                       if (childpid && errno != ECHILD)
-                               bb_perror_msg("waitpid");
-#if ENABLE_HUSH_FAST
-                       else { /* Until next SIGCHLD, waitpid's are useless */
-                               G.we_have_children = (childpid == 0);
-                               G.handled_SIGCHLD = i;
-//bb_error_msg("[%d] checkjobs: waitpid returned <= 0, G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+               if (is_blank) {
+                       if (done_word(&dest, &ctx)) {
+                               goto parse_error;
                        }
+                       if (ch == '\n') {
+#if ENABLE_HUSH_CASE
+                               /* "case ... in <newline> word) ..." -
+                                * newlines are ignored (but ';' wouldn't be) */
+                               if (ctx.command->argv == NULL
+                                && ctx.ctx_res_w == RES_MATCH
+                               ) {
+                                       continue;
+                               }
 #endif
-                       break;
+                               /* Treat newline as a command separator. */
+                               done_pipe(&ctx, PIPE_SEQ);
+                               debug_printf_parse("heredoc_cnt:%d\n", heredoc_cnt);
+                               if (heredoc_cnt) {
+                                       if (fetch_heredocs(heredoc_cnt, &ctx, input)) {
+                                               goto parse_error;
+                                       }
+                                       heredoc_cnt = 0;
+                               }
+                               dest.o_assignment = MAYBE_ASSIGNMENT;
+                               ch = ';';
+                               /* note: if (is_blank) continue;
+                                * will still trigger for us */
+                       }
                }
-               dead = WIFEXITED(status) || WIFSIGNALED(status);
 
-#if DEBUG_JOBS
-               if (WIFSTOPPED(status))
-                       debug_printf_jobs("pid %d stopped by sig %d (exitcode %d)\n",
-                                       childpid, WSTOPSIG(status), WEXITSTATUS(status));
-               if (WIFSIGNALED(status))
-                       debug_printf_jobs("pid %d killed by sig %d (exitcode %d)\n",
-                                       childpid, WTERMSIG(status), WEXITSTATUS(status));
-               if (WIFEXITED(status))
-                       debug_printf_jobs("pid %d exited, exitcode %d\n",
-                                       childpid, WEXITSTATUS(status));
-#endif
-               /* Were we asked to wait for fg pipe? */
-               if (fg_pipe) {
-                       for (i = 0; i < fg_pipe->num_cmds; i++) {
-                               debug_printf_jobs("check pid %d\n", fg_pipe->cmds[i].pid);
-                               if (fg_pipe->cmds[i].pid != childpid)
-                                       continue;
-                               if (dead) {
-                                       fg_pipe->cmds[i].pid = 0;
-                                       fg_pipe->alive_cmds--;
-                                       if (i == fg_pipe->num_cmds - 1) {
-                                               /* last process gives overall exitstatus */
-                                               rcode = WEXITSTATUS(status);
-                                               /* bash prints killer signal's name for *last*
-                                                * process in pipe (prints just newline for SIGINT).
-                                                * Mimic this. Example: "sleep 5" + (^\ or kill -QUIT)
-                                                */
-                                               if (WIFSIGNALED(status)) {
-                                                       int sig = WTERMSIG(status);
-                                                       printf("%s\n", sig == SIGINT ? "" : get_signame(sig));
-                                                       /* TODO: MIPS has 128 sigs (1..128), what if sig==128 here?
-                                                        * Maybe we need to use sig | 128? */
-                                                       rcode = sig + 128;
-                                               }
-                                               IF_HAS_KEYWORDS(if (fg_pipe->pi_inverted) rcode = !rcode;)
-                                       }
-                               } else {
-                                       fg_pipe->cmds[i].is_stopped = 1;
-                                       fg_pipe->stopped_cmds++;
-                               }
-                               debug_printf_jobs("fg_pipe: alive_cmds %d stopped_cmds %d\n",
-                                               fg_pipe->alive_cmds, fg_pipe->stopped_cmds);
-                               if (fg_pipe->alive_cmds - fg_pipe->stopped_cmds <= 0) {
-                                       /* All processes in fg pipe have exited or stopped */
-/* Note: *non-interactive* bash does not continue if all processes in fg pipe
- * are stopped. Testcase: "cat | cat" in a script (not on command line!)
- * and "killall -STOP cat" */
-                                       if (G_interactive_fd) {
-#if ENABLE_HUSH_JOB
-                                               if (fg_pipe->alive_cmds)
-                                                       insert_bg_job(fg_pipe);
-#endif
-                                               return rcode;
-                                       }
-                                       if (!fg_pipe->alive_cmds)
-                                               return rcode;
-                               }
-                               /* There are still running processes in the fg pipe */
-                               goto wait_more; /* do waitpid again */
+               /* "cmd}" or "cmd }..." without semicolon or &:
+                * } is an ordinary char in this case, even inside { cmd; }
+                * Pathological example: { ""}; } should exec "}" cmd
+                */
+               if (ch == '}') {
+                       if (!IS_NULL_CMD(ctx.command) /* cmd } */
+                        || dest.length != 0 /* word} */
+                        || dest.has_quoted_part    /* ""} */
+                       ) {
+                               goto ordinary_char;
                        }
-                       /* it wasnt fg_pipe, look for process in bg pipes */
+                       if (!IS_NULL_PIPE(ctx.pipe)) /* cmd | } */
+                               goto skip_end_trigger;
+                       /* else: } does terminate a group */
                }
 
-#if ENABLE_HUSH_JOB
-               /* We asked to wait for bg or orphaned children */
-               /* No need to remember exitcode in this case */
-               for (pi = G.job_list; pi; pi = pi->next) {
-                       for (i = 0; i < pi->num_cmds; i++) {
-                               if (pi->cmds[i].pid == childpid)
-                                       goto found_pi_and_prognum;
+               if (end_trigger && end_trigger == ch
+                && (ch != ';' || heredoc_cnt == 0)
+#if ENABLE_HUSH_CASE
+                && (ch != ')'
+                   || ctx.ctx_res_w != RES_MATCH
+                   || (!dest.has_quoted_part && strcmp(dest.data, "esac") == 0)
+                   )
+#endif
+               ) {
+                       if (heredoc_cnt) {
+                               /* This is technically valid:
+                                * { cat <<HERE; }; echo Ok
+                                * heredoc
+                                * heredoc
+                                * HERE
+                                * but we don't support this.
+                                * We require heredoc to be in enclosing {}/(),
+                                * if any.
+                                */
+                               syntax_error_unterm_str("here document");
+                               goto parse_error;
+                       }
+                       if (done_word(&dest, &ctx)) {
+                               goto parse_error;
+                       }
+                       done_pipe(&ctx, PIPE_SEQ);
+                       dest.o_assignment = MAYBE_ASSIGNMENT;
+                       /* Do we sit outside of any if's, loops or case's? */
+                       if (!HAS_KEYWORDS
+                        IF_HAS_KEYWORDS(|| (ctx.ctx_res_w == RES_NONE && ctx.old_flag == 0))
+                       ) {
+                               o_free(&dest);
+#if !BB_MMU
+                               debug_printf_parse("as_string '%s'\n", ctx.as_string.data);
+                               if (pstring)
+                                       *pstring = ctx.as_string.data;
+                               else
+                                       o_free_unsafe(&ctx.as_string);
+#endif
+                               debug_leave();
+                               debug_printf_parse("parse_stream return %p: "
+                                               "end_trigger char found\n",
+                                               ctx.list_head);
+                               return ctx.list_head;
                        }
                }
-               /* Happens when shell is used as init process (init=/bin/sh) */
-               debug_printf("checkjobs: pid %d was not in our list!\n", childpid);
-               continue; /* do waitpid again */
+ skip_end_trigger:
+               if (is_blank)
+                       continue;
 
- found_pi_and_prognum:
-               if (dead) {
-                       /* child exited */
-                       pi->cmds[i].pid = 0;
-                       pi->alive_cmds--;
-                       if (!pi->alive_cmds) {
-                               if (G_interactive_fd)
-                                       printf(JOB_STATUS_FORMAT, pi->jobid,
-                                                       "Done", pi->cmdtext);
-                               delete_finished_bg_job(pi);
+               /* Catch <, > before deciding whether this word is
+                * an assignment. a=1 2>z b=2: b=2 is still assignment */
+               switch (ch) {
+               case '>':
+                       redir_fd = redirect_opt_num(&dest);
+                       if (done_word(&dest, &ctx)) {
+                               goto parse_error;
+                       }
+                       redir_style = REDIRECT_OVERWRITE;
+                       if (next == '>') {
+                               redir_style = REDIRECT_APPEND;
+                               ch = i_getch(input);
+                               nommu_addchr(&ctx.as_string, ch);
+                       }
+#if 0
+                       else if (next == '(') {
+                               syntax_error(">(process) not supported");
+                               goto parse_error;
                        }
-               } else {
-                       /* child stopped */
-                       pi->cmds[i].is_stopped = 1;
-                       pi->stopped_cmds++;
-               }
 #endif
-       } /* while (waitpid succeeds)... */
-
-       return rcode;
-}
-
-#if ENABLE_HUSH_JOB
-static int checkjobs_and_fg_shell(struct pipe* fg_pipe)
-{
-       pid_t p;
-       int rcode = checkjobs(fg_pipe);
-       if (G_saved_tty_pgrp) {
-               /* Job finished, move the shell to the foreground */
-               p = getpgrp(); /* our process group id */
-               debug_printf_jobs("fg'ing ourself: getpgrp()=%d\n", (int)p);
-               tcsetpgrp(G_interactive_fd, p);
-       }
-       return rcode;
-}
+                       if (parse_redirect(&ctx, redir_fd, redir_style, input))
+                               goto parse_error;
+                       continue; /* back to top of while (1) */
+               case '<':
+                       redir_fd = redirect_opt_num(&dest);
+                       if (done_word(&dest, &ctx)) {
+                               goto parse_error;
+                       }
+                       redir_style = REDIRECT_INPUT;
+                       if (next == '<') {
+                               redir_style = REDIRECT_HEREDOC;
+                               heredoc_cnt++;
+                               debug_printf_parse("++heredoc_cnt=%d\n", heredoc_cnt);
+                               ch = i_getch(input);
+                               nommu_addchr(&ctx.as_string, ch);
+                       } else if (next == '>') {
+                               redir_style = REDIRECT_IO;
+                               ch = i_getch(input);
+                               nommu_addchr(&ctx.as_string, ch);
+                       }
+#if 0
+                       else if (next == '(') {
+                               syntax_error("<(process) not supported");
+                               goto parse_error;
+                       }
 #endif
+                       if (parse_redirect(&ctx, redir_fd, redir_style, input))
+                               goto parse_error;
+                       continue; /* back to top of while (1) */
+               }
 
-/* Start all the jobs, but don't wait for anything to finish.
- * See checkjobs().
- *
- * Return code is normally -1, when the caller has to wait for children
- * to finish to determine the exit status of the pipe.  If the pipe
- * is a simple builtin command, however, the action is done by the
- * time run_pipe returns, and the exit code is provided as the
- * return value.
- *
- * Returns -1 only if started some children. IOW: we have to
- * mask out retvals of builtins etc with 0xff!
- *
- * The only case when we do not need to [v]fork is when the pipe
- * is single, non-backgrounded, non-subshell command. Examples:
- * cmd ; ...   { list } ; ...
- * cmd && ...  { list } && ...
- * cmd || ...  { list } || ...
- * If it is, then we can run cmd as a builtin, NOFORK [do we do this?],
- * or (if SH_STANDALONE) an applet, and we can run the { list }
- * with run_list. If it isn't one of these, we fork and exec cmd.
- *
- * Cases when we must fork:
- * non-single:   cmd | cmd
- * backgrounded: cmd &     { list } &
- * subshell:     ( list ) [&]
- */
-static NOINLINE int run_pipe(struct pipe *pi)
-{
-       static const char *const null_ptr = NULL;
-
-       int cmd_no;
-       int next_infd;
-       struct command *command;
-       char **argv_expanded;
-       char **argv;
-       char *p;
-       /* it is not always needed, but we aim to smaller code */
-       int squirrel[] = { -1, -1, -1 };
-       int rcode;
-
-       debug_printf_exec("run_pipe start: members:%d\n", pi->num_cmds);
-       debug_enter();
-
-       IF_HUSH_JOB(pi->pgrp = -1;)
-       pi->stopped_cmds = 0;
-       command = &pi->cmds[0];
-       argv_expanded = NULL;
-
-       if (pi->num_cmds != 1
-        || pi->followup == PIPE_BG
-        || command->cmd_type == CMD_SUBSHELL
-       ) {
-               goto must_fork;
-       }
+               if (dest.o_assignment == MAYBE_ASSIGNMENT
+                /* check that we are not in word in "a=1 2>word b=1": */
+                && !ctx.pending_redirect
+               ) {
+                       /* ch is a special char and thus this word
+                        * cannot be an assignment */
+                       dest.o_assignment = NOT_ASSIGNMENT;
+               }
 
-       pi->alive_cmds = 1;
+               /* Note: nommu_addchr(&ctx.as_string, ch) is already done */
 
-       debug_printf_exec(": group:%p argv:'%s'\n",
-               command->group, command->argv ? command->argv[0] : "NONE");
-
-       if (command->group) {
-#if ENABLE_HUSH_FUNCTIONS
-               if (command->cmd_type == CMD_FUNCDEF) {
-                       /* "executing" func () { list } */
-                       struct function *funcp;
+               switch (ch) {
+               case '#':
+                       if (dest.length == 0) {
+                               while (1) {
+                                       ch = i_peek(input);
+                                       if (ch == EOF || ch == '\n')
+                                               break;
+                                       i_getch(input);
+                                       /* note: we do not add it to &ctx.as_string */
+                               }
+                               nommu_addchr(&ctx.as_string, '\n');
+                       } else {
+                               o_addQchr(&dest, ch);
+                       }
+                       break;
+               case '\\':
+                       if (next == EOF) {
+                               syntax_error("\\<eof>");
+                               xfunc_die();
+                       }
+                       ch = i_getch(input);
+                       if (ch != '\n') {
+                               o_addchr(&dest, '\\');
+                               /*nommu_addchr(&ctx.as_string, '\\'); - already done */
+                               o_addchr(&dest, ch);
+                               nommu_addchr(&ctx.as_string, ch);
+                               /* Example: echo Hello \2>file
+                                * we need to know that word 2 is quoted */
+                               dest.has_quoted_part = 1;
+                       }
+#if !BB_MMU
+                       else {
+                               /* It's "\<newline>". Remove trailing '\' from ctx.as_string */
+                               ctx.as_string.data[--ctx.as_string.length] = '\0';
+                       }
+#endif
+                       break;
+               case '$':
+                       if (parse_dollar(&ctx.as_string, &dest, input) != 0) {
+                               debug_printf_parse("parse_stream parse error: "
+                                       "parse_dollar returned non-0\n");
+                               goto parse_error;
+                       }
+                       break;
+               case '\'':
+                       dest.has_quoted_part = 1;
+                       while (1) {
+                               ch = i_getch(input);
+                               if (ch == EOF) {
+                                       syntax_error_unterm_ch('\'');
+                                       /*xfunc_die(); - redundant */
+                               }
+                               nommu_addchr(&ctx.as_string, ch);
+                               if (ch == '\'')
+                                       break;
+                               o_addqchr(&dest, ch);
+                       }
+                       break;
+               case '"':
+                       dest.has_quoted_part = 1;
+                       is_in_dquote ^= 1; /* invert */
+                       if (dest.o_assignment == NOT_ASSIGNMENT)
+                               dest.o_expflags ^= EXP_FLAG_ESC_GLOB_CHARS;
+                       break;
+#if ENABLE_HUSH_TICK
+               case '`': {
+                       unsigned pos;
 
-                       funcp = new_function(command->argv[0]);
-                       /* funcp->name is already set to argv[0] */
-                       funcp->body = command->group;
+                       o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+                       o_addchr(&dest, '`');
+                       pos = dest.length;
+                       add_till_backquote(&dest, input);
 # if !BB_MMU
-                       funcp->body_as_string = command->group_as_string;
-                       command->group_as_string = NULL;
+                       o_addstr(&ctx.as_string, dest.data + pos);
+                       o_addchr(&ctx.as_string, '`');
 # endif
-                       command->group = NULL;
-                       command->argv[0] = NULL;
-                       debug_printf_exec("cmd %p has child func at %p\n", command, funcp);
-                       funcp->parent_cmd = command;
-                       command->child_func = funcp;
-
-                       debug_printf_exec("run_pipe: return EXIT_SUCCESS\n");
-                       debug_leave();
-                       return EXIT_SUCCESS;
+                       o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+                       //debug_printf_subst("SUBST RES3 '%s'\n", dest.data + pos);
+                       break;
                }
 #endif
-               /* { list } */
-               debug_printf("non-subshell group\n");
-               rcode = 1; /* exitcode if redir failed */
-               if (setup_redirects(command, squirrel) == 0) {
-                       debug_printf_exec(": run_list\n");
-                       rcode = run_list(command->group) & 0xff;
-               }
-               restore_redirects(squirrel);
-               IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
-               debug_leave();
-               debug_printf_exec("run_pipe: return %d\n", rcode);
-               return rcode;
-       }
-
-       argv = command->argv ? command->argv : (char **) &null_ptr;
-       {
-               const struct built_in_command *x;
-#if ENABLE_HUSH_FUNCTIONS
-               const struct function *funcp;
-#else
-               enum { funcp = 0 };
+               case ';':
+#if ENABLE_HUSH_CASE
+ case_semi:
 #endif
-               char **new_env = NULL;
-               struct variable *old_vars = NULL;
-
-               if (argv[command->assignment_cnt] == NULL) {
-                       /* Assignments, but no command */
-                       /* Ensure redirects take effect (that is, create files).
-                        * Try "a=t >file": */
-                       rcode = setup_redirects(command, squirrel);
-                       restore_redirects(squirrel);
-                       /* Set shell variables */
-                       if (G.x_mode)
-                               bb_putchar_stderr('+');
-                       while (*argv) {
-                               p = expand_string_to_string(*argv);
-                               if (G.x_mode)
-                                       fprintf(stderr, " %s", p);
-                               debug_printf_exec("set shell var:'%s'->'%s'\n",
-                                               *argv, p);
-                               set_local_var(p, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
-                               argv++;
+                       if (done_word(&dest, &ctx)) {
+                               goto parse_error;
                        }
-                       if (G.x_mode)
-                               bb_putchar_stderr('\n');
-                       /* Redirect error sets $? to 1. Otherwise,
-                        * if evaluating assignment value set $?, retain it.
-                        * Try "false; q=`exit 2`; echo $?" - should print 2: */
-                       if (rcode == 0)
-                               rcode = G.last_exitcode;
-                       /* Do we need to flag set_local_var() errors?
-                        * "assignment to readonly var" and "putenv error"
-                        */
-                       IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
-                       debug_leave();
-                       debug_printf_exec("run_pipe: return %d\n", rcode);
-                       return rcode;
-               }
-
-               /* Expand the rest into (possibly) many strings each */
-               if (0) {}
-#if ENABLE_HUSH_BASH_COMPAT
-               else if (command->cmd_type == CMD_SINGLEWORD_NOGLOB) {
-                       argv_expanded = expand_strvec_to_strvec_singleword_noglob(argv + command->assignment_cnt);
-               }
-#endif
-#ifdef CMD_SINGLEWORD_NOGLOB_COND
-               else if (command->cmd_type == CMD_SINGLEWORD_NOGLOB_COND) {
-                       argv_expanded = expand_strvec_to_strvec_singleword_noglob_cond(argv + command->assignment_cnt);
-
-               }
-#endif
-               else {
-                       argv_expanded = expand_strvec_to_strvec(argv + command->assignment_cnt);
-               }
-
-               /* if someone gives us an empty string: `cmd with empty output` */
-               if (!argv_expanded[0]) {
-                       free(argv_expanded);
-                       debug_leave();
-                       return G.last_exitcode;
-               }
-
-               x = find_builtin(argv_expanded[0]);
-#if ENABLE_HUSH_FUNCTIONS
-               funcp = NULL;
-               if (!x)
-                       funcp = find_function(argv_expanded[0]);
-#endif
-               if (x || funcp) {
-                       if (!funcp) {
-                               if (x->b_function == builtin_exec && argv_expanded[1] == NULL) {
-                                       debug_printf("exec with redirects only\n");
-                                       rcode = setup_redirects(command, NULL);
-                                       goto clean_up_and_ret1;
+                       done_pipe(&ctx, PIPE_SEQ);
+#if ENABLE_HUSH_CASE
+                       /* Eat multiple semicolons, detect
+                        * whether it means something special */
+                       while (1) {
+                               ch = i_peek(input);
+                               if (ch != ';')
+                                       break;
+                               ch = i_getch(input);
+                               nommu_addchr(&ctx.as_string, ch);
+                               if (ctx.ctx_res_w == RES_CASE_BODY) {
+                                       ctx.ctx_dsemicolon = 1;
+                                       ctx.ctx_res_w = RES_MATCH;
+                                       break;
                                }
                        }
-                       /* setup_redirects acts on file descriptors, not FILEs.
-                        * This is perfect for work that comes after exec().
-                        * Is it really safe for inline use?  Experimentally,
-                        * things seem to work. */
-                       rcode = setup_redirects(command, squirrel);
-                       if (rcode == 0) {
-                               new_env = expand_assignments(argv, command->assignment_cnt);
-                               dump_cmd_in_x_mode(new_env);
-                               dump_cmd_in_x_mode(argv_expanded);
-                               old_vars = set_vars_and_save_old(new_env);
-                               if (!funcp) {
-                                       debug_printf_exec(": builtin '%s' '%s'...\n",
-                                               x->b_cmd, argv_expanded[1]);
-                                       rcode = x->b_function(argv_expanded) & 0xff;
-                                       fflush_all();
-                               }
-#if ENABLE_HUSH_FUNCTIONS
-                               else {
-# if ENABLE_HUSH_LOCAL
-                                       struct variable **sv;
-                                       sv = G.shadowed_vars_pp;
-                                       G.shadowed_vars_pp = &old_vars;
-# endif
-                                       debug_printf_exec(": function '%s' '%s'...\n",
-                                               funcp->name, argv_expanded[1]);
-                                       rcode = run_function(funcp, argv_expanded) & 0xff;
-# if ENABLE_HUSH_LOCAL
-                                       G.shadowed_vars_pp = sv;
-# endif
-                               }
 #endif
+ new_cmd:
+                       /* We just finished a cmd. New one may start
+                        * with an assignment */
+                       dest.o_assignment = MAYBE_ASSIGNMENT;
+                       break;
+               case '&':
+                       if (done_word(&dest, &ctx)) {
+                               goto parse_error;
                        }
- clean_up_and_ret:
-                       restore_redirects(squirrel);
-                       unset_vars(new_env);
-                       add_vars(old_vars);
- clean_up_and_ret1:
-                       free(argv_expanded);
-                       IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
-                       debug_leave();
-                       debug_printf_exec("run_pipe return %d\n", rcode);
-                       return rcode;
-               }
-
-               if (ENABLE_FEATURE_SH_STANDALONE) {
-                       int n = find_applet_by_name(argv_expanded[0]);
-                       if (n >= 0 && APPLET_IS_NOFORK(n)) {
-                               rcode = setup_redirects(command, squirrel);
-                               if (rcode == 0) {
-                                       new_env = expand_assignments(argv, command->assignment_cnt);
-                                       dump_cmd_in_x_mode(new_env);
-                                       dump_cmd_in_x_mode(argv_expanded);
-                                       old_vars = set_vars_and_save_old(new_env);
-                                       debug_printf_exec(": run_nofork_applet '%s' '%s'...\n",
-                                               argv_expanded[0], argv_expanded[1]);
-                                       rcode = run_nofork_applet(n, argv_expanded);
-                               }
-                               goto clean_up_and_ret;
+                       if (next == '&') {
+                               ch = i_getch(input);
+                               nommu_addchr(&ctx.as_string, ch);
+                               done_pipe(&ctx, PIPE_AND);
+                       } else {
+                               done_pipe(&ctx, PIPE_BG);
                        }
-               }
-               /* It is neither builtin nor applet. We must fork. */
-       }
-
- must_fork:
-       /* NB: argv_expanded may already be created, and that
-        * might include `cmd` runs! Do not rerun it! We *must*
-        * use argv_expanded if it's non-NULL */
-
-       /* Going to fork a child per each pipe member */
-       pi->alive_cmds = 0;
-       next_infd = 0;
-
-       cmd_no = 0;
-       while (cmd_no < pi->num_cmds) {
-               struct fd_pair pipefds;
+                       goto new_cmd;
+               case '|':
+                       if (done_word(&dest, &ctx)) {
+                               goto parse_error;
+                       }
+#if ENABLE_HUSH_CASE
+                       if (ctx.ctx_res_w == RES_MATCH)
+                               break; /* we are in case's "word | word)" */
+#endif
+                       if (next == '|') { /* || */
+                               ch = i_getch(input);
+                               nommu_addchr(&ctx.as_string, ch);
+                               done_pipe(&ctx, PIPE_OR);
+                       } else {
+                               /* we could pick up a file descriptor choice here
+                                * with redirect_opt_num(), but bash doesn't do it.
+                                * "echo foo 2| cat" yields "foo 2". */
+                               done_command(&ctx);
 #if !BB_MMU
-               volatile nommu_save_t nommu_save;
-               nommu_save.new_env = NULL;
-               nommu_save.old_vars = NULL;
-               nommu_save.argv = NULL;
-               nommu_save.argv_from_re_execing = NULL;
+                               o_reset_to_empty_unquoted(&ctx.as_string);
 #endif
-               command = &pi->cmds[cmd_no];
-               cmd_no++;
-               if (command->argv) {
-                       debug_printf_exec(": pipe member '%s' '%s'...\n",
-                                       command->argv[0], command->argv[1]);
-               } else {
-                       debug_printf_exec(": pipe member with no argv\n");
-               }
-
-               /* pipes are inserted between pairs of commands */
-               pipefds.rd = 0;
-               pipefds.wr = 1;
-               if (cmd_no < pi->num_cmds)
-                       xpiped_pair(pipefds);
-
-               command->pid = BB_MMU ? fork() : vfork();
-               if (!command->pid) { /* child */
-#if ENABLE_HUSH_JOB
-                       disable_restore_tty_pgrp_on_exit();
-                       CLEAR_RANDOM_T(&G.random_gen); /* or else $RANDOM repeats in child */
-
-                       /* Every child adds itself to new process group
-                        * with pgid == pid_of_first_child_in_pipe */
-                       if (G.run_list_level == 1 && G_interactive_fd) {
-                               pid_t pgrp;
-                               pgrp = pi->pgrp;
-                               if (pgrp < 0) /* true for 1st process only */
-                                       pgrp = getpid();
-                               if (setpgid(0, pgrp) == 0
-                                && pi->followup != PIPE_BG
-                                && G_saved_tty_pgrp /* we have ctty */
-                               ) {
-                                       /* We do it in *every* child, not just first,
-                                        * to avoid races */
-                                       tcsetpgrp(G_interactive_fd, pgrp);
-                               }
+                       }
+                       goto new_cmd;
+               case '(':
+#if ENABLE_HUSH_CASE
+                       /* "case... in [(]word)..." - skip '(' */
+                       if (ctx.ctx_res_w == RES_MATCH
+                        && ctx.command->argv == NULL /* not (word|(... */
+                        && dest.length == 0 /* not word(... */
+                        && dest.has_quoted_part == 0 /* not ""(... */
+                       ) {
+                               continue;
                        }
 #endif
-                       if (pi->alive_cmds == 0 && pi->followup == PIPE_BG) {
-                               /* 1st cmd in backgrounded pipe
-                                * should have its stdin /dev/null'ed */
-                               close(0);
-                               if (open(bb_dev_null, O_RDONLY))
-                                       xopen("/", O_RDONLY);
-                       } else {
-                               xmove_fd(next_infd, 0);
+               case '{':
+                       if (parse_group(&dest, &ctx, input, ch) != 0) {
+                               goto parse_error;
                        }
-                       xmove_fd(pipefds.wr, 1);
-                       if (pipefds.rd > 1)
-                               close(pipefds.rd);
-                       /* Like bash, explicit redirects override pipes,
-                        * and the pipe fd is available for dup'ing. */
-                       if (setup_redirects(command, NULL))
-                               _exit(1);
-
-                       /* Restore default handlers just prior to exec */
-                       /*signal(SIGCHLD, SIG_DFL); - so far we don't have any handlers */
-
-                       /* Stores to nommu_save list of env vars putenv'ed
-                        * (NOMMU, on MMU we don't need that) */
-                       /* cast away volatility... */
-                       pseudo_exec((nommu_save_t*) &nommu_save, command, argv_expanded);
-                       /* pseudo_exec() does not return */
+                       goto new_cmd;
+               case ')':
+#if ENABLE_HUSH_CASE
+                       if (ctx.ctx_res_w == RES_MATCH)
+                               goto case_semi;
+#endif
+               case '}':
+                       /* proper use of this character is caught by end_trigger:
+                        * if we see {, we call parse_group(..., end_trigger='}')
+                        * and it will match } earlier (not here). */
+                       syntax_error_unexpected_ch(ch);
+                       goto parse_error;
+               default:
+                       if (HUSH_DEBUG)
+                               bb_error_msg_and_die("BUG: unexpected %c\n", ch);
                }
+       } /* while (1) */
 
-               /* parent or error */
-#if ENABLE_HUSH_FAST
-               G.count_SIGCHLD++;
-//bb_error_msg("[%d] fork in run_pipe: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
-#endif
-               enable_restore_tty_pgrp_on_exit();
+ parse_error:
+       {
+               struct parse_context *pctx;
+               IF_HAS_KEYWORDS(struct parse_context *p2;)
+
+               /* Clean up allocated tree.
+                * Sample for finding leaks on syntax error recovery path.
+                * Run it from interactive shell, watch pmap `pidof hush`.
+                * while if false; then false; fi; do break; fi
+                * Samples to catch leaks at execution:
+                * while if (true | {true;}); then echo ok; fi; do break; done
+                * while if (true | {true;}); then echo ok; fi; do (if echo ok; break; then :; fi) | cat; break; done
+                */
+               pctx = &ctx;
+               do {
+                       /* Update pipe/command counts,
+                        * otherwise freeing may miss some */
+                       done_pipe(pctx, PIPE_SEQ);
+                       debug_printf_clean("freeing list %p from ctx %p\n",
+                                       pctx->list_head, pctx);
+                       debug_print_tree(pctx->list_head, 0);
+                       free_pipe_list(pctx->list_head);
+                       debug_printf_clean("freed list %p\n", pctx->list_head);
 #if !BB_MMU
-               /* Clean up after vforked child */
-               free(nommu_save.argv);
-               free(nommu_save.argv_from_re_execing);
-               unset_vars(nommu_save.new_env);
-               add_vars(nommu_save.old_vars);
+                       o_free_unsafe(&pctx->as_string);
 #endif
-               free(argv_expanded);
-               argv_expanded = NULL;
-               if (command->pid < 0) { /* [v]fork failed */
-                       /* Clearly indicate, was it fork or vfork */
-                       bb_perror_msg(BB_MMU ? "vfork"+1 : "vfork");
-               } else {
-                       pi->alive_cmds++;
-#if ENABLE_HUSH_JOB
-                       /* Second and next children need to know pid of first one */
-                       if (pi->pgrp < 0)
-                               pi->pgrp = command->pid;
+                       IF_HAS_KEYWORDS(p2 = pctx->stack;)
+                       if (pctx != &ctx) {
+                               free(pctx);
+                       }
+                       IF_HAS_KEYWORDS(pctx = p2;)
+               } while (HAS_KEYWORDS && pctx);
+               /* Free text, clear all dest fields */
+               o_free(&dest);
+               /* If we are not in top-level parse, we return,
+                * our caller will propagate error.
+                */
+               if (end_trigger != ';') {
+#if !BB_MMU
+                       if (pstring)
+                               *pstring = NULL;
 #endif
+                       debug_leave();
+                       return ERR_PTR;
                }
-
-               if (cmd_no > 1)
-                       close(next_infd);
-               if (cmd_no < pi->num_cmds)
-                       close(pipefds.wr);
-               /* Pass read (output) pipe end to next iteration */
-               next_infd = pipefds.rd;
+               /* Discard cached input, force prompt */
+               input->p = NULL;
+               IF_HUSH_INTERACTIVE(input->promptme = 1;)
+               goto reset;
        }
+}
 
-       if (!pi->alive_cmds) {
-               debug_leave();
-               debug_printf_exec("run_pipe return 1 (all forks failed, no children)\n");
-               return 1;
-       }
 
-       debug_leave();
-       debug_printf_exec("run_pipe return -1 (%u children started)\n", pi->alive_cmds);
-       return -1;
+/*** Execution routines ***/
+
+/* Expansion can recurse, need forward decls: */
+static char *expand_string_to_string(const char *str);
+static int process_command_subs(o_string *dest, const char *s);
+
+/* expand_strvec_to_strvec() takes a list of strings, expands
+ * all variable references within and returns a pointer to
+ * a list of expanded strings, possibly with larger number
+ * of strings. (Think VAR="a b"; echo $VAR).
+ * This new list is allocated as a single malloc block.
+ * NULL-terminated list of char* pointers is at the beginning of it,
+ * followed by strings themselves.
+ * Caller can deallocate entire list by single free(list). */
+
+/* Store given string, finalizing the word and starting new one whenever
+ * we encounter IFS char(s). This is used for expanding variable values.
+ * End-of-string does NOT finalize word: think about 'echo -$VAR-' */
+static int expand_on_ifs(o_string *output, int n, const char *str)
+{
+       while (1) {
+               int word_len = strcspn(str, G.ifs);
+               if (word_len) {
+                       if (output->o_expflags & EXP_FLAG_ESC_GLOB_CHARS)
+                               o_addqblock(output, str, word_len);
+                       else if (!(output->o_expflags & EXP_FLAG_GLOB))
+                               o_addblock(output, str, word_len);
+                       else /* if (!escape && glob) */ {
+                               /* Protect backslashes against globbing up :)
+                                * Example: "v='\*'; echo b$v"
+                                */
+                               o_addblock_duplicate_backslash(output, str, word_len);
+                               /*/ Why can't we do it easier? */
+                               /*o_addblock(output, str, word_len); - WRONG: "v='\*'; echo Z$v" prints "Z*" instead of "Z\*" */
+                               /*o_addqblock(output, str, word_len); - WRONG: "v='*'; echo Z$v" prints "Z*" instead of Z* files */
+                       }
+                       str += word_len;
+               }
+               if (!*str)  /* EOL - do not finalize word */
+                       break;
+               o_addchr(output, '\0');
+               debug_print_list("expand_on_ifs", output, n);
+               n = o_save_ptr(output, n);
+               str += strspn(str, G.ifs); /* skip ifs chars */
+       }
+       debug_print_list("expand_on_ifs[1]", output, n);
+       return n;
 }
 
-#ifndef debug_print_tree
-static void debug_print_tree(struct pipe *pi, int lvl)
+/* Helper to expand $((...)) and heredoc body. These act as if
+ * they are in double quotes, with the exception that they are not :).
+ * Just the rules are similar: "expand only $var and `cmd`"
+ *
+ * Returns malloced string.
+ * As an optimization, we return NULL if expansion is not needed.
+ */
+static char *expand_pseudo_dquoted(const char *str)
 {
-       static const char *const PIPE[] = {
-               [PIPE_SEQ] = "SEQ",
-               [PIPE_AND] = "AND",
-               [PIPE_OR ] = "OR" ,
-               [PIPE_BG ] = "BG" ,
-       };
-       static const char *RES[] = {
-               [RES_NONE ] = "NONE" ,
-# if ENABLE_HUSH_IF
-               [RES_IF   ] = "IF"   ,
-               [RES_THEN ] = "THEN" ,
-               [RES_ELIF ] = "ELIF" ,
-               [RES_ELSE ] = "ELSE" ,
-               [RES_FI   ] = "FI"   ,
-# endif
-# if ENABLE_HUSH_LOOPS
-               [RES_FOR  ] = "FOR"  ,
-               [RES_WHILE] = "WHILE",
-               [RES_UNTIL] = "UNTIL",
-               [RES_DO   ] = "DO"   ,
-               [RES_DONE ] = "DONE" ,
-# endif
-# if ENABLE_HUSH_LOOPS || ENABLE_HUSH_CASE
-               [RES_IN   ] = "IN"   ,
-# endif
-# if ENABLE_HUSH_CASE
-               [RES_CASE ] = "CASE" ,
-               [RES_CASE_IN ] = "CASE_IN" ,
-               [RES_MATCH] = "MATCH",
-               [RES_CASE_BODY] = "CASE_BODY",
-               [RES_ESAC ] = "ESAC" ,
-# endif
-               [RES_XXXX ] = "XXXX" ,
-               [RES_SNTX ] = "SNTX" ,
-       };
-       static const char *const CMDTYPE[] = {
-               "{}",
-               "()",
-               "[noglob]",
-# if ENABLE_HUSH_FUNCTIONS
-               "func()",
-# endif
-       };
-
-       int pin, prn;
-
-       pin = 0;
-       while (pi) {
-               fprintf(stderr, "%*spipe %d res_word=%s followup=%d %s\n", lvl*2, "",
-                               pin, RES[pi->res_word], pi->followup, PIPE[pi->followup]);
-               prn = 0;
-               while (prn < pi->num_cmds) {
-                       struct command *command = &pi->cmds[prn];
-                       char **argv = command->argv;
+       char *exp_str;
+       struct in_str input;
+       o_string dest = NULL_O_STRING;
 
-                       fprintf(stderr, "%*s cmd %d assignment_cnt:%d",
-                                       lvl*2, "", prn,
-                                       command->assignment_cnt);
-                       if (command->group) {
-                               fprintf(stderr, " group %s: (argv=%p)%s%s\n",
-                                               CMDTYPE[command->cmd_type],
-                                               argv
-# if !BB_MMU
-                                               , " group_as_string:", command->group_as_string
-# else
-                                               , "", ""
-# endif
-                               );
-                               debug_print_tree(command->group, lvl+1);
-                               prn++;
-                               continue;
-                       }
-                       if (argv) while (*argv) {
-                               fprintf(stderr, " '%s'", *argv);
-                               argv++;
-                       }
-                       fprintf(stderr, "\n");
-                       prn++;
-               }
-               pi = pi->next;
-               pin++;
+       if (!strchr(str, '$')
+        && !strchr(str, '\\')
+#if ENABLE_HUSH_TICK
+        && !strchr(str, '`')
+#endif
+       ) {
+               return NULL;
        }
+
+       /* We need to expand. Example:
+        * echo $(($a + `echo 1`)) $((1 + $((2)) ))
+        */
+       setup_string_in_str(&input, str);
+       parse_stream_dquoted(NULL, &dest, &input, EOF);
+       //bb_error_msg("'%s' -> '%s'", str, dest.data);
+       exp_str = expand_string_to_string(dest.data);
+       //bb_error_msg("'%s' -> '%s'", dest.data, exp_str);
+       o_free_unsafe(&dest);
+       return exp_str;
 }
-#endif /* debug_print_tree */
 
-/* NB: called by pseudo_exec, and therefore must not modify any
- * global data until exec/_exit (we can be a child after vfork!) */
-static int run_list(struct pipe *pi)
+#if ENABLE_SH_MATH_SUPPORT
+static arith_t expand_and_evaluate_arith(const char *arg, int *errcode_p)
 {
-#if ENABLE_HUSH_CASE
-       char *case_word = NULL;
-#endif
-#if ENABLE_HUSH_LOOPS
-       struct pipe *loop_top = NULL;
-       char **for_lcur = NULL;
-       char **for_list = NULL;
-#endif
-       smallint last_followup;
-       smalluint rcode;
-#if ENABLE_HUSH_IF || ENABLE_HUSH_CASE
-       smalluint cond_code = 0;
-#else
-       enum { cond_code = 0 };
-#endif
-#if HAS_KEYWORDS
-       smallint rword; /* enum reserved_style */
-       smallint last_rword; /* ditto */
-#endif
+       arith_eval_hooks_t hooks;
+       arith_t res;
+       char *exp_str;
 
-       debug_printf_exec("run_list start lvl %d\n", G.run_list_level);
-       debug_enter();
+       hooks.lookupvar = get_local_var_value;
+       hooks.setvar = set_local_var_from_halves;
+       //hooks.endofname = endofname;
+       exp_str = expand_pseudo_dquoted(arg);
+       res = arith(exp_str ? exp_str : arg, errcode_p, &hooks);
+       free(exp_str);
+       return res;
+}
+#endif
 
-#if ENABLE_HUSH_LOOPS
-       /* Check syntax for "for" */
-       for (struct pipe *cpipe = pi; cpipe; cpipe = cpipe->next) {
-               if (cpipe->res_word != RES_FOR && cpipe->res_word != RES_IN)
-                       continue;
-               /* current word is FOR or IN (BOLD in comments below) */
-               if (cpipe->next == NULL) {
-                       syntax_error("malformed for");
-                       debug_leave();
-                       debug_printf_exec("run_list lvl %d return 1\n", G.run_list_level);
-                       return 1;
-               }
-               /* "FOR v; do ..." and "for v IN a b; do..." are ok */
-               if (cpipe->next->res_word == RES_DO)
-                       continue;
-               /* next word is not "do". It must be "in" then ("FOR v in ...") */
-               if (cpipe->res_word == RES_IN /* "for v IN a b; not_do..."? */
-                || cpipe->next->res_word != RES_IN /* FOR v not_do_and_not_in..."? */
-               ) {
-                       syntax_error("malformed for");
-                       debug_leave();
-                       debug_printf_exec("run_list lvl %d return 1\n", G.run_list_level);
-                       return 1;
+#if ENABLE_HUSH_BASH_COMPAT
+/* ${var/[/]pattern[/repl]} helpers */
+static char *strstr_pattern(char *val, const char *pattern, int *size)
+{
+       while (1) {
+               char *end = scan_and_match(val, pattern, SCAN_MOVE_FROM_RIGHT + SCAN_MATCH_LEFT_HALF);
+               debug_printf_varexp("val:'%s' pattern:'%s' end:'%s'\n", val, pattern, end);
+               if (end) {
+                       *size = end - val;
+                       return val;
                }
+               if (*val == '\0')
+                       return NULL;
+               /* Optimization: if "*pat" did not match the start of "string",
+                * we know that "tring", "ring" etc will not match too:
+                */
+               if (pattern[0] == '*')
+                       return NULL;
+               val++;
        }
-#endif
-
-       /* Past this point, all code paths should jump to ret: label
-        * in order to return, no direct "return" statements please.
-        * This helps to ensure that no memory is leaked. */
+}
+static char *replace_pattern(char *val, const char *pattern, const char *repl, char exp_op)
+{
+       char *result = NULL;
+       unsigned res_len = 0;
+       unsigned repl_len = strlen(repl);
 
-#if ENABLE_HUSH_JOB
-       G.run_list_level++;
-#endif
+       while (1) {
+               int size;
+               char *s = strstr_pattern(val, pattern, &size);
+               if (!s)
+                       break;
 
-#if HAS_KEYWORDS
-       rword = RES_NONE;
-       last_rword = RES_XXXX;
-#endif
-       last_followup = PIPE_SEQ;
-       rcode = G.last_exitcode;
+               result = xrealloc(result, res_len + (s - val) + repl_len + 1);
+               memcpy(result + res_len, val, s - val);
+               res_len += s - val;
+               strcpy(result + res_len, repl);
+               res_len += repl_len;
+               debug_printf_varexp("val:'%s' s:'%s' result:'%s'\n", val, s, result);
 
-       /* Go through list of pipes, (maybe) executing them. */
-       for (; pi; pi = IF_HUSH_LOOPS(rword == RES_DONE ? loop_top : ) pi->next) {
-               if (G.flag_SIGINT)
+               val = s + size;
+               if (exp_op == '/')
                        break;
+       }
+       if (val[0] && result) {
+               result = xrealloc(result, res_len + strlen(val) + 1);
+               strcpy(result + res_len, val);
+               debug_printf_varexp("val:'%s' result:'%s'\n", val, result);
+       }
+       debug_printf_varexp("result:'%s'\n", result);
+       return result;
+}
+#endif
 
-               IF_HAS_KEYWORDS(rword = pi->res_word;)
-               debug_printf_exec(": rword=%d cond_code=%d last_rword=%d\n",
-                               rword, cond_code, last_rword);
-#if ENABLE_HUSH_LOOPS
-               if ((rword == RES_WHILE || rword == RES_UNTIL || rword == RES_FOR)
-                && loop_top == NULL /* avoid bumping G.depth_of_loop twice */
+/* Helper:
+ * Handles <SPECIAL_VAR_SYMBOL>varname...<SPECIAL_VAR_SYMBOL> construct.
+ */
+static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, char **pp, char first_ch)
+{
+       const char *val = NULL;
+       char *to_be_freed = NULL;
+       char *p = *pp;
+       char *var;
+       char first_char;
+       char exp_op;
+       char exp_save = exp_save; /* for compiler */
+       char *exp_saveptr; /* points to expansion operator */
+       char *exp_word = exp_word; /* for compiler */
+
+       *p = '\0'; /* replace trailing SPECIAL_VAR_SYMBOL */
+       var = arg;
+       exp_saveptr = arg[1] ? strchr(VAR_ENCODED_SUBST_OPS, arg[1]) : NULL;
+       first_char = arg[0] = first_ch & 0x7f;
+       exp_op = 0;
+
+       if (first_char == '#'      /* ${#... */
+        && arg[1] && !exp_saveptr /* not ${#} and not ${#<op_char>...} */
+       ) {
+               /* It must be length operator: ${#var} */
+               var++;
+               exp_op = 'L';
+       } else {
+               /* Maybe handle parameter expansion */
+               if (exp_saveptr /* if 2nd char is one of expansion operators */
+                && strchr(NUMERIC_SPECVARS_STR, first_char) /* 1st char is special variable */
                ) {
-                       /* start of a loop: remember where loop starts */
-                       loop_top = pi;
-                       G.depth_of_loop++;
-               }
-#endif
-               /* Still in the same "if...", "then..." or "do..." branch? */
-               if (IF_HAS_KEYWORDS(rword == last_rword &&) 1) {
-                       if ((rcode == 0 && last_followup == PIPE_OR)
-                        || (rcode != 0 && last_followup == PIPE_AND)
-                       ) {
-                               /* It is "<true> || CMD" or "<false> && CMD"
-                                * and we should not execute CMD */
-                               debug_printf_exec("skipped cmd because of || or &&\n");
-                               last_followup = pi->followup;
-                               continue;
+                       /* ${?:0}, ${#[:]%0} etc */
+                       exp_saveptr = var + 1;
+               } else {
+                       /* ${?}, ${var}, ${var:0}, ${var[:]%0} etc */
+                       exp_saveptr = var+1 + strcspn(var+1, VAR_ENCODED_SUBST_OPS);
+               }
+               exp_op = exp_save = *exp_saveptr;
+               if (exp_op) {
+                       exp_word = exp_saveptr + 1;
+                       if (exp_op == ':') {
+                               exp_op = *exp_word++;
+//TODO: try ${var:} and ${var:bogus} in non-bash config
+                               if (ENABLE_HUSH_BASH_COMPAT
+                                && (!exp_op || !strchr(MINUS_PLUS_EQUAL_QUESTION, exp_op))
+                               ) {
+                                       /* oops... it's ${var:N[:M]}, not ${var:?xxx} or some such */
+                                       exp_op = ':';
+                                       exp_word--;
+                               }
                        }
-               }
-               last_followup = pi->followup;
-               IF_HAS_KEYWORDS(last_rword = rword;)
-#if ENABLE_HUSH_IF
-               if (cond_code) {
-                       if (rword == RES_THEN) {
-                               /* if false; then ... fi has exitcode 0! */
-                               G.last_exitcode = rcode = EXIT_SUCCESS;
-                               /* "if <false> THEN cmd": skip cmd */
-                               continue;
+                       *exp_saveptr = '\0';
+               } /* else: it's not an expansion op, but bare ${var} */
+       }
+
+       /* Look up the variable in question */
+       if (isdigit(var[0])) {
+               /* parse_dollar() should have vetted var for us */
+               int n = xatoi_positive(var);
+               if (n < G.global_argc)
+                       val = G.global_argv[n];
+               /* else val remains NULL: $N with too big N */
+       } else {
+               switch (var[0]) {
+               case '$': /* pid */
+                       val = utoa(G.root_pid);
+                       break;
+               case '!': /* bg pid */
+                       val = G.last_bg_pid ? utoa(G.last_bg_pid) : "";
+                       break;
+               case '?': /* exitcode */
+                       val = utoa(G.last_exitcode);
+                       break;
+               case '#': /* argc */
+                       val = utoa(G.global_argc ? G.global_argc-1 : 0);
+                       break;
+               default:
+                       val = get_local_var_value(var);
+               }
+       }
+
+       /* Handle any expansions */
+       if (exp_op == 'L') {
+               debug_printf_expand("expand: length(%s)=", val);
+               val = utoa(val ? strlen(val) : 0);
+               debug_printf_expand("%s\n", val);
+       } else if (exp_op) {
+               if (exp_op == '%' || exp_op == '#') {
+                       /* Standard-mandated substring removal ops:
+                        * ${parameter%word} - remove smallest suffix pattern
+                        * ${parameter%%word} - remove largest suffix pattern
+                        * ${parameter#word} - remove smallest prefix pattern
+                        * ${parameter##word} - remove largest prefix pattern
+                        *
+                        * Word is expanded to produce a glob pattern.
+                        * Then var's value is matched to it and matching part removed.
+                        */
+                       if (val && val[0]) {
+                               char *exp_exp_word;
+                               char *loc;
+                               unsigned scan_flags = pick_scan(exp_op, *exp_word);
+                               if (exp_op == *exp_word)        /* ## or %% */
+                                       exp_word++;
+//TODO: avoid xstrdup unless needed
+// (see HACK ALERT below for an example)
+                               val = to_be_freed = xstrdup(val);
+//TODO: fix expansion rules:
+                               exp_exp_word = expand_pseudo_dquoted(exp_word);
+                               if (exp_exp_word)
+                                       exp_word = exp_exp_word;
+                               loc = scan_and_match(to_be_freed, exp_word, scan_flags);
+                               //bb_error_msg("op:%c str:'%s' pat:'%s' res:'%s'",
+                               //              exp_op, to_be_freed, exp_word, loc);
+                               free(exp_exp_word);
+                               if (loc) { /* match was found */
+                                       if (scan_flags & SCAN_MATCH_LEFT_HALF) /* #[#] */
+                                               val = loc;
+                                       else /* %[%] */
+                                               *loc = '\0';
+                               }
                        }
-               } else {
-                       if (rword == RES_ELSE || rword == RES_ELIF) {
-                               /* "if <true> then ... ELSE/ELIF cmd":
-                                * skip cmd and all following ones */
-                               break;
+               }
+#if ENABLE_HUSH_BASH_COMPAT
+               else if (exp_op == '/' || exp_op == '\\') {
+                       /* It's ${var/[/]pattern[/repl]} thing.
+                        * Note that in encoded form it has TWO parts:
+                        * var/pattern<SPECIAL_VAR_SYMBOL>repl<SPECIAL_VAR_SYMBOL>
+                        */
+                       /* Empty variable always gives nothing: */
+                       // "v=''; echo ${v/*/w}" prints "", not "w"
+                       if (val && val[0]) {
+                               /* It's ${var/[/]pattern[/repl]} thing */
+                               /*
+                                * Pattern is taken literally, while
+                                * repl should be unbackslashed and globbed
+                                * by the usual expansion rules:
+                                * >az; >bz;
+                                * v='a bz'; echo "${v/a*z/a*z}" prints "a*z"
+                                * v='a bz'; echo "${v/a*z/\z}"  prints "\z"
+                                * v='a bz'; echo ${v/a*z/a*z}   prints "az"
+                                * v='a bz'; echo ${v/a*z/\z}    prints "z"
+                                * (note that a*z _pattern_ is never globbed!)
+                                */
+//TODO: fix expansion rules:
+                               char *pattern, *repl, *t;
+                               pattern = expand_pseudo_dquoted(exp_word);
+                               if (!pattern)
+                                       pattern = xstrdup(exp_word);
+                               debug_printf_varexp("pattern:'%s'->'%s'\n", exp_word, pattern);
+                               *p++ = SPECIAL_VAR_SYMBOL;
+                               exp_word = p;
+                               p = strchr(p, SPECIAL_VAR_SYMBOL);
+                               *p = '\0';
+                               repl = expand_pseudo_dquoted(exp_word);
+                               debug_printf_varexp("repl:'%s'->'%s'\n", exp_word, repl);
+                               /* HACK ALERT. We depend here on the fact that
+                                * G.global_argv and results of utoa and get_local_var_value
+                                * are actually in writable memory:
+                                * replace_pattern momentarily stores NULs there. */
+                               t = (char*)val;
+                               to_be_freed = replace_pattern(t,
+                                               pattern,
+                                               (repl ? repl : exp_word),
+                                               exp_op);
+                               if (to_be_freed) /* at least one replace happened */
+                                       val = to_be_freed;
+                               free(pattern);
+                               free(repl);
                        }
                }
 #endif
-#if ENABLE_HUSH_LOOPS
-               if (rword == RES_FOR) { /* && pi->num_cmds - always == 1 */
-                       if (!for_lcur) {
-                               /* first loop through for */
-
-                               static const char encoded_dollar_at[] ALIGN1 = {
-                                       SPECIAL_VAR_SYMBOL, '@' | 0x80, SPECIAL_VAR_SYMBOL, '\0'
-                               }; /* encoded representation of "$@" */
-                               static const char *const encoded_dollar_at_argv[] = {
-                                       encoded_dollar_at, NULL
-                               }; /* argv list with one element: "$@" */
-                               char **vals;
-
-                               vals = (char**)encoded_dollar_at_argv;
-                               if (pi->next->res_word == RES_IN) {
-                                       /* if no variable values after "in" we skip "for" */
-                                       if (!pi->next->cmds[0].argv) {
-                                               G.last_exitcode = rcode = EXIT_SUCCESS;
-                                               debug_printf_exec(": null FOR: exitcode EXIT_SUCCESS\n");
-                                               break;
+               else if (exp_op == ':') {
+#if ENABLE_HUSH_BASH_COMPAT && ENABLE_SH_MATH_SUPPORT
+                       /* It's ${var:N[:M]} bashism.
+                        * Note that in encoded form it has TWO parts:
+                        * var:N<SPECIAL_VAR_SYMBOL>M<SPECIAL_VAR_SYMBOL>
+                        */
+                       arith_t beg, len;
+                       int errcode = 0;
+
+                       beg = expand_and_evaluate_arith(exp_word, &errcode);
+                       debug_printf_varexp("beg:'%s'=%lld\n", exp_word, (long long)beg);
+                       *p++ = SPECIAL_VAR_SYMBOL;
+                       exp_word = p;
+                       p = strchr(p, SPECIAL_VAR_SYMBOL);
+                       *p = '\0';
+                       len = expand_and_evaluate_arith(exp_word, &errcode);
+                       debug_printf_varexp("len:'%s'=%lld\n", exp_word, (long long)len);
+
+                       if (errcode >= 0 && len >= 0) { /* bash compat: len < 0 is illegal */
+                               if (beg < 0) /* bash compat */
+                                       beg = 0;
+                               debug_printf_varexp("from val:'%s'\n", val);
+                               if (len == 0 || !val || beg >= strlen(val))
+                                       val = "";
+                               else {
+                                       /* Paranoia. What if user entered 9999999999999
+                                        * which fits in arith_t but not int? */
+                                       if (len >= INT_MAX)
+                                               len = INT_MAX;
+                                       val = to_be_freed = xstrndup(val + beg, len);
+                               }
+                               debug_printf_varexp("val:'%s'\n", val);
+                       } else
+#endif
+                       {
+                               die_if_script("malformed ${%s:...}", var);
+                               val = "";
+                       }
+               } else { /* one of "-=+?" */
+                       /* Standard-mandated substitution ops:
+                        * ${var?word} - indicate error if unset
+                        *      If var is unset, word (or a message indicating it is unset
+                        *      if word is null) is written to standard error
+                        *      and the shell exits with a non-zero exit status.
+                        *      Otherwise, the value of var is substituted.
+                        * ${var-word} - use default value
+                        *      If var is unset, word is substituted.
+                        * ${var=word} - assign and use default value
+                        *      If var is unset, word is assigned to var.
+                        *      In all cases, final value of var is substituted.
+                        * ${var+word} - use alternative value
+                        *      If var is unset, null is substituted.
+                        *      Otherwise, word is substituted.
+                        *
+                        * Word is subjected to tilde expansion, parameter expansion,
+                        * command substitution, and arithmetic expansion.
+                        * If word is not needed, it is not expanded.
+                        *
+                        * Colon forms (${var:-word}, ${var:=word} etc) do the same,
+                        * but also treat null var as if it is unset.
+                        */
+                       int use_word = (!val || ((exp_save == ':') && !val[0]));
+                       if (exp_op == '+')
+                               use_word = !use_word;
+                       debug_printf_expand("expand: op:%c (null:%s) test:%i\n", exp_op,
+                                       (exp_save == ':') ? "true" : "false", use_word);
+                       if (use_word) {
+                               to_be_freed = expand_pseudo_dquoted(exp_word);
+                               if (to_be_freed)
+                                       exp_word = to_be_freed;
+                               if (exp_op == '?') {
+                                       /* mimic bash message */
+                                       die_if_script("%s: %s",
+                                               var,
+                                               exp_word[0] ? exp_word : "parameter null or not set"
+                                       );
+//TODO: how interactive bash aborts expansion mid-command?
+                               } else {
+                                       val = exp_word;
+                               }
+
+                               if (exp_op == '=') {
+                                       /* ${var=[word]} or ${var:=[word]} */
+                                       if (isdigit(var[0]) || var[0] == '#') {
+                                               /* mimic bash message */
+                                               die_if_script("$%s: cannot assign in this way", var);
+                                               val = NULL;
+                                       } else {
+                                               char *new_var = xasprintf("%s=%s", var, val);
+                                               set_local_var(new_var, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
                                        }
-                                       vals = pi->next->cmds[0].argv;
-                               } /* else: "for var; do..." -> assume "$@" list */
-                               /* create list of variable values */
-                               debug_print_strings("for_list made from", vals);
-                               for_list = expand_strvec_to_strvec(vals);
-                               for_lcur = for_list;
-                               debug_print_strings("for_list", for_list);
+                               }
                        }
-                       if (!*for_lcur) {
-                               /* "for" loop is over, clean up */
-                               free(for_list);
-                               for_list = NULL;
-                               for_lcur = NULL;
+               } /* one of "-=+?" */
+
+               *exp_saveptr = exp_save;
+       } /* if (exp_op) */
+
+       arg[0] = first_ch;
+
+       *pp = p;
+       *to_be_freed_pp = to_be_freed;
+       return val;
+}
+
+/* Expand all variable references in given string, adding words to list[]
+ * at n, n+1,... positions. Return updated n (so that list[n] is next one
+ * to be filled). This routine is extremely tricky: has to deal with
+ * variables/parameters with whitespace, $* and $@, and constructs like
+ * 'echo -$*-'. If you play here, you must run testsuite afterwards! */
+static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char or_mask)
+{
+       /* or_mask is either 0 (normal case) or 0x80 -
+        * expansion of right-hand side of assignment == 1-element expand.
+        * It will also do no globbing, and thus we must not backslash-quote!
+        */
+       char ored_ch;
+       char *p;
+
+       ored_ch = 0;
+
+       debug_printf_expand("expand_vars_to_list: arg:'%s' or_mask:%x\n", arg, or_mask);
+       debug_print_list("expand_vars_to_list", output, n);
+       n = o_save_ptr(output, n);
+       debug_print_list("expand_vars_to_list[0]", output, n);
+
+       while ((p = strchr(arg, SPECIAL_VAR_SYMBOL)) != NULL) {
+               char first_ch;
+               char *to_be_freed = NULL;
+               const char *val = NULL;
+#if ENABLE_HUSH_TICK
+               o_string subst_result = NULL_O_STRING;
+#endif
+#if ENABLE_SH_MATH_SUPPORT
+               char arith_buf[sizeof(arith_t)*3 + 2];
+#endif
+               o_addblock(output, arg, p - arg);
+               debug_print_list("expand_vars_to_list[1]", output, n);
+               arg = ++p;
+               p = strchr(p, SPECIAL_VAR_SYMBOL);
+
+               first_ch = arg[0] | or_mask; /* forced to "quoted" if or_mask = 0x80 */
+               /* "$@" is special. Even if quoted, it can still
+                * expand to nothing (not even an empty string) */
+               if ((first_ch & 0x7f) != '@')
+                       ored_ch |= first_ch;
+
+               switch (first_ch & 0x7f) {
+               /* Highest bit in first_ch indicates that var is double-quoted */
+               case '*':
+               case '@': {
+                       int i;
+                       if (!G.global_argv[1])
                                break;
+                       i = 1;
+                       ored_ch |= first_ch; /* do it for "$@" _now_, when we know it's not empty */
+                       if (!(first_ch & 0x80)) { /* unquoted $* or $@ */
+                               int sv = output->o_expflags;
+                               /* unquoted var's contents should be globbed, so don't escape */
+                               output->o_expflags &= ~EXP_FLAG_ESC_GLOB_CHARS;
+                               while (G.global_argv[i]) {
+                                       n = expand_on_ifs(output, n, G.global_argv[i]);
+                                       debug_printf_expand("expand_vars_to_list: argv %d (last %d)\n", i, G.global_argc - 1);
+                                       if (G.global_argv[i++][0] && G.global_argv[i]) {
+                                               /* this argv[] is not empty and not last:
+                                                * put terminating NUL, start new word */
+                                               o_addchr(output, '\0');
+                                               debug_print_list("expand_vars_to_list[2]", output, n);
+                                               n = o_save_ptr(output, n);
+                                               debug_print_list("expand_vars_to_list[3]", output, n);
+                                       }
+                               }
+                               output->o_expflags = sv;
+                       } else
+                       /* If or_mask is nonzero, we handle assignment 'a=....$@.....'
+                        * and in this case should treat it like '$*' - see 'else...' below */
+                       if (first_ch == ('@'|0x80) && !or_mask) { /* quoted $@ */
+                               while (1) {
+                                       o_addQstr(output, G.global_argv[i]);
+                                       if (++i >= G.global_argc)
+                                               break;
+                                       o_addchr(output, '\0');
+                                       debug_print_list("expand_vars_to_list[4]", output, n);
+                                       n = o_save_ptr(output, n);
+                               }
+                       } else { /* quoted $*: add as one word */
+                               while (1) {
+                                       o_addQstr(output, G.global_argv[i]);
+                                       if (!G.global_argv[++i])
+                                               break;
+                                       if (G.ifs[0])
+                                               o_addchr(output, G.ifs[0]);
+                               }
                        }
-                       /* Insert next value from for_lcur */
-                       /* note: *for_lcur already has quotes removed, $var expanded, etc */
-                       set_local_var(xasprintf("%s=%s", pi->cmds[0].argv[0], *for_lcur++), /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
-                       continue;
-               }
-               if (rword == RES_IN) {
-                       continue; /* "for v IN list;..." - "in" has no cmds anyway */
-               }
-               if (rword == RES_DONE) {
-                       continue; /* "done" has no cmds too */
+                       break;
                }
+               case SPECIAL_VAR_SYMBOL: /* <SPECIAL_VAR_SYMBOL><SPECIAL_VAR_SYMBOL> */
+                       /* "Empty variable", used to make "" etc to not disappear */
+                       arg++;
+                       ored_ch = 0x80;
+                       break;
+#if ENABLE_HUSH_TICK
+               case '`': /* <SPECIAL_VAR_SYMBOL>`cmd<SPECIAL_VAR_SYMBOL> */
+                       *p = '\0';
+                       arg++;
+                       /* Can't just stuff it into output o_string,
+                        * expanded result may need to be globbed
+                        * and $IFS-splitted */
+                       debug_printf_subst("SUBST '%s' first_ch %x\n", arg, first_ch);
+                       G.last_exitcode = process_command_subs(&subst_result, arg);
+                       debug_printf_subst("SUBST RES:%d '%s'\n", G.last_exitcode, subst_result.data);
+                       val = subst_result.data;
+                       goto store_val;
 #endif
-#if ENABLE_HUSH_CASE
-               if (rword == RES_CASE) {
-                       case_word = expand_strvec_to_string(pi->cmds->argv);
-                       continue;
-               }
-               if (rword == RES_MATCH) {
-                       char **argv;
+#if ENABLE_SH_MATH_SUPPORT
+               case '+': { /* <SPECIAL_VAR_SYMBOL>+cmd<SPECIAL_VAR_SYMBOL> */
+                       arith_t res;
+                       int errcode;
 
-                       if (!case_word) /* "case ... matched_word) ... WORD)": we executed selected branch, stop */
-                               break;
-                       /* all prev words didn't match, does this one match? */
-                       argv = pi->cmds->argv;
-                       while (*argv) {
-                               char *pattern = expand_string_to_string(*argv);
-                               /* TODO: which FNM_xxx flags to use? */
-                               cond_code = (fnmatch(pattern, case_word, /*flags:*/ 0) != 0);
-                               free(pattern);
-                               if (cond_code == 0) { /* match! we will execute this branch */
-                                       free(case_word); /* make future "word)" stop */
-                                       case_word = NULL;
+                       arg++; /* skip '+' */
+                       *p = '\0'; /* replace trailing <SPECIAL_VAR_SYMBOL> */
+                       debug_printf_subst("ARITH '%s' first_ch %x\n", arg, first_ch);
+                       res = expand_and_evaluate_arith(arg, &errcode);
+
+                       if (errcode < 0) {
+                               const char *msg = "error in arithmetic";
+                               switch (errcode) {
+                               case -3:
+                                       msg = "exponent less than 0";
+                                       break;
+                               case -2:
+                                       msg = "divide by 0";
+                                       break;
+                               case -5:
+                                       msg = "expression recursion loop detected";
                                        break;
                                }
-                               argv++;
+                               die_if_script(msg);
                        }
-                       continue;
-               }
-               if (rword == RES_CASE_BODY) { /* inside of a case branch */
-                       if (cond_code != 0)
-                               continue; /* not matched yet, skip this pipe */
+                       debug_printf_subst("ARITH RES '"arith_t_fmt"'\n", res);
+                       sprintf(arith_buf, arith_t_fmt, res);
+                       val = arith_buf;
+                       break;
                }
 #endif
-               /* Just pressing <enter> in shell should check for jobs.
-                * OTOH, in non-interactive shell this is useless
-                * and only leads to extra job checks */
-               if (pi->num_cmds == 0) {
-                       if (G_interactive_fd)
-                               goto check_jobs_and_continue;
-                       continue;
+               default:
+                       val = expand_one_var(&to_be_freed, arg, &p, first_ch);
+ IF_HUSH_TICK(store_val:)
+                       if (!(first_ch & 0x80)) { /* unquoted $VAR */
+                               debug_printf_expand("unquoted '%s', output->o_escape:%d\n", val,
+                                               !!(output->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+                               if (val && val[0]) {
+                                       /* unquoted var's contents should be globbed, so don't escape */
+                                       int sv = output->o_expflags;
+                                       output->o_expflags &= ~EXP_FLAG_ESC_GLOB_CHARS;
+                                       n = expand_on_ifs(output, n, val);
+                                       val = NULL;
+                                       output->o_expflags = sv;
+                               }
+                       } else { /* quoted $VAR, val will be appended below */
+                               debug_printf_expand("quoted '%s', output->o_escape:%d\n", val,
+                                               !!(output->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
+                       }
+                       break;
+
+               } /* switch (char after <SPECIAL_VAR_SYMBOL>) */
+
+               if (val && val[0]) {
+                       o_addQstr(output, val);
                }
+               free(to_be_freed);
+               /* Do the check to avoid writing to a const string */
+               if (*p != SPECIAL_VAR_SYMBOL)
+                       *p = SPECIAL_VAR_SYMBOL;
 
-               /* After analyzing all keywords and conditions, we decided
-                * to execute this pipe. NB: have to do checkjobs(NULL)
-                * after run_pipe to collect any background children,
-                * even if list execution is to be stopped. */
-               debug_printf_exec(": run_pipe with %d members\n", pi->num_cmds);
-               {
-                       int r;
-#if ENABLE_HUSH_LOOPS
-                       G.flag_break_continue = 0;
-#endif
-                       rcode = r = run_pipe(pi); /* NB: rcode is a smallint */
-                       if (r != -1) {
-                               /* We ran a builtin, function, or group.
-                                * rcode is already known
-                                * and we don't need to wait for anything. */
-                               G.last_exitcode = rcode;
-                               debug_printf_exec(": builtin/func exitcode %d\n", rcode);
-                               check_and_run_traps(0);
-#if ENABLE_HUSH_LOOPS
-                               /* Was it "break" or "continue"? */
-                               if (G.flag_break_continue) {
-                                       smallint fbc = G.flag_break_continue;
-                                       /* We might fall into outer *loop*,
-                                        * don't want to break it too */
-                                       if (loop_top) {
-                                               G.depth_break_continue--;
-                                               if (G.depth_break_continue == 0)
-                                                       G.flag_break_continue = 0;
-                                               /* else: e.g. "continue 2" should *break* once, *then* continue */
-                                       } /* else: "while... do... { we are here (innermost list is not a loop!) };...done" */
-                                       if (G.depth_break_continue != 0 || fbc == BC_BREAK)
-                                               goto check_jobs_and_break;
-                                       /* "continue": simulate end of loop */
-                                       rword = RES_DONE;
-                                       continue;
-                               }
+#if ENABLE_HUSH_TICK
+               o_free(&subst_result);
 #endif
-#if ENABLE_HUSH_FUNCTIONS
-                               if (G.flag_return_in_progress == 1) {
-                                       /* same as "goto check_jobs_and_break" */
-                                       checkjobs(NULL);
-                                       break;
-                               }
-#endif
-                       } else if (pi->followup == PIPE_BG) {
-                               /* What does bash do with attempts to background builtins? */
-                               /* even bash 3.2 doesn't do that well with nested bg:
-                                * try "{ { sleep 10; echo DEEP; } & echo HERE; } &".
-                                * I'm NOT treating inner &'s as jobs */
-                               check_and_run_traps(0);
-#if ENABLE_HUSH_JOB
-                               if (G.run_list_level == 1)
-                                       insert_bg_job(pi);
-#endif
-                               G.last_exitcode = rcode = EXIT_SUCCESS;
-                               debug_printf_exec(": cmd&: exitcode EXIT_SUCCESS\n");
-                       } else {
-#if ENABLE_HUSH_JOB
-                               if (G.run_list_level == 1 && G_interactive_fd) {
-                                       /* Waits for completion, then fg's main shell */
-                                       rcode = checkjobs_and_fg_shell(pi);
-                                       debug_printf_exec(": checkjobs_and_fg_shell exitcode %d\n", rcode);
-                                       check_and_run_traps(0);
-                               } else
-#endif
-                               { /* This one just waits for completion */
-                                       rcode = checkjobs(pi);
-                                       debug_printf_exec(": checkjobs exitcode %d\n", rcode);
-                                       check_and_run_traps(0);
-                               }
-                               G.last_exitcode = rcode;
-                       }
-               }
+               arg = ++p;
+       } /* end of "while (SPECIAL_VAR_SYMBOL is found) ..." */
 
-               /* Analyze how result affects subsequent commands */
-#if ENABLE_HUSH_IF
-               if (rword == RES_IF || rword == RES_ELIF)
-                       cond_code = rcode;
+       if (arg[0]) {
+               debug_print_list("expand_vars_to_list[a]", output, n);
+               /* this part is literal, and it was already pre-quoted
+                * if needed (much earlier), do not use o_addQstr here! */
+               o_addstr_with_NUL(output, arg);
+               debug_print_list("expand_vars_to_list[b]", output, n);
+       } else if (output->length == o_get_last_ptr(output, n) /* expansion is empty */
+        && !(ored_ch & 0x80) /* and all vars were not quoted. */
+       ) {
+               n--;
+               /* allow to reuse list[n] later without re-growth */
+               output->has_empty_slot = 1;
+       } else {
+               o_addchr(output, '\0');
+       }
+
+       return n;
+}
+
+static char **expand_variables(char **argv, unsigned or_mask)
+{
+       int n;
+       char **list;
+       o_string output = NULL_O_STRING;
+
+       output.o_expflags = or_mask;
+
+       n = 0;
+       while (*argv) {
+               n = expand_vars_to_list(&output, n, *argv, (unsigned char)or_mask);
+               argv++;
+       }
+       debug_print_list("expand_variables", &output, n);
+
+       /* output.data (malloced in one block) gets returned in "list" */
+       list = o_finalize_list(&output, n);
+       debug_print_strings("expand_variables[1]", list);
+       return list;
+}
+
+static char **expand_strvec_to_strvec(char **argv)
+{
+       return expand_variables(argv, EXP_FLAG_GLOB | EXP_FLAG_ESC_GLOB_CHARS);
+}
+
+#if ENABLE_HUSH_BASH_COMPAT
+static char **expand_strvec_to_strvec_singleword_noglob(char **argv)
+{
+       return expand_variables(argv, EXP_FLAG_SINGLEWORD);
+}
 #endif
-#if ENABLE_HUSH_LOOPS
-               /* Beware of "while false; true; do ..."! */
-               if (pi->next && pi->next->res_word == RES_DO) {
-                       if (rword == RES_WHILE) {
-                               if (rcode) {
-                                       /* "while false; do...done" - exitcode 0 */
-                                       G.last_exitcode = rcode = EXIT_SUCCESS;
-                                       debug_printf_exec(": while expr is false: breaking (exitcode:EXIT_SUCCESS)\n");
-                                       goto check_jobs_and_break;
-                               }
-                       }
-                       if (rword == RES_UNTIL) {
-                               if (!rcode) {
-                                       debug_printf_exec(": until expr is true: breaking\n");
- check_jobs_and_break:
-                                       checkjobs(NULL);
-                                       break;
-                               }
-                       }
+
+/* Used for expansion of right hand of assignments,
+ * $((...)), heredocs, variable espansion parts.
+ *
+ * NB: should NOT do globbing!
+ * "export v=/bin/c*; env | grep ^v=" outputs "v=/bin/c*"
+ */
+static char *expand_string_to_string(const char *str)
+{
+       char *argv[2], **list;
+
+       /* This is generally an optimization, but it also
+        * handles "", which otherwise trips over !list[0] check below.
+        * (is this ever happens that we actually get str="" here?)
+        */
+       if (!strchr(str, SPECIAL_VAR_SYMBOL) && !strchr(str, '\\')) {
+               //TODO: Can use on strings with \ too, just unbackslash() them?
+               debug_printf_expand("string_to_string(fast)='%s'\n", str);
+               return xstrdup(str);
+       }
+
+       argv[0] = (char*)str;
+       argv[1] = NULL;
+       list = expand_variables(argv, EXP_FLAG_ESC_GLOB_CHARS | EXP_FLAG_SINGLEWORD);
+       if (HUSH_DEBUG)
+               if (!list[0] || list[1])
+                       bb_error_msg_and_die("BUG in varexp2");
+       /* actually, just move string 2*sizeof(char*) bytes back */
+       overlapping_strcpy((char*)list, list[0]);
+       unbackslash((char*)list);
+       debug_printf_expand("string_to_string='%s'\n", (char*)list);
+       return (char*)list;
+}
+
+/* Used for "eval" builtin */
+static char* expand_strvec_to_string(char **argv)
+{
+       char **list;
+
+       list = expand_variables(argv, EXP_FLAG_SINGLEWORD);
+       /* Convert all NULs to spaces */
+       if (list[0]) {
+               int n = 1;
+               while (list[n]) {
+                       if (HUSH_DEBUG)
+                               if (list[n-1] + strlen(list[n-1]) + 1 != list[n])
+                                       bb_error_msg_and_die("BUG in varexp3");
+                       /* bash uses ' ' regardless of $IFS contents */
+                       list[n][-1] = ' ';
+                       n++;
                }
-#endif
+       }
+       overlapping_strcpy((char*)list, list[0]);
+       debug_printf_expand("strvec_to_string='%s'\n", (char*)list);
+       return (char*)list;
+}
 
- check_jobs_and_continue:
-               checkjobs(NULL);
-       } /* for (pi) */
+static char **expand_assignments(char **argv, int count)
+{
+       int i;
+       char **p;
 
-#if ENABLE_HUSH_JOB
-       G.run_list_level--;
-#endif
-#if ENABLE_HUSH_LOOPS
-       if (loop_top)
-               G.depth_of_loop--;
-       free(for_list);
-#endif
-#if ENABLE_HUSH_CASE
-       free(case_word);
-#endif
-       debug_leave();
-       debug_printf_exec("run_list lvl %d return %d\n", G.run_list_level + 1, rcode);
-       return rcode;
+       G.expanded_assignments = p = NULL;
+       /* Expand assignments into one string each */
+       for (i = 0; i < count; i++) {
+               G.expanded_assignments = p = add_string_to_strings(p, expand_string_to_string(argv[i]));
+       }
+       G.expanded_assignments = NULL;
+       return p;
 }
 
-/* Select which version we will use */
-static int run_and_free_list(struct pipe *pi)
+
+#if BB_MMU
+/* never called */
+void re_execute_shell(char ***to_free, const char *s,
+               char *g_argv0, char **g_argv,
+               char **builtin_argv) NORETURN;
+
+static void reset_traps_to_defaults(void)
 {
-       int rcode = 0;
-       debug_printf_exec("run_and_free_list entered\n");
-       if (!G.fake_mode) {
-               debug_printf_exec(": run_list: 1st pipe with %d cmds\n", pi->num_cmds);
-               rcode = run_list(pi);
+       /* This function is always called in a child shell
+        * after fork (not vfork, NOMMU doesn't use this function).
+        */
+       unsigned sig;
+       unsigned mask;
+
+       /* Child shells are not interactive.
+        * SIGTTIN/SIGTTOU/SIGTSTP should not have special handling.
+        * Testcase: (while :; do :; done) + ^Z should background.
+        * Same goes for SIGTERM, SIGHUP, SIGINT.
+        */
+       if (!G.traps && !(G.non_DFL_mask & SPECIAL_INTERACTIVE_SIGS))
+               return; /* already no traps and no SPECIAL_INTERACTIVE_SIGS */
+
+       /* Switching off SPECIAL_INTERACTIVE_SIGS.
+        * Stupid. It can be done with *single* &= op, but we can't use
+        * the fact that G.blocked_set is implemented as a bitmask
+        * in libc... */
+       mask = (SPECIAL_INTERACTIVE_SIGS >> 1);
+       sig = 1;
+       while (1) {
+               if (mask & 1) {
+                       /* Careful. Only if no trap or trap is not "" */
+                       if (!G.traps || !G.traps[sig] || G.traps[sig][0])
+                               sigdelset(&G.blocked_set, sig);
+               }
+               mask >>= 1;
+               if (!mask)
+                       break;
+               sig++;
+       }
+       /* Our homegrown sig mask is saner to work with :) */
+       G.non_DFL_mask &= ~SPECIAL_INTERACTIVE_SIGS;
+
+       /* Resetting all traps to default except empty ones */
+       mask = G.non_DFL_mask;
+       if (G.traps) for (sig = 0; sig < NSIG; sig++, mask >>= 1) {
+               if (!G.traps[sig] || !G.traps[sig][0])
+                       continue;
+               free(G.traps[sig]);
+               G.traps[sig] = NULL;
+               /* There is no signal for 0 (EXIT) */
+               if (sig == 0)
+                       continue;
+               /* There was a trap handler, we just removed it.
+                * But if sig still has non-DFL handling,
+                * we should not unblock the sig. */
+               if (mask & 1)
+                       continue;
+               sigdelset(&G.blocked_set, sig);
+       }
+       sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
+}
+
+#else /* !BB_MMU */
+
+static void re_execute_shell(char ***to_free, const char *s,
+               char *g_argv0, char **g_argv,
+               char **builtin_argv) NORETURN;
+static void re_execute_shell(char ***to_free, const char *s,
+               char *g_argv0, char **g_argv,
+               char **builtin_argv)
+{
+# define NOMMU_HACK_FMT ("-$%x:%x:%x:%x:%x:%llx" IF_HUSH_LOOPS(":%x"))
+       /* delims + 2 * (number of bytes in printed hex numbers) */
+       char param_buf[sizeof(NOMMU_HACK_FMT) + 2 * (sizeof(int)*6 + sizeof(long long)*1)];
+       char *heredoc_argv[4];
+       struct variable *cur;
+# if ENABLE_HUSH_FUNCTIONS
+       struct function *funcp;
+# endif
+       char **argv, **pp;
+       unsigned cnt;
+       unsigned long long empty_trap_mask;
+
+       if (!g_argv0) { /* heredoc */
+               argv = heredoc_argv;
+               argv[0] = (char *) G.argv0_for_re_execing;
+               argv[1] = (char *) "-<";
+               argv[2] = (char *) s;
+               argv[3] = NULL;
+               pp = &argv[3]; /* used as pointer to empty environment */
+               goto do_exec;
+       }
+
+       cnt = 0;
+       pp = builtin_argv;
+       if (pp) while (*pp++)
+               cnt++;
+
+       empty_trap_mask = 0;
+       if (G.traps) {
+               int sig;
+               for (sig = 1; sig < NSIG; sig++) {
+                       if (G.traps[sig] && !G.traps[sig][0])
+                               empty_trap_mask |= 1LL << sig;
+               }
+       }
+
+       sprintf(param_buf, NOMMU_HACK_FMT
+                       , (unsigned) G.root_pid
+                       , (unsigned) G.root_ppid
+                       , (unsigned) G.last_bg_pid
+                       , (unsigned) G.last_exitcode
+                       , cnt
+                       , empty_trap_mask
+                       IF_HUSH_LOOPS(, G.depth_of_loop)
+                       );
+# undef NOMMU_HACK_FMT
+       /* 1:hush 2:-$<pid>:<pid>:<exitcode>:<etc...> <vars...> <funcs...>
+        * 3:-c 4:<cmd> 5:<arg0> <argN...> 6:NULL
+        */
+       cnt += 6;
+       for (cur = G.top_var; cur; cur = cur->next) {
+               if (!cur->flg_export || cur->flg_read_only)
+                       cnt += 2;
+       }
+# if ENABLE_HUSH_FUNCTIONS
+       for (funcp = G.top_func; funcp; funcp = funcp->next)
+               cnt += 3;
+# endif
+       pp = g_argv;
+       while (*pp++)
+               cnt++;
+       *to_free = argv = pp = xzalloc(sizeof(argv[0]) * cnt);
+       *pp++ = (char *) G.argv0_for_re_execing;
+       *pp++ = param_buf;
+       for (cur = G.top_var; cur; cur = cur->next) {
+               if (strcmp(cur->varstr, hush_version_str) == 0)
+                       continue;
+               if (cur->flg_read_only) {
+                       *pp++ = (char *) "-R";
+                       *pp++ = cur->varstr;
+               } else if (!cur->flg_export) {
+                       *pp++ = (char *) "-V";
+                       *pp++ = cur->varstr;
+               }
+       }
+# if ENABLE_HUSH_FUNCTIONS
+       for (funcp = G.top_func; funcp; funcp = funcp->next) {
+               *pp++ = (char *) "-F";
+               *pp++ = funcp->name;
+               *pp++ = funcp->body_as_string;
+       }
+# endif
+       /* We can pass activated traps here. Say, -Tnn:trap_string
+        *
+        * However, POSIX says that subshells reset signals with traps
+        * to SIG_DFL.
+        * I tested bash-3.2 and it not only does that with true subshells
+        * of the form ( list ), but with any forked children shells.
+        * I set trap "echo W" WINCH; and then tried:
+        *
+        * { echo 1; sleep 20; echo 2; } &
+        * while true; do echo 1; sleep 20; echo 2; break; done &
+        * true | { echo 1; sleep 20; echo 2; } | cat
+        *
+        * In all these cases sending SIGWINCH to the child shell
+        * did not run the trap. If I add trap "echo V" WINCH;
+        * _inside_ group (just before echo 1), it works.
+        *
+        * I conclude it means we don't need to pass active traps here.
+        * Even if we would use signal handlers instead of signal masking
+        * in order to implement trap handling,
+        * exec syscall below resets signals to SIG_DFL for us.
+        */
+       *pp++ = (char *) "-c";
+       *pp++ = (char *) s;
+       if (builtin_argv) {
+               while (*++builtin_argv)
+                       *pp++ = *builtin_argv;
+               *pp++ = (char *) "";
        }
-       /* free_pipe_list has the side effect of clearing memory.
-        * In the long run that function can be merged with run_list,
-        * but doing that now would hobble the debugging effort. */
-       free_pipe_list(pi);
-       debug_printf_exec("run_and_free_list return %d\n", rcode);
-       return rcode;
+       *pp++ = g_argv0;
+       while (*g_argv)
+               *pp++ = *g_argv++;
+       /* *pp = NULL; - is already there */
+       pp = environ;
+
+ do_exec:
+       debug_printf_exec("re_execute_shell pid:%d cmd:'%s'\n", getpid(), s);
+       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
+       execve(bb_busybox_exec_path, argv, pp);
+       /* Fallback. Useful for init=/bin/hush usage etc */
+       if (argv[0][0] == '/')
+               execve(argv[0], argv, pp);
+       xfunc_error_retval = 127;
+       bb_error_msg_and_die("can't re-execute the shell");
 }
+#endif  /* !BB_MMU */
 
 
-static struct pipe *new_pipe(void)
-{
-       struct pipe *pi;
-       pi = xzalloc(sizeof(struct pipe));
-       /*pi->followup = 0; - deliberately invalid value */
-       /*pi->res_word = RES_NONE; - RES_NONE is 0 anyway */
-       return pi;
-}
+static int run_and_free_list(struct pipe *pi);
 
-/* Command (member of a pipe) is complete, or we start a new pipe
- * if ctx->command is NULL.
- * No errors possible here.
+/* Executing from string: eval, sh -c '...'
+ *          or from file: /etc/profile, . file, sh <script>, sh (intereactive)
+ * end_trigger controls how often we stop parsing
+ * NUL: parse all, execute, return
+ * ';': parse till ';' or newline, execute, repeat till EOF
  */
-static int done_command(struct parse_context *ctx)
+static void parse_and_run_stream(struct in_str *inp, int end_trigger)
 {
-       /* The command is really already in the pipe structure, so
-        * advance the pipe counter and make a new, null command. */
-       struct pipe *pi = ctx->pipe;
-       struct command *command = ctx->command;
+       /* Why we need empty flag?
+        * An obscure corner case "false; ``; echo $?":
+        * empty command in `` should still set $? to 0.
+        * But we can't just set $? to 0 at the start,
+        * this breaks "false; echo `echo $?`" case.
+        */
+       bool empty = 1;
+       while (1) {
+               struct pipe *pipe_list;
 
-       if (command) {
-               if (IS_NULL_CMD(command)) {
-                       debug_printf_parse("done_command: skipping null cmd, num_cmds=%d\n", pi->num_cmds);
-                       goto clear_and_ret;
+               pipe_list = parse_stream(NULL, inp, end_trigger);
+               if (!pipe_list) { /* EOF */
+                       if (empty)
+                               G.last_exitcode = 0;
+                       break;
                }
-               pi->num_cmds++;
-               debug_printf_parse("done_command: ++num_cmds=%d\n", pi->num_cmds);
-               //debug_print_tree(ctx->list_head, 20);
-       } else {
-               debug_printf_parse("done_command: initializing, num_cmds=%d\n", pi->num_cmds);
+               debug_print_tree(pipe_list, 0);
+               debug_printf_exec("parse_and_run_stream: run_and_free_list\n");
+               run_and_free_list(pipe_list);
+               empty = 0;
        }
-
-       /* Only real trickiness here is that the uncommitted
-        * command structure is not counted in pi->num_cmds. */
-       pi->cmds = xrealloc(pi->cmds, sizeof(*pi->cmds) * (pi->num_cmds+1));
-       ctx->command = command = &pi->cmds[pi->num_cmds];
- clear_and_ret:
-       memset(command, 0, sizeof(*command));
-       return pi->num_cmds; /* used only for 0/nonzero check */
 }
 
-static void done_pipe(struct parse_context *ctx, pipe_style type)
+static void parse_and_run_string(const char *s)
 {
-       int not_null;
-
-       debug_printf_parse("done_pipe entered, followup %d\n", type);
-       /* Close previous command */
-       not_null = done_command(ctx);
-       ctx->pipe->followup = type;
-#if HAS_KEYWORDS
-       ctx->pipe->pi_inverted = ctx->ctx_inverted;
-       ctx->ctx_inverted = 0;
-       ctx->pipe->res_word = ctx->ctx_res_w;
-#endif
-
-       /* Without this check, even just <enter> on command line generates
-        * tree of three NOPs (!). Which is harmless but annoying.
-        * IOW: it is safe to do it unconditionally. */
-       if (not_null
-#if ENABLE_HUSH_IF
-        || ctx->ctx_res_w == RES_FI
-#endif
-#if ENABLE_HUSH_LOOPS
-        || ctx->ctx_res_w == RES_DONE
-        || ctx->ctx_res_w == RES_FOR
-        || ctx->ctx_res_w == RES_IN
-#endif
-#if ENABLE_HUSH_CASE
-        || ctx->ctx_res_w == RES_ESAC
-#endif
-       ) {
-               struct pipe *new_p;
-               debug_printf_parse("done_pipe: adding new pipe: "
-                               "not_null:%d ctx->ctx_res_w:%d\n",
-                               not_null, ctx->ctx_res_w);
-               new_p = new_pipe();
-               ctx->pipe->next = new_p;
-               ctx->pipe = new_p;
-               /* RES_THEN, RES_DO etc are "sticky" -
-                * they remain set for pipes inside if/while.
-                * This is used to control execution.
-                * RES_FOR and RES_IN are NOT sticky (needed to support
-                * cases where variable or value happens to match a keyword):
-                */
-#if ENABLE_HUSH_LOOPS
-               if (ctx->ctx_res_w == RES_FOR
-                || ctx->ctx_res_w == RES_IN)
-                       ctx->ctx_res_w = RES_NONE;
-#endif
-#if ENABLE_HUSH_CASE
-               if (ctx->ctx_res_w == RES_MATCH)
-                       ctx->ctx_res_w = RES_CASE_BODY;
-               if (ctx->ctx_res_w == RES_CASE)
-                       ctx->ctx_res_w = RES_CASE_IN;
-#endif
-               ctx->command = NULL; /* trick done_command below */
-               /* Create the memory for command, roughly:
-                * ctx->pipe->cmds = new struct command;
-                * ctx->command = &ctx->pipe->cmds[0];
-                */
-               done_command(ctx);
-               //debug_print_tree(ctx->list_head, 10);
-       }
-       debug_printf_parse("done_pipe return\n");
+       struct in_str input;
+       setup_string_in_str(&input, s);
+       parse_and_run_stream(&input, '\0');
 }
 
-static void initialize_context(struct parse_context *ctx)
+static void parse_and_run_file(FILE *f)
 {
-       memset(ctx, 0, sizeof(*ctx));
-       ctx->pipe = ctx->list_head = new_pipe();
-       /* Create the memory for command, roughly:
-        * ctx->pipe->cmds = new struct command;
-        * ctx->command = &ctx->pipe->cmds[0];
-        */
-       done_command(ctx);
+       struct in_str input;
+       setup_file_in_str(&input, f);
+       parse_and_run_stream(&input, ';');
 }
 
-/* If a reserved word is found and processed, parse context is modified
- * and 1 is returned.
- */
-#if HAS_KEYWORDS
-struct reserved_combo {
-       char literal[6];
-       unsigned char res;
-       unsigned char assignment_flag;
-       int flag;
-};
-enum {
-       FLAG_END   = (1 << RES_NONE ),
-# if ENABLE_HUSH_IF
-       FLAG_IF    = (1 << RES_IF   ),
-       FLAG_THEN  = (1 << RES_THEN ),
-       FLAG_ELIF  = (1 << RES_ELIF ),
-       FLAG_ELSE  = (1 << RES_ELSE ),
-       FLAG_FI    = (1 << RES_FI   ),
-# endif
-# if ENABLE_HUSH_LOOPS
-       FLAG_FOR   = (1 << RES_FOR  ),
-       FLAG_WHILE = (1 << RES_WHILE),
-       FLAG_UNTIL = (1 << RES_UNTIL),
-       FLAG_DO    = (1 << RES_DO   ),
-       FLAG_DONE  = (1 << RES_DONE ),
-       FLAG_IN    = (1 << RES_IN   ),
-# endif
-# if ENABLE_HUSH_CASE
-       FLAG_MATCH = (1 << RES_MATCH),
-       FLAG_ESAC  = (1 << RES_ESAC ),
+#if ENABLE_HUSH_TICK
+static FILE *generate_stream_from_string(const char *s, pid_t *pid_p)
+{
+       pid_t pid;
+       int channel[2];
+# if !BB_MMU
+       char **to_free = NULL;
 # endif
-       FLAG_START = (1 << RES_XXXX ),
-};
 
-static const struct reserved_combo* match_reserved_word(o_string *word)
-{
-       /* Mostly a list of accepted follow-up reserved words.
-        * FLAG_END means we are done with the sequence, and are ready
-        * to turn the compound list into a command.
-        * FLAG_START means the word must start a new compound list.
+       xpipe(channel);
+       pid = BB_MMU ? xfork() : xvfork();
+       if (pid == 0) { /* child */
+               disable_restore_tty_pgrp_on_exit();
+               /* Process substitution is not considered to be usual
+                * 'command execution'.
+                * SUSv3 says ctrl-Z should be ignored, ctrl-C should not.
+                */
+               bb_signals(0
+                       + (1 << SIGTSTP)
+                       + (1 << SIGTTIN)
+                       + (1 << SIGTTOU)
+                       , SIG_IGN);
+               CLEAR_RANDOM_T(&G.random_gen); /* or else $RANDOM repeats in child */
+               close(channel[0]); /* NB: close _first_, then move fd! */
+               xmove_fd(channel[1], 1);
+               /* Prevent it from trying to handle ctrl-z etc */
+               IF_HUSH_JOB(G.run_list_level = 1;)
+               /* Awful hack for `trap` or $(trap).
+                *
+                * http://www.opengroup.org/onlinepubs/009695399/utilities/trap.html
+                * contains an example where "trap" is executed in a subshell:
+                *
+                * save_traps=$(trap)
+                * ...
+                * eval "$save_traps"
+                *
+                * Standard does not say that "trap" in subshell shall print
+                * parent shell's traps. It only says that its output
+                * must have suitable form, but then, in the above example
+                * (which is not supposed to be normative), it implies that.
+                *
+                * bash (and probably other shell) does implement it
+                * (traps are reset to defaults, but "trap" still shows them),
+                * but as a result, "trap" logic is hopelessly messed up:
+                *
+                * # trap
+                * trap -- 'echo Ho' SIGWINCH  <--- we have a handler
+                * # (trap)        <--- trap is in subshell - no output (correct, traps are reset)
+                * # true | trap   <--- trap is in subshell - no output (ditto)
+                * # echo `true | trap`    <--- in subshell - output (but traps are reset!)
+                * trap -- 'echo Ho' SIGWINCH
+                * # echo `(trap)`         <--- in subshell in subshell - output
+                * trap -- 'echo Ho' SIGWINCH
+                * # echo `true | (trap)`  <--- in subshell in subshell in subshell - output!
+                * trap -- 'echo Ho' SIGWINCH
+                *
+                * The rules when to forget and when to not forget traps
+                * get really complex and nonsensical.
+                *
+                * Our solution: ONLY bare $(trap) or `trap` is special.
+                */
+               s = skip_whitespace(s);
+               if (strncmp(s, "trap", 4) == 0
+                && skip_whitespace(s + 4)[0] == '\0'
+               ) {
+                       static const char *const argv[] = { NULL, NULL };
+                       builtin_trap((char**)argv);
+                       exit(0); /* not _exit() - we need to fflush */
+               }
+# if BB_MMU
+               reset_traps_to_defaults();
+               parse_and_run_string(s);
+               _exit(G.last_exitcode);
+# else
+       /* We re-execute after vfork on NOMMU. This makes this script safe:
+        * yes "0123456789012345678901234567890" | dd bs=32 count=64k >BIG
+        * huge=`cat BIG` # was blocking here forever
+        * echo OK
         */
-       static const struct reserved_combo reserved_list[] = {
-# if ENABLE_HUSH_IF
-               { "!",     RES_NONE,  NOT_ASSIGNMENT , 0 },
-               { "if",    RES_IF,    WORD_IS_KEYWORD, FLAG_THEN | FLAG_START },
-               { "then",  RES_THEN,  WORD_IS_KEYWORD, FLAG_ELIF | FLAG_ELSE | FLAG_FI },
-               { "elif",  RES_ELIF,  WORD_IS_KEYWORD, FLAG_THEN },
-               { "else",  RES_ELSE,  WORD_IS_KEYWORD, FLAG_FI   },
-               { "fi",    RES_FI,    NOT_ASSIGNMENT , FLAG_END  },
+               re_execute_shell(&to_free,
+                               s,
+                               G.global_argv[0],
+                               G.global_argv + 1,
+                               NULL);
 # endif
-# if ENABLE_HUSH_LOOPS
-               { "for",   RES_FOR,   NOT_ASSIGNMENT , FLAG_IN | FLAG_DO | FLAG_START },
-               { "while", RES_WHILE, WORD_IS_KEYWORD, FLAG_DO | FLAG_START },
-               { "until", RES_UNTIL, WORD_IS_KEYWORD, FLAG_DO | FLAG_START },
-               { "in",    RES_IN,    NOT_ASSIGNMENT , FLAG_DO   },
-               { "do",    RES_DO,    WORD_IS_KEYWORD, FLAG_DONE },
-               { "done",  RES_DONE,  NOT_ASSIGNMENT , FLAG_END  },
+       }
+
+       /* parent */
+       *pid_p = pid;
+# if ENABLE_HUSH_FAST
+       G.count_SIGCHLD++;
+//bb_error_msg("[%d] fork in generate_stream_from_string:"
+//             " G.count_SIGCHLD:%d G.handled_SIGCHLD:%d",
+//             getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
 # endif
-# if ENABLE_HUSH_CASE
-               { "case",  RES_CASE,  NOT_ASSIGNMENT , FLAG_MATCH | FLAG_START },
-               { "esac",  RES_ESAC,  NOT_ASSIGNMENT , FLAG_END  },
+       enable_restore_tty_pgrp_on_exit();
+# if !BB_MMU
+       free(to_free);
 # endif
-       };
-       const struct reserved_combo *r;
-
-       for (r = reserved_list; r < reserved_list + ARRAY_SIZE(reserved_list); r++) {
-               if (strcmp(word->data, r->literal) == 0)
-                       return r;
-       }
-       return NULL;
+       close(channel[1]);
+       close_on_exec_on(channel[0]);
+       return xfdopen_for_read(channel[0]);
 }
-/* Return 0: not a keyword, 1: keyword
- */
-static int reserved_word(o_string *word, struct parse_context *ctx)
+
+/* Return code is exit status of the process that is run. */
+static int process_command_subs(o_string *dest, const char *s)
 {
-# if ENABLE_HUSH_CASE
-       static const struct reserved_combo reserved_match = {
-               "",        RES_MATCH, NOT_ASSIGNMENT , FLAG_MATCH | FLAG_ESAC
-       };
-# endif
-       const struct reserved_combo *r;
+       FILE *fp;
+       struct in_str pipe_str;
+       pid_t pid;
+       int status, ch, eol_cnt;
 
-       if (word->o_quoted)
-               return 0;
-       r = match_reserved_word(word);
-       if (!r)
-               return 0;
+       fp = generate_stream_from_string(s, &pid);
 
-       debug_printf("found reserved word %s, res %d\n", r->literal, r->res);
-# if ENABLE_HUSH_CASE
-       if (r->res == RES_IN && ctx->ctx_res_w == RES_CASE_IN) {
-               /* "case word IN ..." - IN part starts first MATCH part */
-               r = &reserved_match;
-       } else
-# endif
-       if (r->flag == 0) { /* '!' */
-               if (ctx->ctx_inverted) { /* bash doesn't accept '! ! true' */
-                       syntax_error("! ! command");
-                       ctx->ctx_res_w = RES_SNTX;
+       /* Now send results of command back into original context */
+       setup_file_in_str(&pipe_str, fp);
+       eol_cnt = 0;
+       while ((ch = i_getch(&pipe_str)) != EOF) {
+               if (ch == '\n') {
+                       eol_cnt++;
+                       continue;
                }
-               ctx->ctx_inverted = 1;
-               return 1;
+               while (eol_cnt) {
+                       o_addchr(dest, '\n');
+                       eol_cnt--;
+               }
+               o_addQchr(dest, ch);
        }
-       if (r->flag & FLAG_START) {
-               struct parse_context *old;
 
-               old = xmalloc(sizeof(*old));
-               debug_printf_parse("push stack %p\n", old);
-               *old = *ctx;   /* physical copy */
-               initialize_context(ctx);
-               ctx->stack = old;
-       } else if (/*ctx->ctx_res_w == RES_NONE ||*/ !(ctx->old_flag & (1 << r->res))) {
-               syntax_error_at(word->data);
-               ctx->ctx_res_w = RES_SNTX;
-               return 1;
-       } else {
-               /* "{...} fi" is ok. "{...} if" is not
-                * Example:
-                * if { echo foo; } then { echo bar; } fi */
-               if (ctx->command->group)
-                       done_pipe(ctx, PIPE_SEQ);
-       }
+       debug_printf("done reading from `cmd` pipe, closing it\n");
+       fclose(fp);
+       /* We need to extract exitcode. Test case
+        * "true; echo `sleep 1; false` $?"
+        * should print 1 */
+       safe_waitpid(pid, &status, 0);
+       debug_printf("child exited. returning its exitcode:%d\n", WEXITSTATUS(status));
+       return WEXITSTATUS(status);
+}
+#endif /* ENABLE_HUSH_TICK */
 
-       ctx->ctx_res_w = r->res;
-       ctx->old_flag = r->flag;
-       word->o_assignment = r->assignment_flag;
 
-       if (ctx->old_flag & FLAG_END) {
-               struct parse_context *old;
+static void setup_heredoc(struct redir_struct *redir)
+{
+       struct fd_pair pair;
+       pid_t pid;
+       int len, written;
+       /* the _body_ of heredoc (misleading field name) */
+       const char *heredoc = redir->rd_filename;
+       char *expanded;
+#if !BB_MMU
+       char **to_free;
+#endif
 
-               done_pipe(ctx, PIPE_SEQ);
-               debug_printf_parse("pop stack %p\n", ctx->stack);
-               old = ctx->stack;
-               old->command->group = ctx->list_head;
-               old->command->cmd_type = CMD_NORMAL;
-# if !BB_MMU
-               o_addstr(&old->as_string, ctx->as_string.data);
-               o_free_unsafe(&ctx->as_string);
-               old->command->group_as_string = xstrdup(old->as_string.data);
-               debug_printf_parse("pop, remembering as:'%s'\n",
-                               old->command->group_as_string);
-# endif
-               *ctx = *old;   /* physical copy */
-               free(old);
+       expanded = NULL;
+       if (!(redir->rd_dup & HEREDOC_QUOTED)) {
+               expanded = expand_pseudo_dquoted(heredoc);
+               if (expanded)
+                       heredoc = expanded;
        }
-       return 1;
-}
-#endif /* HAS_KEYWORDS */
+       len = strlen(heredoc);
 
-/* Word is complete, look at it and update parsing context.
- * Normal return is 0. Syntax errors return 1.
- * Note: on return, word is reset, but not o_free'd!
- */
-static int done_word(o_string *word, struct parse_context *ctx)
-{
-       struct command *command = ctx->command;
+       close(redir->rd_fd); /* often saves dup2+close in xmove_fd */
+       xpiped_pair(pair);
+       xmove_fd(pair.rd, redir->rd_fd);
 
-       debug_printf_parse("done_word entered: '%s' %p\n", word->data, command);
-       if (word->length == 0 && word->o_quoted == 0) {
-               debug_printf_parse("done_word return 0: true null, ignored\n");
-               return 0;
+       /* Try writing without forking. Newer kernels have
+        * dynamically growing pipes. Must use non-blocking write! */
+       ndelay_on(pair.wr);
+       while (1) {
+               written = write(pair.wr, heredoc, len);
+               if (written <= 0)
+                       break;
+               len -= written;
+               if (len == 0) {
+                       close(pair.wr);
+                       free(expanded);
+                       return;
+               }
+               heredoc += written;
+       }
+       ndelay_off(pair.wr);
+
+       /* Okay, pipe buffer was not big enough */
+       /* Note: we must not create a stray child (bastard? :)
+        * for the unsuspecting parent process. Child creates a grandchild
+        * and exits before parent execs the process which consumes heredoc
+        * (that exec happens after we return from this function) */
+#if !BB_MMU
+       to_free = NULL;
+#endif
+       pid = xvfork();
+       if (pid == 0) {
+               /* child */
+               disable_restore_tty_pgrp_on_exit();
+               pid = BB_MMU ? xfork() : xvfork();
+               if (pid != 0)
+                       _exit(0);
+               /* grandchild */
+               close(redir->rd_fd); /* read side of the pipe */
+#if BB_MMU
+               full_write(pair.wr, heredoc, len); /* may loop or block */
+               _exit(0);
+#else
+               /* Delegate blocking writes to another process */
+               xmove_fd(pair.wr, STDOUT_FILENO);
+               re_execute_shell(&to_free, heredoc, NULL, NULL, NULL);
+#endif
        }
+       /* parent */
+#if ENABLE_HUSH_FAST
+       G.count_SIGCHLD++;
+//bb_error_msg("[%d] fork in setup_heredoc: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+#endif
+       enable_restore_tty_pgrp_on_exit();
+#if !BB_MMU
+       free(to_free);
+#endif
+       close(pair.wr);
+       free(expanded);
+       wait(NULL); /* wait till child has died */
+}
 
-       if (ctx->pending_redirect) {
-               /* We do not glob in e.g. >*.tmp case. bash seems to glob here
-                * only if run as "bash", not "sh" */
-               /* http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html
-                * "2.7 Redirection
-                * ...the word that follows the redirection operator
-                * shall be subjected to tilde expansion, parameter expansion,
-                * command substitution, arithmetic expansion, and quote
-                * removal. Pathname expansion shall not be performed
-                * on the word by a non-interactive shell; an interactive
-                * shell may perform it, but shall do so only when
-                * the expansion would result in one word."
-                */
-               ctx->pending_redirect->rd_filename = xstrdup(word->data);
-               /* Cater for >\file case:
-                * >\a creates file a; >\\a, >"\a", >"\\a" create file \a
-                * Same with heredocs:
-                * for <<\H delim is H; <<\\H, <<"\H", <<"\\H" - \H
-                */
-               if (ctx->pending_redirect->rd_type == REDIRECT_HEREDOC) {
-                       unbackslash(ctx->pending_redirect->rd_filename);
-                       /* Is it <<"HEREDOC"? */
-                       if (word->o_quoted) {
-                               ctx->pending_redirect->rd_dup |= HEREDOC_QUOTED;
+/* squirrel != NULL means we squirrel away copies of stdin, stdout,
+ * and stderr if they are redirected. */
+static int setup_redirects(struct command *prog, int squirrel[])
+{
+       int openfd, mode;
+       struct redir_struct *redir;
+
+       for (redir = prog->redirects; redir; redir = redir->next) {
+               if (redir->rd_type == REDIRECT_HEREDOC2) {
+                       /* rd_fd<<HERE case */
+                       if (squirrel && redir->rd_fd < 3
+                        && squirrel[redir->rd_fd] < 0
+                       ) {
+                               squirrel[redir->rd_fd] = dup(redir->rd_fd);
                        }
-               }
-               debug_printf_parse("word stored in rd_filename: '%s'\n", word->data);
-               ctx->pending_redirect = NULL;
-       } else {
-               /* If this word wasn't an assignment, next ones definitely
-                * can't be assignments. Even if they look like ones. */
-               if (word->o_assignment != DEFINITELY_ASSIGNMENT
-                && word->o_assignment != WORD_IS_KEYWORD
-               ) {
-                       word->o_assignment = NOT_ASSIGNMENT;
-               } else {
-                       if (word->o_assignment == DEFINITELY_ASSIGNMENT)
-                               command->assignment_cnt++;
-                       word->o_assignment = MAYBE_ASSIGNMENT;
+                       /* for REDIRECT_HEREDOC2, rd_filename holds _contents_
+                        * of the heredoc */
+                       debug_printf_parse("set heredoc '%s'\n",
+                                       redir->rd_filename);
+                       setup_heredoc(redir);
+                       continue;
                }
 
-#if HAS_KEYWORDS
-# if ENABLE_HUSH_CASE
-               if (ctx->ctx_dsemicolon
-                && strcmp(word->data, "esac") != 0 /* not "... pattern) cmd;; esac" */
-               ) {
-                       /* already done when ctx_dsemicolon was set to 1: */
-                       /* ctx->ctx_res_w = RES_MATCH; */
-                       ctx->ctx_dsemicolon = 0;
-               } else
-# endif
-               if (!command->argv /* if it's the first word... */
-# if ENABLE_HUSH_LOOPS
-                && ctx->ctx_res_w != RES_FOR /* ...not after FOR or IN */
-                && ctx->ctx_res_w != RES_IN
-# endif
-# if ENABLE_HUSH_CASE
-                && ctx->ctx_res_w != RES_CASE
-# endif
-               ) {
-                       debug_printf_parse("checking '%s' for reserved-ness\n", word->data);
-                       if (reserved_word(word, ctx)) {
-                               o_reset_to_empty_unquoted(word);
-                               debug_printf_parse("done_word return %d\n",
-                                               (ctx->ctx_res_w == RES_SNTX));
-                               return (ctx->ctx_res_w == RES_SNTX);
+               if (redir->rd_dup == REDIRFD_TO_FILE) {
+                       /* rd_fd<*>file case (<*> is <,>,>>,<>) */
+                       char *p;
+                       if (redir->rd_filename == NULL) {
+                               /* Something went wrong in the parse.
+                                * Pretend it didn't happen */
+                               bb_error_msg("bug in redirect parse");
+                               continue;
                        }
-# ifdef CMD_SINGLEWORD_NOGLOB_COND
-                       if (strcmp(word->data, "export") == 0
-#  if ENABLE_HUSH_LOCAL
-                        || strcmp(word->data, "local") == 0
-#  endif
-                       ) {
-                               command->cmd_type = CMD_SINGLEWORD_NOGLOB_COND;
-                       } else
-# endif
-# if ENABLE_HUSH_BASH_COMPAT
-                       if (strcmp(word->data, "[[") == 0) {
-                               command->cmd_type = CMD_SINGLEWORD_NOGLOB;
+                       mode = redir_table[redir->rd_type].mode;
+                       p = expand_string_to_string(redir->rd_filename);
+                       openfd = open_or_warn(p, mode);
+                       free(p);
+                       if (openfd < 0) {
+                       /* this could get lost if stderr has been redirected, but
+                        * bash and ash both lose it as well (though zsh doesn't!) */
+//what the above comment tries to say?
+                               return 1;
                        }
-                       /* fall through */
-# endif
-               }
-#endif
-               if (command->group) {
-                       /* "{ echo foo; } echo bar" - bad */
-                       syntax_error_at(word->data);
-                       debug_printf_parse("done_word return 1: syntax error, "
-                                       "groups and arglists don't mix\n");
-                       return 1;
+               } else {
+                       /* rd_fd<*>rd_dup or rd_fd<*>- cases */
+                       openfd = redir->rd_dup;
                }
-               if (word->o_quoted /* word had "xx" or 'xx' at least as part of it. */
-                /* optimization: and if it's ("" or '') or ($v... or `cmd`...): */
-                && (word->data[0] == '\0' || word->data[0] == SPECIAL_VAR_SYMBOL)
-                /* (otherwise it's known to be not empty and is already safe) */
-               ) {
-                       /* exclude "$@" - it can expand to no word despite "" */
-                       char *p = word->data;
-                       while (p[0] == SPECIAL_VAR_SYMBOL
-                           && (p[1] & 0x7f) == '@'
-                           && p[2] == SPECIAL_VAR_SYMBOL
+
+               if (openfd != redir->rd_fd) {
+                       if (squirrel && redir->rd_fd < 3
+                        && squirrel[redir->rd_fd] < 0
                        ) {
-                               p += 3;
+                               squirrel[redir->rd_fd] = dup(redir->rd_fd);
                        }
-                       if (p == word->data || p[0] != '\0') {
-                               /* saw no "$@", or not only "$@" but some
-                                * real text is there too */
-                               /* insert "empty variable" reference, this makes
-                                * e.g. "", $empty"" etc to not disappear */
-                               o_addchr(word, SPECIAL_VAR_SYMBOL);
-                               o_addchr(word, SPECIAL_VAR_SYMBOL);
+                       if (openfd == REDIRFD_CLOSE) {
+                               /* "n>-" means "close me" */
+                               close(redir->rd_fd);
+                       } else {
+                               xdup2(openfd, redir->rd_fd);
+                               if (redir->rd_dup == REDIRFD_TO_FILE)
+                                       close(openfd);
                        }
                }
-               command->argv = add_string_to_strings(command->argv, xstrdup(word->data));
-               debug_print_strings("word appended to argv", command->argv);
        }
+       return 0;
+}
 
-#if ENABLE_HUSH_LOOPS
-       if (ctx->ctx_res_w == RES_FOR) {
-               if (word->o_quoted
-                || !is_well_formed_var_name(command->argv[0], '\0')
-               ) {
-                       /* bash says just "not a valid identifier" */
-                       syntax_error("not a valid identifier in for");
-                       return 1;
+static void restore_redirects(int squirrel[])
+{
+       int i, fd;
+       for (i = 0; i < 3; i++) {
+               fd = squirrel[i];
+               if (fd != -1) {
+                       /* We simply die on error */
+                       xmove_fd(fd, i);
                }
-               /* Force FOR to have just one word (variable name) */
-               /* NB: basically, this makes hush see "for v in ..."
-                * syntax as if it is "for v; in ...". FOR and IN become
-                * two pipe structs in parse tree. */
-               done_pipe(ctx, PIPE_SEQ);
-       }
-#endif
-#if ENABLE_HUSH_CASE
-       /* Force CASE to have just one word */
-       if (ctx->ctx_res_w == RES_CASE) {
-               done_pipe(ctx, PIPE_SEQ);
        }
-#endif
-
-       o_reset_to_empty_unquoted(word);
-
-       debug_printf_parse("done_word return 0\n");
-       return 0;
 }
 
-
-/* Peek ahead in the input to find out if we have a "&n" construct,
- * as in "2>&1", that represents duplicating a file descriptor.
- * Return:
- * REDIRFD_CLOSE if >&- "close fd" construct is seen,
- * REDIRFD_SYNTAX_ERR if syntax error,
- * REDIRFD_TO_FILE if no & was seen,
- * or the number found.
- */
-#if BB_MMU
-#define parse_redir_right_fd(as_string, input) \
-       parse_redir_right_fd(input)
-#endif
-static int parse_redir_right_fd(o_string *as_string, struct in_str *input)
+static char *find_in_path(const char *arg)
 {
-       int ch, d, ok;
+       char *ret = NULL;
+       const char *PATH = get_local_var_value("PATH");
 
-       ch = i_peek(input);
-       if (ch != '&')
-               return REDIRFD_TO_FILE;
+       if (!PATH)
+               return NULL;
 
-       ch = i_getch(input);  /* get the & */
-       nommu_addchr(as_string, ch);
-       ch = i_peek(input);
-       if (ch == '-') {
-               ch = i_getch(input);
-               nommu_addchr(as_string, ch);
-               return REDIRFD_CLOSE;
-       }
-       d = 0;
-       ok = 0;
-       while (ch != EOF && isdigit(ch)) {
-               d = d*10 + (ch-'0');
-               ok = 1;
-               ch = i_getch(input);
-               nommu_addchr(as_string, ch);
-               ch = i_peek(input);
+       while (1) {
+               const char *end = strchrnul(PATH, ':');
+               int sz = end - PATH; /* must be int! */
+
+               free(ret);
+               if (sz != 0) {
+                       ret = xasprintf("%.*s/%s", sz, PATH, arg);
+               } else {
+                       /* We have xxx::yyyy in $PATH,
+                        * it means "use current dir" */
+                       ret = xstrdup(arg);
+               }
+               if (access(ret, F_OK) == 0)
+                       break;
+
+               if (*end == '\0') {
+                       free(ret);
+                       return NULL;
+               }
+               PATH = end + 1;
        }
-       if (ok) return d;
 
-//TODO: this is the place to catch ">&file" bashism (redirect both fd 1 and 2)
+       return ret;
+}
 
-       bb_error_msg("ambiguous redirect");
-       return REDIRFD_SYNTAX_ERR;
+static const struct built_in_command *find_builtin_helper(const char *name,
+               const struct built_in_command *x,
+               const struct built_in_command *end)
+{
+       while (x != end) {
+               if (strcmp(name, x->b_cmd) != 0) {
+                       x++;
+                       continue;
+               }
+               debug_printf_exec("found builtin '%s'\n", name);
+               return x;
+       }
+       return NULL;
 }
-
-/* Return code is 0 normal, 1 if a syntax error is detected
- */
-static int parse_redirect(struct parse_context *ctx,
-               int fd,
-               redir_type style,
-               struct in_str *input)
+static const struct built_in_command *find_builtin1(const char *name)
 {
-       struct command *command = ctx->command;
-       struct redir_struct *redir;
-       struct redir_struct **redirp;
-       int dup_num;
+       return find_builtin_helper(name, bltins1, &bltins1[ARRAY_SIZE(bltins1)]);
+}
+static const struct built_in_command *find_builtin(const char *name)
+{
+       const struct built_in_command *x = find_builtin1(name);
+       if (x)
+               return x;
+       return find_builtin_helper(name, bltins2, &bltins2[ARRAY_SIZE(bltins2)]);
+}
 
-       dup_num = REDIRFD_TO_FILE;
-       if (style != REDIRECT_HEREDOC) {
-               /* Check for a '>&1' type redirect */
-               dup_num = parse_redir_right_fd(&ctx->as_string, input);
-               if (dup_num == REDIRFD_SYNTAX_ERR)
-                       return 1;
-       } else {
-               int ch = i_peek(input);
-               dup_num = (ch == '-'); /* HEREDOC_SKIPTABS bit is 1 */
-               if (dup_num) { /* <<-... */
-                       ch = i_getch(input);
-                       nommu_addchr(&ctx->as_string, ch);
-                       ch = i_peek(input);
+#if ENABLE_HUSH_FUNCTIONS
+static struct function **find_function_slot(const char *name)
+{
+       struct function **funcpp = &G.top_func;
+       while (*funcpp) {
+               if (strcmp(name, (*funcpp)->name) == 0) {
+                       break;
                }
+               funcpp = &(*funcpp)->next;
        }
+       return funcpp;
+}
 
-       if (style == REDIRECT_OVERWRITE && dup_num == REDIRFD_TO_FILE) {
-               int ch = i_peek(input);
-               if (ch == '|') {
-                       /* >|FILE redirect ("clobbering" >).
-                        * Since we do not support "set -o noclobber" yet,
-                        * >| and > are the same for now. Just eat |.
-                        */
-                       ch = i_getch(input);
-                       nommu_addchr(&ctx->as_string, ch);
+static const struct function *find_function(const char *name)
+{
+       const struct function *funcp = *find_function_slot(name);
+       if (funcp)
+               debug_printf_exec("found function '%s'\n", name);
+       return funcp;
+}
+
+/* Note: takes ownership on name ptr */
+static struct function *new_function(char *name)
+{
+       struct function **funcpp = find_function_slot(name);
+       struct function *funcp = *funcpp;
+
+       if (funcp != NULL) {
+               struct command *cmd = funcp->parent_cmd;
+               debug_printf_exec("func %p parent_cmd %p\n", funcp, cmd);
+               if (!cmd) {
+                       debug_printf_exec("freeing & replacing function '%s'\n", funcp->name);
+                       free(funcp->name);
+                       /* Note: if !funcp->body, do not free body_as_string!
+                        * This is a special case of "-F name body" function:
+                        * body_as_string was not malloced! */
+                       if (funcp->body) {
+                               free_pipe_list(funcp->body);
+# if !BB_MMU
+                               free(funcp->body_as_string);
+# endif
+                       }
+               } else {
+                       debug_printf_exec("reinserting in tree & replacing function '%s'\n", funcp->name);
+                       cmd->argv[0] = funcp->name;
+                       cmd->group = funcp->body;
+# if !BB_MMU
+                       cmd->group_as_string = funcp->body_as_string;
+# endif
                }
+       } else {
+               debug_printf_exec("remembering new function '%s'\n", name);
+               funcp = *funcpp = xzalloc(sizeof(*funcp));
+               /*funcp->next = NULL;*/
        }
 
-       /* Create a new redir_struct and append it to the linked list */
-       redirp = &command->redirects;
-       while ((redir = *redirp) != NULL) {
-               redirp = &(redir->next);
-       }
-       *redirp = redir = xzalloc(sizeof(*redir));
-       /* redir->next = NULL; */
-       /* redir->rd_filename = NULL; */
-       redir->rd_type = style;
-       redir->rd_fd = (fd == -1) ? redir_table[style].default_fd : fd;
+       funcp->name = name;
+       return funcp;
+}
 
-       debug_printf_parse("redirect type %d %s\n", redir->rd_fd,
-                               redir_table[style].descrip);
+static void unset_func(const char *name)
+{
+       struct function **funcpp = find_function_slot(name);
+       struct function *funcp = *funcpp;
 
-       redir->rd_dup = dup_num;
-       if (style != REDIRECT_HEREDOC && dup_num != REDIRFD_TO_FILE) {
-               /* Erik had a check here that the file descriptor in question
-                * is legit; I postpone that to "run time"
-                * A "-" representation of "close me" shows up as a -3 here */
-               debug_printf_parse("duplicating redirect '%d>&%d'\n",
-                               redir->rd_fd, redir->rd_dup);
-       } else {
-               /* Set ctx->pending_redirect, so we know what to do at the
-                * end of the next parsed word. */
-               ctx->pending_redirect = redir;
+       if (funcp != NULL) {
+               debug_printf_exec("freeing function '%s'\n", funcp->name);
+               *funcpp = funcp->next;
+               /* funcp is unlinked now, deleting it.
+                * Note: if !funcp->body, the function was created by
+                * "-F name body", do not free ->body_as_string
+                * and ->name as they were not malloced. */
+               if (funcp->body) {
+                       free_pipe_list(funcp->body);
+                       free(funcp->name);
+# if !BB_MMU
+                       free(funcp->body_as_string);
+# endif
+               }
+               free(funcp);
        }
-       return 0;
 }
 
-/* If a redirect is immediately preceded by a number, that number is
- * supposed to tell which file descriptor to redirect.  This routine
- * looks for such preceding numbers.  In an ideal world this routine
- * needs to handle all the following classes of redirects...
- *     echo 2>foo     # redirects fd  2 to file "foo", nothing passed to echo
- *     echo 49>foo    # redirects fd 49 to file "foo", nothing passed to echo
- *     echo -2>foo    # redirects fd  1 to file "foo",    "-2" passed to echo
- *     echo 49x>foo   # redirects fd  1 to file "foo",   "49x" passed to echo
- *
- * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html
- * "2.7 Redirection
- * ... If n is quoted, the number shall not be recognized as part of
- * the redirection expression. For example:
- * echo \2>a
- * writes the character 2 into file a"
- * We are getting it right by setting ->o_quoted on any \<char>
- *
- * A -1 return means no valid number was found,
- * the caller should use the appropriate default for this redirection.
- */
-static int redirect_opt_num(o_string *o)
+# if BB_MMU
+#define exec_function(to_free, funcp, argv) \
+       exec_function(funcp, argv)
+# endif
+static void exec_function(char ***to_free,
+               const struct function *funcp,
+               char **argv) NORETURN;
+static void exec_function(char ***to_free,
+               const struct function *funcp,
+               char **argv)
 {
-       int num;
+# if BB_MMU
+       int n = 1;
 
-       if (o->data == NULL)
-               return -1;
-       num = bb_strtou(o->data, NULL, 10);
-       if (errno || num < 0)
-               return -1;
-       o_reset_to_empty_unquoted(o);
-       return num;
+       argv[0] = G.global_argv[0];
+       G.global_argv = argv;
+       while (*++argv)
+               n++;
+       G.global_argc = n;
+       /* On MMU, funcp->body is always non-NULL */
+       n = run_list(funcp->body);
+       fflush_all();
+       _exit(n);
+# else
+       re_execute_shell(to_free,
+                       funcp->body_as_string,
+                       G.global_argv[0],
+                       argv + 1,
+                       NULL);
+# endif
 }
 
-#if BB_MMU
-#define fetch_till_str(as_string, input, word, skip_tabs) \
-       fetch_till_str(input, word, skip_tabs)
-#endif
-static char *fetch_till_str(o_string *as_string,
-               struct in_str *input,
-               const char *word,
-               int skip_tabs)
+static int run_function(const struct function *funcp, char **argv)
 {
-       o_string heredoc = NULL_O_STRING;
-       int past_EOL = 0;
-       int ch;
+       int rc;
+       save_arg_t sv;
+       smallint sv_flg;
 
-       goto jump_in;
-       while (1) {
-               ch = i_getch(input);
-               nommu_addchr(as_string, ch);
-               if (ch == '\n') {
-                       if (strcmp(heredoc.data + past_EOL, word) == 0) {
-                               heredoc.data[past_EOL] = '\0';
-                               debug_printf_parse("parsed heredoc '%s'\n", heredoc.data);
-                               return heredoc.data;
-                       }
-                       do {
-                               o_addchr(&heredoc, ch);
-                               past_EOL = heredoc.length;
- jump_in:
-                               do {
-                                       ch = i_getch(input);
-                                       nommu_addchr(as_string, ch);
-                               } while (skip_tabs && ch == '\t');
-                       } while (ch == '\n');
-               }
-               if (ch == EOF) {
-                       o_free_unsafe(&heredoc);
-                       return NULL;
+       save_and_replace_G_args(&sv, argv);
+
+       /* "we are in function, ok to use return" */
+       sv_flg = G.flag_return_in_progress;
+       G.flag_return_in_progress = -1;
+# if ENABLE_HUSH_LOCAL
+       G.func_nest_level++;
+# endif
+
+       /* On MMU, funcp->body is always non-NULL */
+# if !BB_MMU
+       if (!funcp->body) {
+               /* Function defined by -F */
+               parse_and_run_string(funcp->body_as_string);
+               rc = G.last_exitcode;
+       } else
+# endif
+       {
+               rc = run_list(funcp->body);
+       }
+
+# if ENABLE_HUSH_LOCAL
+       {
+               struct variable *var;
+               struct variable **var_pp;
+
+               var_pp = &G.top_var;
+               while ((var = *var_pp) != NULL) {
+                       if (var->func_nest_level < G.func_nest_level) {
+                               var_pp = &var->next;
+                               continue;
+                       }
+                       /* Unexport */
+                       if (var->flg_export)
+                               bb_unsetenv(var->varstr);
+                       /* Remove from global list */
+                       *var_pp = var->next;
+                       /* Free */
+                       if (!var->max_len)
+                               free(var->varstr);
+                       free(var);
                }
-               o_addchr(&heredoc, ch);
-               nommu_addchr(as_string, ch);
+               G.func_nest_level--;
        }
+# endif
+       G.flag_return_in_progress = sv_flg;
+
+       restore_G_args(&sv, argv);
+
+       return rc;
 }
+#endif /* ENABLE_HUSH_FUNCTIONS */
 
-/* Look at entire parse tree for not-yet-loaded REDIRECT_HEREDOCs
- * and load them all. There should be exactly heredoc_cnt of them.
- */
-static int fetch_heredocs(int heredoc_cnt, struct parse_context *ctx, struct in_str *input)
+
+#if BB_MMU
+#define exec_builtin(to_free, x, argv) \
+       exec_builtin(x, argv)
+#else
+#define exec_builtin(to_free, x, argv) \
+       exec_builtin(to_free, argv)
+#endif
+static void exec_builtin(char ***to_free,
+               const struct built_in_command *x,
+               char **argv) NORETURN;
+static void exec_builtin(char ***to_free,
+               const struct built_in_command *x,
+               char **argv)
 {
-       struct pipe *pi = ctx->list_head;
+#if BB_MMU
+       int rcode = x->b_function(argv);
+       fflush_all();
+       _exit(rcode);
+#else
+       /* On NOMMU, we must never block!
+        * Example: { sleep 99 | read line; } & echo Ok
+        */
+       re_execute_shell(to_free,
+                       argv[0],
+                       G.global_argv[0],
+                       G.global_argv + 1,
+                       argv);
+#endif
+}
 
-       while (pi && heredoc_cnt) {
-               int i;
-               struct command *cmd = pi->cmds;
 
-               debug_printf_parse("fetch_heredocs: num_cmds:%d cmd argv0:'%s'\n",
-                               pi->num_cmds,
-                               cmd->argv ? cmd->argv[0] : "NONE");
-               for (i = 0; i < pi->num_cmds; i++) {
-                       struct redir_struct *redir = cmd->redirects;
+static void execvp_or_die(char **argv) NORETURN;
+static void execvp_or_die(char **argv)
+{
+       debug_printf_exec("execing '%s'\n", argv[0]);
+       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
+       execvp(argv[0], argv);
+       bb_perror_msg("can't execute '%s'", argv[0]);
+       _exit(127); /* bash compat */
+}
 
-                       debug_printf_parse("fetch_heredocs: %d cmd argv0:'%s'\n",
-                                       i, cmd->argv ? cmd->argv[0] : "NONE");
-                       while (redir) {
-                               if (redir->rd_type == REDIRECT_HEREDOC) {
-                                       char *p;
+#if ENABLE_HUSH_MODE_X
+static void dump_cmd_in_x_mode(char **argv)
+{
+       if (G_x_mode && argv) {
+               /* We want to output the line in one write op */
+               char *buf, *p;
+               int len;
+               int n;
 
-                                       redir->rd_type = REDIRECT_HEREDOC2;
-                                       /* redir->rd_dup is (ab)used to indicate <<- */
-                                       p = fetch_till_str(&ctx->as_string, input,
-                                               redir->rd_filename, redir->rd_dup & HEREDOC_SKIPTABS);
-                                       if (!p) {
-                                               syntax_error("unexpected EOF in here document");
-                                               return 1;
-                                       }
-                                       free(redir->rd_filename);
-                                       redir->rd_filename = p;
-                                       heredoc_cnt--;
-                               }
-                               redir = redir->next;
-                       }
-                       cmd++;
-               }
-               pi = pi->next;
+               len = 3;
+               n = 0;
+               while (argv[n])
+                       len += strlen(argv[n++]) + 1;
+               buf = xmalloc(len);
+               buf[0] = '+';
+               p = buf + 1;
+               n = 0;
+               while (argv[n])
+                       p += sprintf(p, " %s", argv[n++]);
+               *p++ = '\n';
+               *p = '\0';
+               fputs(buf, stderr);
+               free(buf);
        }
-#if 0
-       /* Should be 0. If it isn't, it's a parse error */
-       if (heredoc_cnt)
-               bb_error_msg_and_die("heredoc BUG 2");
-#endif
-       return 0;
 }
+#else
+# define dump_cmd_in_x_mode(argv) ((void)0)
+#endif
 
+#if BB_MMU
+#define pseudo_exec_argv(nommu_save, argv, assignment_cnt, argv_expanded) \
+       pseudo_exec_argv(argv, assignment_cnt, argv_expanded)
+#define pseudo_exec(nommu_save, command, argv_expanded) \
+       pseudo_exec(command, argv_expanded)
+#endif
 
-#if ENABLE_HUSH_TICK
-static FILE *generate_stream_from_string(const char *s, pid_t *pid_p)
+/* Called after [v]fork() in run_pipe, or from builtin_exec.
+ * Never returns.
+ * Don't exit() here.  If you don't exec, use _exit instead.
+ * The at_exit handlers apparently confuse the calling process,
+ * in particular stdin handling.  Not sure why? -- because of vfork! (vda) */
+static void pseudo_exec_argv(nommu_save_t *nommu_save,
+               char **argv, int assignment_cnt,
+               char **argv_expanded) NORETURN;
+static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
+               char **argv, int assignment_cnt,
+               char **argv_expanded)
 {
-       pid_t pid;
-       int channel[2];
-# if !BB_MMU
-       char **to_free = NULL;
-# endif
+       char **new_env;
 
-       xpipe(channel);
-       pid = BB_MMU ? xfork() : xvfork();
-       if (pid == 0) { /* child */
-               disable_restore_tty_pgrp_on_exit();
-               /* Process substitution is not considered to be usual
-                * 'command execution'.
-                * SUSv3 says ctrl-Z should be ignored, ctrl-C should not.
-                */
-               bb_signals(0
-                       + (1 << SIGTSTP)
-                       + (1 << SIGTTIN)
-                       + (1 << SIGTTOU)
-                       , SIG_IGN);
-               CLEAR_RANDOM_T(&G.random_gen); /* or else $RANDOM repeats in child */
-               close(channel[0]); /* NB: close _first_, then move fd! */
-               xmove_fd(channel[1], 1);
-               /* Prevent it from trying to handle ctrl-z etc */
-               IF_HUSH_JOB(G.run_list_level = 1;)
-               /* Awful hack for `trap` or $(trap).
-                *
-                * http://www.opengroup.org/onlinepubs/009695399/utilities/trap.html
-                * contains an example where "trap" is executed in a subshell:
-                *
-                * save_traps=$(trap)
-                * ...
-                * eval "$save_traps"
-                *
-                * Standard does not say that "trap" in subshell shall print
-                * parent shell's traps. It only says that its output
-                * must have suitable form, but then, in the above example
-                * (which is not supposed to be normative), it implies that.
-                *
-                * bash (and probably other shell) does implement it
-                * (traps are reset to defaults, but "trap" still shows them),
-                * but as a result, "trap" logic is hopelessly messed up:
-                *
-                * # trap
-                * trap -- 'echo Ho' SIGWINCH  <--- we have a handler
-                * # (trap)        <--- trap is in subshell - no output (correct, traps are reset)
-                * # true | trap   <--- trap is in subshell - no output (ditto)
-                * # echo `true | trap`    <--- in subshell - output (but traps are reset!)
-                * trap -- 'echo Ho' SIGWINCH
-                * # echo `(trap)`         <--- in subshell in subshell - output
-                * trap -- 'echo Ho' SIGWINCH
-                * # echo `true | (trap)`  <--- in subshell in subshell in subshell - output!
-                * trap -- 'echo Ho' SIGWINCH
-                *
-                * The rules when to forget and when to not forget traps
-                * get really complex and nonsensical.
-                *
-                * Our solution: ONLY bare $(trap) or `trap` is special.
+       new_env = expand_assignments(argv, assignment_cnt);
+       dump_cmd_in_x_mode(new_env);
+
+       if (!argv[assignment_cnt]) {
+               /* Case when we are here: ... | var=val | ...
+                * (note that we do not exit early, i.e., do not optimize out
+                * expand_assignments(): think about ... | var=`sleep 1` | ...
                 */
-               s = skip_whitespace(s);
-               if (strncmp(s, "trap", 4) == 0 && (*skip_whitespace(s + 4) == '\0'))
-               {
-                       static const char *const argv[] = { NULL, NULL };
-                       builtin_trap((char**)argv);
-                       exit(0); /* not _exit() - we need to fflush */
-               }
-# if BB_MMU
-               reset_traps_to_defaults();
-               parse_and_run_string(s);
-               _exit(G.last_exitcode);
-# else
-       /* We re-execute after vfork on NOMMU. This makes this script safe:
-        * yes "0123456789012345678901234567890" | dd bs=32 count=64k >BIG
-        * huge=`cat BIG` # was blocking here forever
-        * echo OK
-        */
-               re_execute_shell(&to_free,
-                               s,
-                               G.global_argv[0],
-                               G.global_argv + 1,
-                               NULL);
-# endif
+               free_strings(new_env);
+               _exit(EXIT_SUCCESS);
        }
 
-       /* parent */
-       *pid_p = pid;
-# if ENABLE_HUSH_FAST
-       G.count_SIGCHLD++;
-//bb_error_msg("[%d] fork in generate_stream_from_string: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
-# endif
-       enable_restore_tty_pgrp_on_exit();
-# if !BB_MMU
-       free(to_free);
-# endif
-       close(channel[1]);
-       close_on_exec_on(channel[0]);
-       return xfdopen_for_read(channel[0]);
-}
-
-/* Return code is exit status of the process that is run. */
-static int process_command_subs(o_string *dest, const char *s)
-{
-       FILE *fp;
-       struct in_str pipe_str;
-       pid_t pid;
-       int status, ch, eol_cnt;
-
-       fp = generate_stream_from_string(s, &pid);
+#if BB_MMU
+       set_vars_and_save_old(new_env);
+       free(new_env); /* optional */
+       /* we can also destroy set_vars_and_save_old's return value,
+        * to save memory */
+#else
+       nommu_save->new_env = new_env;
+       nommu_save->old_vars = set_vars_and_save_old(new_env);
+#endif
 
-       /* Now send results of command back into original context */
-       setup_file_in_str(&pipe_str, fp);
-       eol_cnt = 0;
-       while ((ch = i_getch(&pipe_str)) != EOF) {
-               if (ch == '\n') {
-                       eol_cnt++;
-                       continue;
-               }
-               while (eol_cnt) {
-                       o_addchr(dest, '\n');
-                       eol_cnt--;
-               }
-               o_addQchr(dest, ch);
+       if (argv_expanded) {
+               argv = argv_expanded;
+       } else {
+               argv = expand_strvec_to_strvec(argv + assignment_cnt);
+#if !BB_MMU
+               nommu_save->argv = argv;
+#endif
        }
+       dump_cmd_in_x_mode(argv);
 
-       debug_printf("done reading from `cmd` pipe, closing it\n");
-       fclose(fp);
-       /* We need to extract exitcode. Test case
-        * "true; echo `sleep 1; false` $?"
-        * should print 1 */
-       safe_waitpid(pid, &status, 0);
-       debug_printf("child exited. returning its exitcode:%d\n", WEXITSTATUS(status));
-       return WEXITSTATUS(status);
-}
-#endif /* ENABLE_HUSH_TICK */
-
-#if !ENABLE_HUSH_FUNCTIONS
-#define parse_group(dest, ctx, input, ch) \
-       parse_group(ctx, input, ch)
+#if ENABLE_FEATURE_SH_STANDALONE || BB_MMU
+       if (strchr(argv[0], '/') != NULL)
+               goto skip;
 #endif
-static int parse_group(o_string *dest, struct parse_context *ctx,
-       struct in_str *input, int ch)
-{
-       /* dest contains characters seen prior to ( or {.
-        * Typically it's empty, but for function defs,
-        * it contains function name (without '()'). */
-       struct pipe *pipe_list;
-       int endch;
-       struct command *command = ctx->command;
 
-       debug_printf_parse("parse_group entered\n");
-#if ENABLE_HUSH_FUNCTIONS
-       if (ch == '(' && !dest->o_quoted) {
-               if (dest->length)
-                       if (done_word(dest, ctx))
-                               return 1;
-               if (!command->argv)
-                       goto skip; /* (... */
-               if (command->argv[1]) { /* word word ... (... */
-                       syntax_error_unexpected_ch('(');
-                       return 1;
-               }
-               /* it is "word(..." or "word (..." */
-               do
-                       ch = i_getch(input);
-               while (ch == ' ' || ch == '\t');
-               if (ch != ')') {
-                       syntax_error_unexpected_ch(ch);
-                       return 1;
+       /* Check if the command matches any of the builtins.
+        * Depending on context, this might be redundant.  But it's
+        * easier to waste a few CPU cycles than it is to figure out
+        * if this is one of those cases.
+        */
+       {
+               /* On NOMMU, it is more expensive to re-execute shell
+                * just in order to run echo or test builtin.
+                * It's better to skip it here and run corresponding
+                * non-builtin later. */
+               const struct built_in_command *x;
+               x = BB_MMU ? find_builtin(argv[0]) : find_builtin1(argv[0]);
+               if (x) {
+                       exec_builtin(&nommu_save->argv_from_re_execing, x, argv);
                }
-               nommu_addchr(&ctx->as_string, ch);
-               do
-                       ch = i_getch(input);
-               while (ch == ' ' || ch == '\t' || ch == '\n');
-               if (ch != '{') {
-                       syntax_error_unexpected_ch(ch);
-                       return 1;
+       }
+#if ENABLE_HUSH_FUNCTIONS
+       /* Check if the command matches any functions */
+       {
+               const struct function *funcp = find_function(argv[0]);
+               if (funcp) {
+                       exec_function(&nommu_save->argv_from_re_execing, funcp, argv);
                }
-               nommu_addchr(&ctx->as_string, ch);
-               command->cmd_type = CMD_FUNCDEF;
-               goto skip;
        }
 #endif
 
-#if 0 /* Prevented by caller */
-       if (command->argv /* word [word]{... */
-        || dest->length /* word{... */
-        || dest->o_quoted /* ""{... */
-       ) {
-               syntax_error(NULL);
-               debug_printf_parse("parse_group return 1: "
-                       "syntax error, groups and arglists don't mix\n");
-               return 1;
+#if ENABLE_FEATURE_SH_STANDALONE
+       /* Check if the command matches any busybox applets */
+       {
+               int a = find_applet_by_name(argv[0]);
+               if (a >= 0) {
+# if BB_MMU /* see above why on NOMMU it is not allowed */
+                       if (APPLET_IS_NOEXEC(a)) {
+                               debug_printf_exec("running applet '%s'\n", argv[0]);
+                               run_applet_no_and_exit(a, argv);
+                       }
+# endif
+                       /* Re-exec ourselves */
+                       debug_printf_exec("re-execing applet '%s'\n", argv[0]);
+                       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
+                       execv(bb_busybox_exec_path, argv);
+                       /* If they called chroot or otherwise made the binary no longer
+                        * executable, fall through */
+               }
        }
 #endif
 
-#if ENABLE_HUSH_FUNCTIONS
+#if ENABLE_FEATURE_SH_STANDALONE || BB_MMU
  skip:
 #endif
-       endch = '}';
-       if (ch == '(') {
-               endch = ')';
-               command->cmd_type = CMD_SUBSHELL;
-       } else {
-               /* bash does not allow "{echo...", requires whitespace */
-               ch = i_getch(input);
-               if (ch != ' ' && ch != '\t' && ch != '\n') {
-                       syntax_error_unexpected_ch(ch);
-                       return 1;
-               }
-               nommu_addchr(&ctx->as_string, ch);
+       execvp_or_die(argv);
+}
+
+/* Called after [v]fork() in run_pipe
+ */
+static void pseudo_exec(nommu_save_t *nommu_save,
+               struct command *command,
+               char **argv_expanded) NORETURN;
+static void pseudo_exec(nommu_save_t *nommu_save,
+               struct command *command,
+               char **argv_expanded)
+{
+       if (command->argv) {
+               pseudo_exec_argv(nommu_save, command->argv,
+                               command->assignment_cnt, argv_expanded);
        }
 
-       {
+       if (command->group) {
+               /* Cases when we are here:
+                * ( list )
+                * { list } &
+                * ... | ( list ) | ...
+                * ... | { list } | ...
+                */
 #if BB_MMU
-# define as_string NULL
+               int rcode;
+               debug_printf_exec("pseudo_exec: run_list\n");
+               reset_traps_to_defaults();
+               rcode = run_list(command->group);
+               /* OK to leak memory by not calling free_pipe_list,
+                * since this process is about to exit */
+               _exit(rcode);
 #else
-               char *as_string = NULL;
-#endif
-               pipe_list = parse_stream(&as_string, input, endch);
-#if !BB_MMU
-               if (as_string)
-                       o_addstr(&ctx->as_string, as_string);
-#endif
-               /* empty ()/{} or parse error? */
-               if (!pipe_list || pipe_list == ERR_PTR) {
-                       /* parse_stream already emitted error msg */
-                       if (!BB_MMU)
-                               free(as_string);
-                       debug_printf_parse("parse_group return 1: "
-                               "parse_stream returned %p\n", pipe_list);
-                       return 1;
-               }
-               command->group = pipe_list;
-#if !BB_MMU
-               as_string[strlen(as_string) - 1] = '\0'; /* plink ')' or '}' */
-               command->group_as_string = as_string;
-               debug_printf_parse("end of group, remembering as:'%s'\n",
-                               command->group_as_string);
+               re_execute_shell(&nommu_save->argv_from_re_execing,
+                               command->group_as_string,
+                               G.global_argv[0],
+                               G.global_argv + 1,
+                               NULL);
 #endif
-#undef as_string
        }
-       debug_printf_parse("parse_group return 0\n");
-       return 0;
-       /* command remains "open", available for possible redirects */
+
+       /* Case when we are here: ... | >file */
+       debug_printf_exec("pseudo_exec'ed null command\n");
+       _exit(EXIT_SUCCESS);
+}
+
+#if ENABLE_HUSH_JOB
+static const char *get_cmdtext(struct pipe *pi)
+{
+       char **argv;
+       char *p;
+       int len;
+
+       /* This is subtle. ->cmdtext is created only on first backgrounding.
+        * (Think "cat, <ctrl-z>, fg, <ctrl-z>, fg, <ctrl-z>...." here...)
+        * On subsequent bg argv is trashed, but we won't use it */
+       if (pi->cmdtext)
+               return pi->cmdtext;
+       argv = pi->cmds[0].argv;
+       if (!argv || !argv[0]) {
+               pi->cmdtext = xzalloc(1);
+               return pi->cmdtext;
+       }
+
+       len = 0;
+       do {
+               len += strlen(*argv) + 1;
+       } while (*++argv);
+       p = xmalloc(len);
+       pi->cmdtext = p;
+       argv = pi->cmds[0].argv;
+       do {
+               len = strlen(*argv);
+               memcpy(p, *argv, len);
+               p += len;
+               *p++ = ' ';
+       } while (*++argv);
+       p[-1] = '\0';
+       return pi->cmdtext;
+}
+
+static void insert_bg_job(struct pipe *pi)
+{
+       struct pipe *job, **jobp;
+       int i;
+
+       /* Linear search for the ID of the job to use */
+       pi->jobid = 1;
+       for (job = G.job_list; job; job = job->next)
+               if (job->jobid >= pi->jobid)
+                       pi->jobid = job->jobid + 1;
+
+       /* Add job to the list of running jobs */
+       jobp = &G.job_list;
+       while ((job = *jobp) != NULL)
+               jobp = &job->next;
+       job = *jobp = xmalloc(sizeof(*job));
+
+       *job = *pi; /* physical copy */
+       job->next = NULL;
+       job->cmds = xzalloc(sizeof(pi->cmds[0]) * pi->num_cmds);
+       /* Cannot copy entire pi->cmds[] vector! This causes double frees */
+       for (i = 0; i < pi->num_cmds; i++) {
+               job->cmds[i].pid = pi->cmds[i].pid;
+               /* all other fields are not used and stay zero */
+       }
+       job->cmdtext = xstrdup(get_cmdtext(pi));
+
+       if (G_interactive_fd)
+               printf("[%d] %d %s\n", job->jobid, job->cmds[0].pid, job->cmdtext);
+       G.last_jobid = job->jobid;
 }
 
-#if ENABLE_HUSH_TICK || ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_DOLLAR_OPS
-/* Subroutines for copying $(...) and `...` things */
-static void add_till_backquote(o_string *dest, struct in_str *input);
-/* '...' */
-static void add_till_single_quote(o_string *dest, struct in_str *input)
+static void remove_bg_job(struct pipe *pi)
 {
-       while (1) {
-               int ch = i_getch(input);
-               if (ch == EOF) {
-                       syntax_error_unterm_ch('\'');
-                       /*xfunc_die(); - redundant */
-               }
-               if (ch == '\'')
-                       return;
-               o_addchr(dest, ch);
+       struct pipe *prev_pipe;
+
+       if (pi == G.job_list) {
+               G.job_list = pi->next;
+       } else {
+               prev_pipe = G.job_list;
+               while (prev_pipe->next != pi)
+                       prev_pipe = prev_pipe->next;
+               prev_pipe->next = pi->next;
        }
+       if (G.job_list)
+               G.last_jobid = G.job_list->jobid;
+       else
+               G.last_jobid = 0;
 }
-/* "...\"...`..`...." - do we need to handle "...$(..)..." too? */
-static void add_till_double_quote(o_string *dest, struct in_str *input)
+
+/* Remove a backgrounded job */
+static void delete_finished_bg_job(struct pipe *pi)
 {
-       while (1) {
-               int ch = i_getch(input);
-               if (ch == EOF) {
-                       syntax_error_unterm_ch('"');
-                       /*xfunc_die(); - redundant */
-               }
-               if (ch == '"')
-                       return;
-               if (ch == '\\') {  /* \x. Copy both chars. */
-                       o_addchr(dest, ch);
-                       ch = i_getch(input);
-               }
-               o_addchr(dest, ch);
-               if (ch == '`') {
-                       add_till_backquote(dest, input);
-                       o_addchr(dest, ch);
-                       continue;
-               }
-               //if (ch == '$') ...
-       }
+       remove_bg_job(pi);
+       free_pipe(pi);
 }
-/* Process `cmd` - copy contents until "`" is seen. Complicated by
- * \` quoting.
- * "Within the backquoted style of command substitution, backslash
- * shall retain its literal meaning, except when followed by: '$', '`', or '\'.
- * The search for the matching backquote shall be satisfied by the first
- * backquote found without a preceding backslash; during this search,
- * if a non-escaped backquote is encountered within a shell comment,
- * a here-document, an embedded command substitution of the $(command)
- * form, or a quoted string, undefined results occur. A single-quoted
- * or double-quoted string that begins, but does not end, within the
- * "`...`" sequence produces undefined results."
- * Example                               Output
- * echo `echo '\'TEST\`echo ZZ\`BEST`    \TESTZZBEST
- */
-static void add_till_backquote(o_string *dest, struct in_str *input)
+#endif /* JOB */
+
+/* Check to see if any processes have exited -- if they
+ * have, figure out why and see if a job has completed */
+static int checkjobs(struct pipe *fg_pipe)
 {
-       while (1) {
-               int ch = i_getch(input);
-               if (ch == EOF) {
-                       syntax_error_unterm_ch('`');
-                       /*xfunc_die(); - redundant */
+       int attributes;
+       int status;
+#if ENABLE_HUSH_JOB
+       struct pipe *pi;
+#endif
+       pid_t childpid;
+       int rcode = 0;
+
+       debug_printf_jobs("checkjobs %p\n", fg_pipe);
+
+       attributes = WUNTRACED;
+       if (fg_pipe == NULL)
+               attributes |= WNOHANG;
+
+       errno = 0;
+#if ENABLE_HUSH_FAST
+       if (G.handled_SIGCHLD == G.count_SIGCHLD) {
+//bb_error_msg("[%d] checkjobs: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d children?:%d fg_pipe:%p",
+//getpid(), G.count_SIGCHLD, G.handled_SIGCHLD, G.we_have_children, fg_pipe);
+               /* There was neither fork nor SIGCHLD since last waitpid */
+               /* Avoid doing waitpid syscall if possible */
+               if (!G.we_have_children) {
+                       errno = ECHILD;
+                       return -1;
                }
-               if (ch == '`')
-                       return;
-               if (ch == '\\') {
-                       /* \x. Copy both chars unless it is \` */
-                       int ch2 = i_getch(input);
-                       if (ch2 == EOF) {
-                               syntax_error_unterm_ch('`');
-                               /*xfunc_die(); - redundant */
-                       }
-                       if (ch2 != '`' && ch2 != '$' && ch2 != '\\')
-                               o_addchr(dest, ch);
-                       ch = ch2;
+               if (fg_pipe == NULL) { /* is WNOHANG set? */
+                       /* We have children, but they did not exit
+                        * or stop yet (we saw no SIGCHLD) */
+                       return 0;
                }
-               o_addchr(dest, ch);
+               /* else: !WNOHANG, waitpid will block, can't short-circuit */
        }
-}
-/* Process $(cmd) - copy contents until ")" is seen. Complicated by
- * quoting and nested ()s.
- * "With the $(command) style of command substitution, all characters
- * following the open parenthesis to the matching closing parenthesis
- * constitute the command. Any valid shell script can be used for command,
- * except a script consisting solely of redirections which produces
- * unspecified results."
- * Example                              Output
- * echo $(echo '(TEST)' BEST)           (TEST) BEST
- * echo $(echo 'TEST)' BEST)            TEST) BEST
- * echo $(echo \(\(TEST\) BEST)         ((TEST) BEST
- *
- * Also adapted to eat ${var%...} and $((...)) constructs, since ... part
- * can contain arbitrary constructs, just like $(cmd).
- * In bash compat mode, it needs to also be able to stop on '}' or ':'
- * for ${var:N[:M]} parsing.
- */
-#define DOUBLE_CLOSE_CHAR_FLAG 0x80
-static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsigned end_ch)
-{
-       int ch;
-       char dbl = end_ch & DOUBLE_CLOSE_CHAR_FLAG;
-# if ENABLE_HUSH_BASH_COMPAT
-       char end_char2 = end_ch >> 8;
-# endif
-       end_ch &= (DOUBLE_CLOSE_CHAR_FLAG - 1);
+#endif
 
+/* Do we do this right?
+ * bash-3.00# sleep 20 | false
+ * <ctrl-Z pressed>
+ * [3]+  Stopped          sleep 20 | false
+ * bash-3.00# echo $?
+ * 1   <========== bg pipe is not fully done, but exitcode is already known!
+ * [hush 1.14.0: yes we do it right]
+ */
+ wait_more:
        while (1) {
-               ch = i_getch(input);
-               if (ch == EOF) {
-                       syntax_error_unterm_ch(end_ch);
-                       /*xfunc_die(); - redundant */
+               int i;
+               int dead;
+
+#if ENABLE_HUSH_FAST
+               i = G.count_SIGCHLD;
+#endif
+               childpid = waitpid(-1, &status, attributes);
+               if (childpid <= 0) {
+                       if (childpid && errno != ECHILD)
+                               bb_perror_msg("waitpid");
+#if ENABLE_HUSH_FAST
+                       else { /* Until next SIGCHLD, waitpid's are useless */
+                               G.we_have_children = (childpid == 0);
+                               G.handled_SIGCHLD = i;
+//bb_error_msg("[%d] checkjobs: waitpid returned <= 0, G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+                       }
+#endif
+                       break;
                }
-               if (ch == end_ch  IF_HUSH_BASH_COMPAT( || ch == end_char2)) {
-                       if (!dbl)
-                               break;
-                       /* we look for closing )) of $((EXPR)) */
-                       if (i_peek(input) == end_ch) {
-                               i_getch(input); /* eat second ')' */
-                               break;
+               dead = WIFEXITED(status) || WIFSIGNALED(status);
+
+#if DEBUG_JOBS
+               if (WIFSTOPPED(status))
+                       debug_printf_jobs("pid %d stopped by sig %d (exitcode %d)\n",
+                                       childpid, WSTOPSIG(status), WEXITSTATUS(status));
+               if (WIFSIGNALED(status))
+                       debug_printf_jobs("pid %d killed by sig %d (exitcode %d)\n",
+                                       childpid, WTERMSIG(status), WEXITSTATUS(status));
+               if (WIFEXITED(status))
+                       debug_printf_jobs("pid %d exited, exitcode %d\n",
+                                       childpid, WEXITSTATUS(status));
+#endif
+               /* Were we asked to wait for fg pipe? */
+               if (fg_pipe) {
+                       for (i = 0; i < fg_pipe->num_cmds; i++) {
+                               debug_printf_jobs("check pid %d\n", fg_pipe->cmds[i].pid);
+                               if (fg_pipe->cmds[i].pid != childpid)
+                                       continue;
+                               if (dead) {
+                                       fg_pipe->cmds[i].pid = 0;
+                                       fg_pipe->alive_cmds--;
+                                       if (i == fg_pipe->num_cmds - 1) {
+                                               /* last process gives overall exitstatus */
+                                               rcode = WEXITSTATUS(status);
+                                               /* bash prints killer signal's name for *last*
+                                                * process in pipe (prints just newline for SIGINT).
+                                                * Mimic this. Example: "sleep 5" + (^\ or kill -QUIT)
+                                                */
+                                               if (WIFSIGNALED(status)) {
+                                                       int sig = WTERMSIG(status);
+                                                       printf("%s\n", sig == SIGINT ? "" : get_signame(sig));
+                                                       /* TODO: MIPS has 128 sigs (1..128), what if sig==128 here?
+                                                        * Maybe we need to use sig | 128? */
+                                                       rcode = sig + 128;
+                                               }
+                                               IF_HAS_KEYWORDS(if (fg_pipe->pi_inverted) rcode = !rcode;)
+                                       }
+                               } else {
+                                       fg_pipe->cmds[i].is_stopped = 1;
+                                       fg_pipe->stopped_cmds++;
+                               }
+                               debug_printf_jobs("fg_pipe: alive_cmds %d stopped_cmds %d\n",
+                                               fg_pipe->alive_cmds, fg_pipe->stopped_cmds);
+                               if (fg_pipe->alive_cmds - fg_pipe->stopped_cmds <= 0) {
+                                       /* All processes in fg pipe have exited or stopped */
+/* Note: *non-interactive* bash does not continue if all processes in fg pipe
+ * are stopped. Testcase: "cat | cat" in a script (not on command line!)
+ * and "killall -STOP cat" */
+                                       if (G_interactive_fd) {
+#if ENABLE_HUSH_JOB
+                                               if (fg_pipe->alive_cmds)
+                                                       insert_bg_job(fg_pipe);
+#endif
+                                               return rcode;
+                                       }
+                                       if (!fg_pipe->alive_cmds)
+                                               return rcode;
+                               }
+                               /* There are still running processes in the fg pipe */
+                               goto wait_more; /* do waitpid again */
                        }
+                       /* it wasnt fg_pipe, look for process in bg pipes */
                }
-               o_addchr(dest, ch);
-               if (ch == '(' || ch == '{') {
-                       ch = (ch == '(' ? ')' : '}');
-                       add_till_closing_bracket(dest, input, ch);
-                       o_addchr(dest, ch);
-                       continue;
-               }
-               if (ch == '\'') {
-                       add_till_single_quote(dest, input);
-                       o_addchr(dest, ch);
-                       continue;
-               }
-               if (ch == '"') {
-                       add_till_double_quote(dest, input);
-                       o_addchr(dest, ch);
-                       continue;
-               }
-               if (ch == '`') {
-                       add_till_backquote(dest, input);
-                       o_addchr(dest, ch);
-                       continue;
+
+#if ENABLE_HUSH_JOB
+               /* We asked to wait for bg or orphaned children */
+               /* No need to remember exitcode in this case */
+               for (pi = G.job_list; pi; pi = pi->next) {
+                       for (i = 0; i < pi->num_cmds; i++) {
+                               if (pi->cmds[i].pid == childpid)
+                                       goto found_pi_and_prognum;
+                       }
                }
-               if (ch == '\\') {
-                       /* \x. Copy verbatim. Important for  \(, \) */
-                       ch = i_getch(input);
-                       if (ch == EOF) {
-                               syntax_error_unterm_ch(')');
-                               /*xfunc_die(); - redundant */
+               /* Happens when shell is used as init process (init=/bin/sh) */
+               debug_printf("checkjobs: pid %d was not in our list!\n", childpid);
+               continue; /* do waitpid again */
+
+ found_pi_and_prognum:
+               if (dead) {
+                       /* child exited */
+                       pi->cmds[i].pid = 0;
+                       pi->alive_cmds--;
+                       if (!pi->alive_cmds) {
+                               if (G_interactive_fd)
+                                       printf(JOB_STATUS_FORMAT, pi->jobid,
+                                                       "Done", pi->cmdtext);
+                               delete_finished_bg_job(pi);
                        }
-                       o_addchr(dest, ch);
-                       continue;
+               } else {
+                       /* child stopped */
+                       pi->cmds[i].is_stopped = 1;
+                       pi->stopped_cmds++;
                }
+#endif
+       } /* while (waitpid succeeds)... */
+
+       return rcode;
+}
+
+#if ENABLE_HUSH_JOB
+static int checkjobs_and_fg_shell(struct pipe *fg_pipe)
+{
+       pid_t p;
+       int rcode = checkjobs(fg_pipe);
+       if (G_saved_tty_pgrp) {
+               /* Job finished, move the shell to the foreground */
+               p = getpgrp(); /* our process group id */
+               debug_printf_jobs("fg'ing ourself: getpgrp()=%d\n", (int)p);
+               tcsetpgrp(G_interactive_fd, p);
        }
-       return ch;
+       return rcode;
 }
-#endif /* ENABLE_HUSH_TICK || ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_DOLLAR_OPS */
+#endif
 
-/* Return code: 0 for OK, 1 for syntax error */
-#if BB_MMU
-#define parse_dollar(as_string, dest, input) \
-       parse_dollar(dest, input)
-#define as_string NULL
+/* Start all the jobs, but don't wait for anything to finish.
+ * See checkjobs().
+ *
+ * Return code is normally -1, when the caller has to wait for children
+ * to finish to determine the exit status of the pipe.  If the pipe
+ * is a simple builtin command, however, the action is done by the
+ * time run_pipe returns, and the exit code is provided as the
+ * return value.
+ *
+ * Returns -1 only if started some children. IOW: we have to
+ * mask out retvals of builtins etc with 0xff!
+ *
+ * The only case when we do not need to [v]fork is when the pipe
+ * is single, non-backgrounded, non-subshell command. Examples:
+ * cmd ; ...   { list } ; ...
+ * cmd && ...  { list } && ...
+ * cmd || ...  { list } || ...
+ * If it is, then we can run cmd as a builtin, NOFORK [do we do this?],
+ * or (if SH_STANDALONE) an applet, and we can run the { list }
+ * with run_list. If it isn't one of these, we fork and exec cmd.
+ *
+ * Cases when we must fork:
+ * non-single:   cmd | cmd
+ * backgrounded: cmd &     { list } &
+ * subshell:     ( list ) [&]
+ */
+#if !ENABLE_HUSH_MODE_X
+#define redirect_and_varexp_helper(new_env_p, old_vars_p, command, squirrel, char argv_expanded) \
+       redirect_and_varexp_helper(new_env_p, old_vars_p, command, squirrel)
 #endif
-static int parse_dollar(o_string *as_string,
-               o_string *dest,
-               struct in_str *input)
+static int redirect_and_varexp_helper(char ***new_env_p,
+               struct variable **old_vars_p,
+               struct command *command,
+               int squirrel[3],
+               char **argv_expanded)
 {
-       int ch = i_peek(input);  /* first character after the $ */
-       unsigned char quote_mask = dest->o_escape ? 0x80 : 0;
+       /* setup_redirects acts on file descriptors, not FILEs.
+        * This is perfect for work that comes after exec().
+        * Is it really safe for inline use?  Experimentally,
+        * things seem to work. */
+       int rcode = setup_redirects(command, squirrel);
+       if (rcode == 0) {
+               char **new_env = expand_assignments(command->argv, command->assignment_cnt);
+               *new_env_p = new_env;
+               dump_cmd_in_x_mode(new_env);
+               dump_cmd_in_x_mode(argv_expanded);
+               if (old_vars_p)
+                       *old_vars_p = set_vars_and_save_old(new_env);
+       }
+       return rcode;
+}
+static NOINLINE int run_pipe(struct pipe *pi)
+{
+       static const char *const null_ptr = NULL;
 
-       debug_printf_parse("parse_dollar entered: ch='%c'\n", ch);
-       if (isalpha(ch)) {
-               ch = i_getch(input);
-               nommu_addchr(as_string, ch);
- make_var:
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-               while (1) {
-                       debug_printf_parse(": '%c'\n", ch);
-                       o_addchr(dest, ch | quote_mask);
-                       quote_mask = 0;
-                       ch = i_peek(input);
-                       if (!isalnum(ch) && ch != '_')
-                               break;
-                       ch = i_getch(input);
-                       nommu_addchr(as_string, ch);
-               }
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-       } else if (isdigit(ch)) {
- make_one_char_var:
-               ch = i_getch(input);
-               nommu_addchr(as_string, ch);
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-               debug_printf_parse(": '%c'\n", ch);
-               o_addchr(dest, ch | quote_mask);
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-       } else switch (ch) {
-       case '$': /* pid */
-       case '!': /* last bg pid */
-       case '?': /* last exit code */
-       case '#': /* number of args */
-       case '*': /* args */
-       case '@': /* args */
-               goto make_one_char_var;
-       case '{': {
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
+       int cmd_no;
+       int next_infd;
+       struct command *command;
+       char **argv_expanded;
+       char **argv;
+       /* it is not always needed, but we aim to smaller code */
+       int squirrel[] = { -1, -1, -1 };
+       int rcode;
 
-               ch = i_getch(input); /* eat '{' */
-               nommu_addchr(as_string, ch);
+       debug_printf_exec("run_pipe start: members:%d\n", pi->num_cmds);
+       debug_enter();
 
-               ch = i_getch(input); /* first char after '{' */
-               nommu_addchr(as_string, ch);
-               /* It should be ${?}, or ${#var},
-                * or even ${?+subst} - operator acting on a special variable,
-                * or the beginning of variable name.
-                */
-               if (!strchr(_SPECIAL_VARS_STR, ch) && !isalnum(ch)) { /* not one of those */
- bad_dollar_syntax:
-                       syntax_error_unterm_str("${name}");
-                       debug_printf_parse("parse_dollar return 1: unterminated ${name}\n");
-                       return 1;
+       /* Testcase: set -- q w e; (IFS='' echo "$*"; IFS=''; echo "$*"); echo "$*"
+        * Result should be 3 lines: q w e, qwe, q w e
+        */
+       G.ifs = get_local_var_value("IFS");
+       if (!G.ifs)
+               G.ifs = defifs;
+
+       IF_HUSH_JOB(pi->pgrp = -1;)
+       pi->stopped_cmds = 0;
+       command = &pi->cmds[0];
+       argv_expanded = NULL;
+
+       if (pi->num_cmds != 1
+        || pi->followup == PIPE_BG
+        || command->cmd_type == CMD_SUBSHELL
+       ) {
+               goto must_fork;
+       }
+
+       pi->alive_cmds = 1;
+
+       debug_printf_exec(": group:%p argv:'%s'\n",
+               command->group, command->argv ? command->argv[0] : "NONE");
+
+       if (command->group) {
+#if ENABLE_HUSH_FUNCTIONS
+               if (command->cmd_type == CMD_FUNCDEF) {
+                       /* "executing" func () { list } */
+                       struct function *funcp;
+
+                       funcp = new_function(command->argv[0]);
+                       /* funcp->name is already set to argv[0] */
+                       funcp->body = command->group;
+# if !BB_MMU
+                       funcp->body_as_string = command->group_as_string;
+                       command->group_as_string = NULL;
+# endif
+                       command->group = NULL;
+                       command->argv[0] = NULL;
+                       debug_printf_exec("cmd %p has child func at %p\n", command, funcp);
+                       funcp->parent_cmd = command;
+                       command->child_func = funcp;
+
+                       debug_printf_exec("run_pipe: return EXIT_SUCCESS\n");
+                       debug_leave();
+                       return EXIT_SUCCESS;
                }
-               ch |= quote_mask;
+#endif
+               /* { list } */
+               debug_printf("non-subshell group\n");
+               rcode = 1; /* exitcode if redir failed */
+               if (setup_redirects(command, squirrel) == 0) {
+                       debug_printf_exec(": run_list\n");
+                       rcode = run_list(command->group) & 0xff;
+               }
+               restore_redirects(squirrel);
+               IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
+               debug_leave();
+               debug_printf_exec("run_pipe: return %d\n", rcode);
+               return rcode;
+       }
+
+       argv = command->argv ? command->argv : (char **) &null_ptr;
+       {
+               const struct built_in_command *x;
+#if ENABLE_HUSH_FUNCTIONS
+               const struct function *funcp;
+#else
+               enum { funcp = 0 };
+#endif
+               char **new_env = NULL;
+               struct variable *old_vars = NULL;
 
-               /* It's possible to just call add_till_closing_bracket() at this point.
-                * However, this regresses some of our testsuite cases
-                * which check invalid constructs like ${%}.
-                * Oh well... let's check that the var name part is fine... */
+               if (argv[command->assignment_cnt] == NULL) {
+                       /* Assignments, but no command */
+                       /* Ensure redirects take effect (that is, create files).
+                        * Try "a=t >file" */
+#if 0 /* A few cases in testsuite fail with this code. FIXME */
+                       rcode = redirect_and_varexp_helper(&new_env, /*old_vars:*/ NULL, command, squirrel, /*argv_expanded:*/ NULL);
+                       /* Set shell variables */
+                       if (new_env) {
+                               argv = new_env;
+                               while (*argv) {
+                                       set_local_var(*argv, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+                                       /* Do we need to flag set_local_var() errors?
+                                        * "assignment to readonly var" and "putenv error"
+                                        */
+                                       argv++;
+                               }
+                       }
+                       /* Redirect error sets $? to 1. Otherwise,
+                        * if evaluating assignment value set $?, retain it.
+                        * Try "false; q=`exit 2`; echo $?" - should print 2: */
+                       if (rcode == 0)
+                               rcode = G.last_exitcode;
+                       /* Exit, _skipping_ variable restoring code: */
+                       goto clean_up_and_ret0;
 
-               while (1) {
-                       unsigned pos;
+#else /* Older, bigger, but more correct code */
 
-                       o_addchr(dest, ch);
-                       debug_printf_parse(": '%c'\n", ch);
+                       rcode = setup_redirects(command, squirrel);
+                       restore_redirects(squirrel);
+                       /* Set shell variables */
+                       if (G_x_mode)
+                               bb_putchar_stderr('+');
+                       while (*argv) {
+                               char *p = expand_string_to_string(*argv);
+                               if (G_x_mode)
+                                       fprintf(stderr, " %s", p);
+                               debug_printf_exec("set shell var:'%s'->'%s'\n",
+                                               *argv, p);
+                               set_local_var(p, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+                               /* Do we need to flag set_local_var() errors?
+                                * "assignment to readonly var" and "putenv error"
+                                */
+                               argv++;
+                       }
+                       if (G_x_mode)
+                               bb_putchar_stderr('\n');
+                       /* Redirect error sets $? to 1. Otherwise,
+                        * if evaluating assignment value set $?, retain it.
+                        * Try "false; q=`exit 2`; echo $?" - should print 2: */
+                       if (rcode == 0)
+                               rcode = G.last_exitcode;
+                       IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
+                       debug_leave();
+                       debug_printf_exec("run_pipe: return %d\n", rcode);
+                       return rcode;
+#endif
+               }
 
-                       ch = i_getch(input);
-                       nommu_addchr(as_string, ch);
-                       if (ch == '}')
-                               break;
+               /* Expand the rest into (possibly) many strings each */
+               if (0) {}
+#if ENABLE_HUSH_BASH_COMPAT
+               else if (command->cmd_type == CMD_SINGLEWORD_NOGLOB) {
+                       argv_expanded = expand_strvec_to_strvec_singleword_noglob(argv + command->assignment_cnt);
+               }
+#endif
+               else {
+                       argv_expanded = expand_strvec_to_strvec(argv + command->assignment_cnt);
+               }
 
-                       if (!isalnum(ch) && ch != '_') {
-                               unsigned end_ch;
-                               unsigned char last_ch;
-                               /* handle parameter expansions
-                                * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02
-                                */
-                               if (!strchr("%#:-=+?", ch)) /* ${var<bad_char>... */
-                                       goto bad_dollar_syntax;
-                               o_addchr(dest, ch);
+               /* if someone gives us an empty string: `cmd with empty output` */
+               if (!argv_expanded[0]) {
+                       free(argv_expanded);
+                       debug_leave();
+                       return G.last_exitcode;
+               }
 
-                               /* Eat everything until closing '}' (or ':') */
-                               end_ch = '}';
-                               if (ENABLE_HUSH_BASH_COMPAT
-                                && ch == ':'
-                                && !strchr("%#:-=+?"+3, i_peek(input))
-                               ) {
-                                       /* It's ${var:N[:M]} thing */
-                                       end_ch = '}' * 0x100 + ':';
-                               }
- again:
-                               if (!BB_MMU)
-                                       pos = dest->length;
-#if ENABLE_HUSH_DOLLAR_OPS
-                               last_ch = add_till_closing_bracket(dest, input, end_ch);
-#else
-#error Simple code to only allow ${var} is not implemented
+               x = find_builtin(argv_expanded[0]);
+#if ENABLE_HUSH_FUNCTIONS
+               funcp = NULL;
+               if (!x)
+                       funcp = find_function(argv_expanded[0]);
 #endif
-                               if (as_string) {
-                                       o_addstr(as_string, dest->data + pos);
-                                       o_addchr(as_string, last_ch);
+               if (x || funcp) {
+                       if (!funcp) {
+                               if (x->b_function == builtin_exec && argv_expanded[1] == NULL) {
+                                       debug_printf("exec with redirects only\n");
+                                       rcode = setup_redirects(command, NULL);
+                                       goto clean_up_and_ret1;
                                }
-
-                               if (ENABLE_HUSH_BASH_COMPAT && (end_ch & 0xff00)) {
-                                       /* close the first block: */
-                                       o_addchr(dest, SPECIAL_VAR_SYMBOL);
-                                       /* while parsing N from ${var:N[:M]}... */
-                                       if ((end_ch & 0xff) == last_ch) {
-                                               /* ...got ':' - parse the rest */
-                                               end_ch = '}';
-                                               goto again;
-                                       }
-                                       /* ...got '}', not ':' - it's ${var:N}! emulate :999999999 */
-                                       o_addstr(dest, "999999999");
+                       }
+                       rcode = redirect_and_varexp_helper(&new_env, &old_vars, command, squirrel, argv_expanded);
+                       if (rcode == 0) {
+                               if (!funcp) {
+                                       debug_printf_exec(": builtin '%s' '%s'...\n",
+                                               x->b_cmd, argv_expanded[1]);
+                                       rcode = x->b_function(argv_expanded) & 0xff;
+                                       fflush_all();
                                }
-                               break;
+#if ENABLE_HUSH_FUNCTIONS
+                               else {
+# if ENABLE_HUSH_LOCAL
+                                       struct variable **sv;
+                                       sv = G.shadowed_vars_pp;
+                                       G.shadowed_vars_pp = &old_vars;
+# endif
+                                       debug_printf_exec(": function '%s' '%s'...\n",
+                                               funcp->name, argv_expanded[1]);
+                                       rcode = run_function(funcp, argv_expanded) & 0xff;
+# if ENABLE_HUSH_LOCAL
+                                       G.shadowed_vars_pp = sv;
+# endif
+                               }
+#endif
                        }
+ clean_up_and_ret:
+                       unset_vars(new_env);
+                       add_vars(old_vars);
+/* clean_up_and_ret0: */
+                       restore_redirects(squirrel);
+ clean_up_and_ret1:
+                       free(argv_expanded);
+                       IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
+                       debug_leave();
+                       debug_printf_exec("run_pipe return %d\n", rcode);
+                       return rcode;
                }
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-               break;
-       }
-#if ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_TICK
-       case '(': {
-               unsigned pos;
 
-               ch = i_getch(input);
-               nommu_addchr(as_string, ch);
-# if ENABLE_SH_MATH_SUPPORT
-               if (i_peek(input) == '(') {
-                       ch = i_getch(input);
-                       nommu_addchr(as_string, ch);
-                       o_addchr(dest, SPECIAL_VAR_SYMBOL);
-                       o_addchr(dest, /*quote_mask |*/ '+');
-                       if (!BB_MMU)
-                               pos = dest->length;
-                       add_till_closing_bracket(dest, input, ')' | DOUBLE_CLOSE_CHAR_FLAG);
-                       if (as_string) {
-                               o_addstr(as_string, dest->data + pos);
-                               o_addchr(as_string, ')');
-                               o_addchr(as_string, ')');
+               if (ENABLE_FEATURE_SH_STANDALONE) {
+                       int n = find_applet_by_name(argv_expanded[0]);
+                       if (n >= 0 && APPLET_IS_NOFORK(n)) {
+                               rcode = redirect_and_varexp_helper(&new_env, &old_vars, command, squirrel, argv_expanded);
+                               if (rcode == 0) {
+                                       debug_printf_exec(": run_nofork_applet '%s' '%s'...\n",
+                                               argv_expanded[0], argv_expanded[1]);
+                                       rcode = run_nofork_applet(n, argv_expanded);
+                               }
+                               goto clean_up_and_ret;
                        }
-                       o_addchr(dest, SPECIAL_VAR_SYMBOL);
-                       break;
                }
-# endif
-# if ENABLE_HUSH_TICK
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-               o_addchr(dest, quote_mask | '`');
-               if (!BB_MMU)
-                       pos = dest->length;
-               add_till_closing_bracket(dest, input, ')');
-               if (as_string) {
-                       o_addstr(as_string, dest->data + pos);
-                       o_addchr(as_string, ')');
-               }
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-# endif
-               break;
+               /* It is neither builtin nor applet. We must fork. */
        }
+
+ must_fork:
+       /* NB: argv_expanded may already be created, and that
+        * might include `cmd` runs! Do not rerun it! We *must*
+        * use argv_expanded if it's non-NULL */
+
+       /* Going to fork a child per each pipe member */
+       pi->alive_cmds = 0;
+       next_infd = 0;
+
+       cmd_no = 0;
+       while (cmd_no < pi->num_cmds) {
+               struct fd_pair pipefds;
+#if !BB_MMU
+               volatile nommu_save_t nommu_save;
+               nommu_save.new_env = NULL;
+               nommu_save.old_vars = NULL;
+               nommu_save.argv = NULL;
+               nommu_save.argv_from_re_execing = NULL;
 #endif
-       case '_':
-               ch = i_getch(input);
-               nommu_addchr(as_string, ch);
-               ch = i_peek(input);
-               if (isalnum(ch)) { /* it's $_name or $_123 */
-                       ch = '_';
-                       goto make_var;
+               command = &pi->cmds[cmd_no];
+               cmd_no++;
+               if (command->argv) {
+                       debug_printf_exec(": pipe member '%s' '%s'...\n",
+                                       command->argv[0], command->argv[1]);
+               } else {
+                       debug_printf_exec(": pipe member with no argv\n");
                }
-               /* else: it's $_ */
-       /* TODO: $_ and $-: */
-       /* $_ Shell or shell script name; or last argument of last command
-        * (if last command wasn't a pipe; if it was, bash sets $_ to "");
-        * but in command's env, set to full pathname used to invoke it */
-       /* $- Option flags set by set builtin or shell options (-i etc) */
-       default:
-               o_addQchr(dest, '$');
-       }
-       debug_printf_parse("parse_dollar return 0\n");
-       return 0;
-#undef as_string
-}
 
-#if BB_MMU
-#define parse_stream_dquoted(as_string, dest, input, dquote_end) \
-       parse_stream_dquoted(dest, input, dquote_end)
-#define as_string NULL
+               /* pipes are inserted between pairs of commands */
+               pipefds.rd = 0;
+               pipefds.wr = 1;
+               if (cmd_no < pi->num_cmds)
+                       xpiped_pair(pipefds);
+
+               command->pid = BB_MMU ? fork() : vfork();
+               if (!command->pid) { /* child */
+#if ENABLE_HUSH_JOB
+                       disable_restore_tty_pgrp_on_exit();
+                       CLEAR_RANDOM_T(&G.random_gen); /* or else $RANDOM repeats in child */
+
+                       /* Every child adds itself to new process group
+                        * with pgid == pid_of_first_child_in_pipe */
+                       if (G.run_list_level == 1 && G_interactive_fd) {
+                               pid_t pgrp;
+                               pgrp = pi->pgrp;
+                               if (pgrp < 0) /* true for 1st process only */
+                                       pgrp = getpid();
+                               if (setpgid(0, pgrp) == 0
+                                && pi->followup != PIPE_BG
+                                && G_saved_tty_pgrp /* we have ctty */
+                               ) {
+                                       /* We do it in *every* child, not just first,
+                                        * to avoid races */
+                                       tcsetpgrp(G_interactive_fd, pgrp);
+                               }
+                       }
 #endif
-static int parse_stream_dquoted(o_string *as_string,
-               o_string *dest,
-               struct in_str *input,
-               int dquote_end)
-{
-       int ch;
-       int next;
+                       if (pi->alive_cmds == 0 && pi->followup == PIPE_BG) {
+                               /* 1st cmd in backgrounded pipe
+                                * should have its stdin /dev/null'ed */
+                               close(0);
+                               if (open(bb_dev_null, O_RDONLY))
+                                       xopen("/", O_RDONLY);
+                       } else {
+                               xmove_fd(next_infd, 0);
+                       }
+                       xmove_fd(pipefds.wr, 1);
+                       if (pipefds.rd > 1)
+                               close(pipefds.rd);
+                       /* Like bash, explicit redirects override pipes,
+                        * and the pipe fd is available for dup'ing. */
+                       if (setup_redirects(command, NULL))
+                               _exit(1);
 
- again:
-       ch = i_getch(input);
-       if (ch != EOF)
-               nommu_addchr(as_string, ch);
-       if (ch == dquote_end) { /* may be only '"' or EOF */
-               if (dest->o_assignment == NOT_ASSIGNMENT)
-                       dest->o_escape ^= 1;
-               debug_printf_parse("parse_stream_dquoted return 0\n");
-               return 0;
-       }
-       /* note: can't move it above ch == dquote_end check! */
-       if (ch == EOF) {
-               syntax_error_unterm_ch('"');
-               /*xfunc_die(); - redundant */
-       }
-       next = '\0';
-       if (ch != '\n') {
-               next = i_peek(input);
-       }
-       debug_printf_parse("\" ch=%c (%d) escape=%d\n",
-                                       ch, ch, dest->o_escape);
-       if (ch == '\\') {
-               if (next == EOF) {
-                       syntax_error("\\<eof>");
-                       xfunc_die();
+                       /* Restore default handlers just prior to exec */
+                       /*signal(SIGCHLD, SIG_DFL); - so far we don't have any handlers */
+
+                       /* Stores to nommu_save list of env vars putenv'ed
+                        * (NOMMU, on MMU we don't need that) */
+                       /* cast away volatility... */
+                       pseudo_exec((nommu_save_t*) &nommu_save, command, argv_expanded);
+                       /* pseudo_exec() does not return */
                }
-               /* bash:
-                * "The backslash retains its special meaning [in "..."]
-                * only when followed by one of the following characters:
-                * $, `, ", \, or <newline>.  A double quote may be quoted
-                * within double quotes by preceding it with a backslash."
-                */
-               if (strchr("$`\"\\\n", next) != NULL) {
-                       ch = i_getch(input);
-                       if (ch != '\n') {
-                               o_addqchr(dest, ch);
-                               nommu_addchr(as_string, ch);
-                       }
+
+               /* parent or error */
+#if ENABLE_HUSH_FAST
+               G.count_SIGCHLD++;
+//bb_error_msg("[%d] fork in run_pipe: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+#endif
+               enable_restore_tty_pgrp_on_exit();
+#if !BB_MMU
+               /* Clean up after vforked child */
+               free(nommu_save.argv);
+               free(nommu_save.argv_from_re_execing);
+               unset_vars(nommu_save.new_env);
+               add_vars(nommu_save.old_vars);
+#endif
+               free(argv_expanded);
+               argv_expanded = NULL;
+               if (command->pid < 0) { /* [v]fork failed */
+                       /* Clearly indicate, was it fork or vfork */
+                       bb_perror_msg(BB_MMU ? "vfork"+1 : "vfork");
                } else {
-                       o_addqchr(dest, '\\');
-                       nommu_addchr(as_string, '\\');
-               }
-               goto again;
-       }
-       if (ch == '$') {
-               if (parse_dollar(as_string, dest, input) != 0) {
-                       debug_printf_parse("parse_stream_dquoted return 1: "
-                                       "parse_dollar returned non-0\n");
-                       return 1;
+                       pi->alive_cmds++;
+#if ENABLE_HUSH_JOB
+                       /* Second and next children need to know pid of first one */
+                       if (pi->pgrp < 0)
+                               pi->pgrp = command->pid;
+#endif
                }
-               goto again;
-       }
-#if ENABLE_HUSH_TICK
-       if (ch == '`') {
-               //unsigned pos = dest->length;
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-               o_addchr(dest, 0x80 | '`');
-               add_till_backquote(dest, input);
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
-               //debug_printf_subst("SUBST RES3 '%s'\n", dest->data + pos);
-               goto again;
+
+               if (cmd_no > 1)
+                       close(next_infd);
+               if (cmd_no < pi->num_cmds)
+                       close(pipefds.wr);
+               /* Pass read (output) pipe end to next iteration */
+               next_infd = pipefds.rd;
        }
-#endif
-       o_addQchr(dest, ch);
-       if (ch == '='
-        && (dest->o_assignment == MAYBE_ASSIGNMENT
-           || dest->o_assignment == WORD_IS_KEYWORD)
-        && is_well_formed_var_name(dest->data, '=')
-       ) {
-               dest->o_assignment = DEFINITELY_ASSIGNMENT;
+
+       if (!pi->alive_cmds) {
+               debug_leave();
+               debug_printf_exec("run_pipe return 1 (all forks failed, no children)\n");
+               return 1;
        }
-       goto again;
-#undef as_string
+
+       debug_leave();
+       debug_printf_exec("run_pipe return -1 (%u children started)\n", pi->alive_cmds);
+       return -1;
 }
 
-/*
- * Scan input until EOF or end_trigger char.
- * Return a list of pipes to execute, or NULL on EOF
- * or if end_trigger character is met.
- * On syntax error, exit is shell is not interactive,
- * reset parsing machinery and start parsing anew,
- * or return ERR_PTR.
- */
-static struct pipe *parse_stream(char **pstring,
-               struct in_str *input,
-               int end_trigger)
+#ifndef debug_print_tree
+static void debug_print_tree(struct pipe *pi, int lvl)
 {
-       struct parse_context ctx;
-       o_string dest = NULL_O_STRING;
-       int is_in_dquote;
-       int heredoc_cnt;
-
-       /* Double-quote state is handled in the state variable is_in_dquote.
-        * A single-quote triggers a bypass of the main loop until its mate is
-        * found.  When recursing, quote state is passed in via dest->o_escape.
-        */
-       debug_printf_parse("parse_stream entered, end_trigger='%c'\n",
-                       end_trigger ? end_trigger : 'X');
-       debug_enter();
-
-       /* If very first arg is "" or '', dest.data may end up NULL.
-        * Preventing this: */
-       o_addchr(&dest, '\0');
-       dest.length = 0;
-
-       G.ifs = get_local_var_value("IFS");
-       if (G.ifs == NULL)
-               G.ifs = defifs;
+       static const char *const PIPE[] = {
+               [PIPE_SEQ] = "SEQ",
+               [PIPE_AND] = "AND",
+               [PIPE_OR ] = "OR" ,
+               [PIPE_BG ] = "BG" ,
+       };
+       static const char *RES[] = {
+               [RES_NONE ] = "NONE" ,
+# if ENABLE_HUSH_IF
+               [RES_IF   ] = "IF"   ,
+               [RES_THEN ] = "THEN" ,
+               [RES_ELIF ] = "ELIF" ,
+               [RES_ELSE ] = "ELSE" ,
+               [RES_FI   ] = "FI"   ,
+# endif
+# if ENABLE_HUSH_LOOPS
+               [RES_FOR  ] = "FOR"  ,
+               [RES_WHILE] = "WHILE",
+               [RES_UNTIL] = "UNTIL",
+               [RES_DO   ] = "DO"   ,
+               [RES_DONE ] = "DONE" ,
+# endif
+# if ENABLE_HUSH_LOOPS || ENABLE_HUSH_CASE
+               [RES_IN   ] = "IN"   ,
+# endif
+# if ENABLE_HUSH_CASE
+               [RES_CASE ] = "CASE" ,
+               [RES_CASE_IN ] = "CASE_IN" ,
+               [RES_MATCH] = "MATCH",
+               [RES_CASE_BODY] = "CASE_BODY",
+               [RES_ESAC ] = "ESAC" ,
+# endif
+               [RES_XXXX ] = "XXXX" ,
+               [RES_SNTX ] = "SNTX" ,
+       };
+       static const char *const CMDTYPE[] = {
+               "{}",
+               "()",
+               "[noglob]",
+# if ENABLE_HUSH_FUNCTIONS
+               "func()",
+# endif
+       };
 
- reset:
-#if ENABLE_HUSH_INTERACTIVE
-       input->promptmode = 0; /* PS1 */
-#endif
-       /* dest.o_assignment = MAYBE_ASSIGNMENT; - already is */
-       initialize_context(&ctx);
-       is_in_dquote = 0;
-       heredoc_cnt = 0;
-       while (1) {
-               const char *is_ifs;
-               const char *is_special;
-               int ch;
-               int next;
-               int redir_fd;
-               redir_type redir_style;
+       int pin, prn;
 
-               if (is_in_dquote) {
-                       /* dest.o_quoted = 1; - already is (see below) */
-                       if (parse_stream_dquoted(&ctx.as_string, &dest, input, '"')) {
-                               goto parse_error;
-                       }
-                       /* We reached closing '"' */
-                       is_in_dquote = 0;
-               }
-               ch = i_getch(input);
-               debug_printf_parse(": ch=%c (%d) escape=%d\n",
-                                               ch, ch, dest.o_escape);
-               if (ch == EOF) {
-                       struct pipe *pi;
+       pin = 0;
+       while (pi) {
+               fprintf(stderr, "%*spipe %d res_word=%s followup=%d %s\n", lvl*2, "",
+                               pin, RES[pi->res_word], pi->followup, PIPE[pi->followup]);
+               prn = 0;
+               while (prn < pi->num_cmds) {
+                       struct command *command = &pi->cmds[prn];
+                       char **argv = command->argv;
 
-                       if (heredoc_cnt) {
-                               syntax_error_unterm_str("here document");
-                               goto parse_error;
+                       fprintf(stderr, "%*s cmd %d assignment_cnt:%d",
+                                       lvl*2, "", prn,
+                                       command->assignment_cnt);
+                       if (command->group) {
+                               fprintf(stderr, " group %s: (argv=%p)%s%s\n",
+                                               CMDTYPE[command->cmd_type],
+                                               argv
+# if !BB_MMU
+                                               , " group_as_string:", command->group_as_string
+# else
+                                               , "", ""
+# endif
+                               );
+                               debug_print_tree(command->group, lvl+1);
+                               prn++;
+                               continue;
                        }
-                       /* end_trigger == '}' case errors out earlier,
-                        * checking only ')' */
-                       if (end_trigger == ')') {
-                               syntax_error_unterm_ch('('); /* exits */
-                               /* goto parse_error; */
+                       if (argv) while (*argv) {
+                               fprintf(stderr, " '%s'", *argv);
+                               argv++;
                        }
+                       fprintf(stderr, "\n");
+                       prn++;
+               }
+               pi = pi->next;
+               pin++;
+       }
+}
+#endif /* debug_print_tree */
 
-                       if (done_word(&dest, &ctx)) {
-                               goto parse_error;
-                       }
-                       o_free(&dest);
-                       done_pipe(&ctx, PIPE_SEQ);
-                       pi = ctx.list_head;
-                       /* If we got nothing... */
-                       /* (this makes bare "&" cmd a no-op.
-                        * bash says: "syntax error near unexpected token '&'") */
-                       if (pi->num_cmds == 0
-                           IF_HAS_KEYWORDS( && pi->res_word == RES_NONE)
-                       ) {
-                               free_pipe_list(pi);
-                               pi = NULL;
-                       }
-#if !BB_MMU
-                       debug_printf_parse("as_string '%s'\n", ctx.as_string.data);
-                       if (pstring)
-                               *pstring = ctx.as_string.data;
-                       else
-                               o_free_unsafe(&ctx.as_string);
+/* NB: called by pseudo_exec, and therefore must not modify any
+ * global data until exec/_exit (we can be a child after vfork!) */
+static int run_list(struct pipe *pi)
+{
+#if ENABLE_HUSH_CASE
+       char *case_word = NULL;
+#endif
+#if ENABLE_HUSH_LOOPS
+       struct pipe *loop_top = NULL;
+       char **for_lcur = NULL;
+       char **for_list = NULL;
+#endif
+       smallint last_followup;
+       smalluint rcode;
+#if ENABLE_HUSH_IF || ENABLE_HUSH_CASE
+       smalluint cond_code = 0;
+#else
+       enum { cond_code = 0 };
+#endif
+#if HAS_KEYWORDS
+       smallint rword;      /* RES_foo */
+       smallint last_rword; /* ditto */
 #endif
-                       debug_leave();
-                       debug_printf_parse("parse_stream return %p\n", pi);
-                       return pi;
-               }
-               nommu_addchr(&ctx.as_string, ch);
 
-               next = '\0';
-               if (ch != '\n')
-                       next = i_peek(input);
+       debug_printf_exec("run_list start lvl %d\n", G.run_list_level);
+       debug_enter();
 
-               is_special = "{}<>;&|()#'" /* special outside of "str" */
-                               "\\$\"" IF_HUSH_TICK("`"); /* always special */
-               /* Are { and } special here? */
-               if (ctx.command->argv /* word [word]{... - non-special */
-                || dest.length       /* word{... - non-special */
-                || dest.o_quoted     /* ""{... - non-special */
-                || (next != ';'            /* }; - special */
-                   && next != ')'          /* }) - special */
-                   && next != '&'          /* }& and }&& ... - special */
-                   && next != '|'          /* }|| ... - special */
-                   && !strchr(G.ifs, next) /* {word - non-special */
-                   )
-               ) {
-                       /* They are not special, skip "{}" */
-                       is_special += 2;
+#if ENABLE_HUSH_LOOPS
+       /* Check syntax for "for" */
+       for (struct pipe *cpipe = pi; cpipe; cpipe = cpipe->next) {
+               if (cpipe->res_word != RES_FOR && cpipe->res_word != RES_IN)
+                       continue;
+               /* current word is FOR or IN (BOLD in comments below) */
+               if (cpipe->next == NULL) {
+                       syntax_error("malformed for");
+                       debug_leave();
+                       debug_printf_exec("run_list lvl %d return 1\n", G.run_list_level);
+                       return 1;
                }
-               is_special = strchr(is_special, ch);
-               is_ifs = strchr(G.ifs, ch);
-
-               if (!is_special && !is_ifs) { /* ordinary char */
- ordinary_char:
-                       o_addQchr(&dest, ch);
-                       if ((dest.o_assignment == MAYBE_ASSIGNMENT
-                           || dest.o_assignment == WORD_IS_KEYWORD)
-                        && ch == '='
-                        && is_well_formed_var_name(dest.data, '=')
-                       ) {
-                               dest.o_assignment = DEFINITELY_ASSIGNMENT;
-                       }
+               /* "FOR v; do ..." and "for v IN a b; do..." are ok */
+               if (cpipe->next->res_word == RES_DO)
                        continue;
+               /* next word is not "do". It must be "in" then ("FOR v in ...") */
+               if (cpipe->res_word == RES_IN /* "for v IN a b; not_do..."? */
+                || cpipe->next->res_word != RES_IN /* FOR v not_do_and_not_in..."? */
+               ) {
+                       syntax_error("malformed for");
+                       debug_leave();
+                       debug_printf_exec("run_list lvl %d return 1\n", G.run_list_level);
+                       return 1;
                }
-
-               if (is_ifs) {
-                       if (done_word(&dest, &ctx)) {
-                               goto parse_error;
-                       }
-                       if (ch == '\n') {
-#if ENABLE_HUSH_CASE
-                               /* "case ... in <newline> word) ..." -
-                                * newlines are ignored (but ';' wouldn't be) */
-                               if (ctx.command->argv == NULL
-                                && ctx.ctx_res_w == RES_MATCH
-                               ) {
-                                       continue;
-                               }
+       }
 #endif
-                               /* Treat newline as a command separator. */
-                               done_pipe(&ctx, PIPE_SEQ);
-                               debug_printf_parse("heredoc_cnt:%d\n", heredoc_cnt);
-                               if (heredoc_cnt) {
-                                       if (fetch_heredocs(heredoc_cnt, &ctx, input)) {
-                                               goto parse_error;
-                                       }
-                                       heredoc_cnt = 0;
-                               }
-                               dest.o_assignment = MAYBE_ASSIGNMENT;
-                               ch = ';';
-                               /* note: if (is_ifs) continue;
-                                * will still trigger for us */
-                       }
-               }
 
-               /* "cmd}" or "cmd }..." without semicolon or &:
-                * } is an ordinary char in this case, even inside { cmd; }
-                * Pathological example: { ""}; } should exec "}" cmd
-                */
-               if (ch == '}') {
-                       if (!IS_NULL_CMD(ctx.command) /* cmd } */
-                        || dest.length != 0 /* word} */
-                        || dest.o_quoted    /* ""} */
-                       ) {
-                               goto ordinary_char;
-                       }
-                       if (!IS_NULL_PIPE(ctx.pipe)) /* cmd | } */
-                               goto skip_end_trigger;
-                       /* else: } does terminate a group */
-               }
+       /* Past this point, all code paths should jump to ret: label
+        * in order to return, no direct "return" statements please.
+        * This helps to ensure that no memory is leaked. */
 
-               if (end_trigger && end_trigger == ch
-                && (ch != ';' || heredoc_cnt == 0)
-#if ENABLE_HUSH_CASE
-                && (ch != ')'
-                   || ctx.ctx_res_w != RES_MATCH
-                   || (!dest.o_quoted && strcmp(dest.data, "esac") == 0)
-                   )
+#if ENABLE_HUSH_JOB
+       G.run_list_level++;
 #endif
-               ) {
-                       if (heredoc_cnt) {
-                               /* This is technically valid:
-                                * { cat <<HERE; }; echo Ok
-                                * heredoc
-                                * heredoc
-                                * HERE
-                                * but we don't support this.
-                                * We require heredoc to be in enclosing {}/(),
-                                * if any.
-                                */
-                               syntax_error_unterm_str("here document");
-                               goto parse_error;
-                       }
-                       if (done_word(&dest, &ctx)) {
-                               goto parse_error;
-                       }
-                       done_pipe(&ctx, PIPE_SEQ);
-                       dest.o_assignment = MAYBE_ASSIGNMENT;
-                       /* Do we sit outside of any if's, loops or case's? */
-                       if (!HAS_KEYWORDS
-                        IF_HAS_KEYWORDS(|| (ctx.ctx_res_w == RES_NONE && ctx.old_flag == 0))
-                       ) {
-                               o_free(&dest);
-#if !BB_MMU
-                               debug_printf_parse("as_string '%s'\n", ctx.as_string.data);
-                               if (pstring)
-                                       *pstring = ctx.as_string.data;
-                               else
-                                       o_free_unsafe(&ctx.as_string);
+
+#if HAS_KEYWORDS
+       rword = RES_NONE;
+       last_rword = RES_XXXX;
 #endif
-                               debug_leave();
-                               debug_printf_parse("parse_stream return %p: "
-                                               "end_trigger char found\n",
-                                               ctx.list_head);
-                               return ctx.list_head;
-                       }
-               }
- skip_end_trigger:
-               if (is_ifs)
-                       continue;
+       last_followup = PIPE_SEQ;
+       rcode = G.last_exitcode;
 
-               /* Catch <, > before deciding whether this word is
-                * an assignment. a=1 2>z b=2: b=2 is still assignment */
-               switch (ch) {
-               case '>':
-                       redir_fd = redirect_opt_num(&dest);
-                       if (done_word(&dest, &ctx)) {
-                               goto parse_error;
-                       }
-                       redir_style = REDIRECT_OVERWRITE;
-                       if (next == '>') {
-                               redir_style = REDIRECT_APPEND;
-                               ch = i_getch(input);
-                               nommu_addchr(&ctx.as_string, ch);
-                       }
-#if 0
-                       else if (next == '(') {
-                               syntax_error(">(process) not supported");
-                               goto parse_error;
-                       }
+       /* Go through list of pipes, (maybe) executing them. */
+       for (; pi; pi = IF_HUSH_LOOPS(rword == RES_DONE ? loop_top : ) pi->next) {
+               if (G.flag_SIGINT)
+                       break;
+
+               IF_HAS_KEYWORDS(rword = pi->res_word;)
+               debug_printf_exec(": rword=%d cond_code=%d last_rword=%d\n",
+                               rword, cond_code, last_rword);
+#if ENABLE_HUSH_LOOPS
+               if ((rword == RES_WHILE || rword == RES_UNTIL || rword == RES_FOR)
+                && loop_top == NULL /* avoid bumping G.depth_of_loop twice */
+               ) {
+                       /* start of a loop: remember where loop starts */
+                       loop_top = pi;
+                       G.depth_of_loop++;
+               }
 #endif
-                       if (parse_redirect(&ctx, redir_fd, redir_style, input))
-                               goto parse_error;
-                       continue; /* back to top of while (1) */
-               case '<':
-                       redir_fd = redirect_opt_num(&dest);
-                       if (done_word(&dest, &ctx)) {
-                               goto parse_error;
+               /* Still in the same "if...", "then..." or "do..." branch? */
+               if (IF_HAS_KEYWORDS(rword == last_rword &&) 1) {
+                       if ((rcode == 0 && last_followup == PIPE_OR)
+                        || (rcode != 0 && last_followup == PIPE_AND)
+                       ) {
+                               /* It is "<true> || CMD" or "<false> && CMD"
+                                * and we should not execute CMD */
+                               debug_printf_exec("skipped cmd because of || or &&\n");
+                               last_followup = pi->followup;
+                               continue;
                        }
-                       redir_style = REDIRECT_INPUT;
-                       if (next == '<') {
-                               redir_style = REDIRECT_HEREDOC;
-                               heredoc_cnt++;
-                               debug_printf_parse("++heredoc_cnt=%d\n", heredoc_cnt);
-                               ch = i_getch(input);
-                               nommu_addchr(&ctx.as_string, ch);
-                       } else if (next == '>') {
-                               redir_style = REDIRECT_IO;
-                               ch = i_getch(input);
-                               nommu_addchr(&ctx.as_string, ch);
+               }
+               last_followup = pi->followup;
+               IF_HAS_KEYWORDS(last_rword = rword;)
+#if ENABLE_HUSH_IF
+               if (cond_code) {
+                       if (rword == RES_THEN) {
+                               /* if false; then ... fi has exitcode 0! */
+                               G.last_exitcode = rcode = EXIT_SUCCESS;
+                               /* "if <false> THEN cmd": skip cmd */
+                               continue;
                        }
-#if 0
-                       else if (next == '(') {
-                               syntax_error("<(process) not supported");
-                               goto parse_error;
+               } else {
+                       if (rword == RES_ELSE || rword == RES_ELIF) {
+                               /* "if <true> then ... ELSE/ELIF cmd":
+                                * skip cmd and all following ones */
+                               break;
                        }
-#endif
-                       if (parse_redirect(&ctx, redir_fd, redir_style, input))
-                               goto parse_error;
-                       continue; /* back to top of while (1) */
-               }
-
-               if (dest.o_assignment == MAYBE_ASSIGNMENT
-                /* check that we are not in word in "a=1 2>word b=1": */
-                && !ctx.pending_redirect
-               ) {
-                       /* ch is a special char and thus this word
-                        * cannot be an assignment */
-                       dest.o_assignment = NOT_ASSIGNMENT;
                }
+#endif
+#if ENABLE_HUSH_LOOPS
+               if (rword == RES_FOR) { /* && pi->num_cmds - always == 1 */
+                       if (!for_lcur) {
+                               /* first loop through for */
 
-               /* Note: nommu_addchr(&ctx.as_string, ch) is already done */
+                               static const char encoded_dollar_at[] ALIGN1 = {
+                                       SPECIAL_VAR_SYMBOL, '@' | 0x80, SPECIAL_VAR_SYMBOL, '\0'
+                               }; /* encoded representation of "$@" */
+                               static const char *const encoded_dollar_at_argv[] = {
+                                       encoded_dollar_at, NULL
+                               }; /* argv list with one element: "$@" */
+                               char **vals;
 
-               switch (ch) {
-               case '#':
-                       if (dest.length == 0) {
-                               while (1) {
-                                       ch = i_peek(input);
-                                       if (ch == EOF || ch == '\n')
+                               vals = (char**)encoded_dollar_at_argv;
+                               if (pi->next->res_word == RES_IN) {
+                                       /* if no variable values after "in" we skip "for" */
+                                       if (!pi->next->cmds[0].argv) {
+                                               G.last_exitcode = rcode = EXIT_SUCCESS;
+                                               debug_printf_exec(": null FOR: exitcode EXIT_SUCCESS\n");
                                                break;
-                                       i_getch(input);
-                                       /* note: we do not add it to &ctx.as_string */
-                               }
-                               nommu_addchr(&ctx.as_string, '\n');
-                       } else {
-                               o_addQchr(&dest, ch);
-                       }
-                       break;
-               case '\\':
-                       if (next == EOF) {
-                               syntax_error("\\<eof>");
-                               xfunc_die();
-                       }
-                       ch = i_getch(input);
-                       if (ch != '\n') {
-                               o_addchr(&dest, '\\');
-                               /*nommu_addchr(&ctx.as_string, '\\'); - already done */
-                               o_addchr(&dest, ch);
-                               nommu_addchr(&ctx.as_string, ch);
-                               /* Example: echo Hello \2>file
-                                * we need to know that word 2 is quoted */
-                               dest.o_quoted = 1;
+                                       }
+                                       vals = pi->next->cmds[0].argv;
+                               } /* else: "for var; do..." -> assume "$@" list */
+                               /* create list of variable values */
+                               debug_print_strings("for_list made from", vals);
+                               for_list = expand_strvec_to_strvec(vals);
+                               for_lcur = for_list;
+                               debug_print_strings("for_list", for_list);
                        }
-#if !BB_MMU
-                       else {
-                               /* It's "\<newline>". Remove trailing '\' from ctx.as_string */
-                               ctx.as_string.data[--ctx.as_string.length] = '\0';
+                       if (!*for_lcur) {
+                               /* "for" loop is over, clean up */
+                               free(for_list);
+                               for_list = NULL;
+                               for_lcur = NULL;
+                               break;
                        }
+                       /* Insert next value from for_lcur */
+                       /* note: *for_lcur already has quotes removed, $var expanded, etc */
+                       set_local_var(xasprintf("%s=%s", pi->cmds[0].argv[0], *for_lcur++), /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+                       continue;
+               }
+               if (rword == RES_IN) {
+                       continue; /* "for v IN list;..." - "in" has no cmds anyway */
+               }
+               if (rword == RES_DONE) {
+                       continue; /* "done" has no cmds too */
+               }
 #endif
-                       break;
-               case '$':
-                       if (parse_dollar(&ctx.as_string, &dest, input) != 0) {
-                               debug_printf_parse("parse_stream parse error: "
-                                       "parse_dollar returned non-0\n");
-                               goto parse_error;
-                       }
-                       break;
-               case '\'':
-                       dest.o_quoted = 1;
-                       while (1) {
-                               ch = i_getch(input);
-                               if (ch == EOF) {
-                                       syntax_error_unterm_ch('\'');
-                                       /*xfunc_die(); - redundant */
-                               }
-                               nommu_addchr(&ctx.as_string, ch);
-                               if (ch == '\'')
+#if ENABLE_HUSH_CASE
+               if (rword == RES_CASE) {
+                       case_word = expand_strvec_to_string(pi->cmds->argv);
+                       continue;
+               }
+               if (rword == RES_MATCH) {
+                       char **argv;
+
+                       if (!case_word) /* "case ... matched_word) ... WORD)": we executed selected branch, stop */
+                               break;
+                       /* all prev words didn't match, does this one match? */
+                       argv = pi->cmds->argv;
+                       while (*argv) {
+                               char *pattern = expand_string_to_string(*argv);
+                               /* TODO: which FNM_xxx flags to use? */
+                               cond_code = (fnmatch(pattern, case_word, /*flags:*/ 0) != 0);
+                               free(pattern);
+                               if (cond_code == 0) { /* match! we will execute this branch */
+                                       free(case_word); /* make future "word)" stop */
+                                       case_word = NULL;
                                        break;
-                               o_addqchr(&dest, ch);
+                               }
+                               argv++;
                        }
-                       break;
-               case '"':
-                       dest.o_quoted = 1;
-                       is_in_dquote ^= 1; /* invert */
-                       if (dest.o_assignment == NOT_ASSIGNMENT)
-                               dest.o_escape ^= 1;
-                       break;
-#if ENABLE_HUSH_TICK
-               case '`': {
-                       unsigned pos;
-
-                       o_addchr(&dest, SPECIAL_VAR_SYMBOL);
-                       o_addchr(&dest, '`');
-                       pos = dest.length;
-                       add_till_backquote(&dest, input);
-# if !BB_MMU
-                       o_addstr(&ctx.as_string, dest.data + pos);
-                       o_addchr(&ctx.as_string, '`');
-# endif
-                       o_addchr(&dest, SPECIAL_VAR_SYMBOL);
-                       //debug_printf_subst("SUBST RES3 '%s'\n", dest.data + pos);
-                       break;
+                       continue;
+               }
+               if (rword == RES_CASE_BODY) { /* inside of a case branch */
+                       if (cond_code != 0)
+                               continue; /* not matched yet, skip this pipe */
                }
 #endif
-               case ';':
-#if ENABLE_HUSH_CASE
- case_semi:
+               /* Just pressing <enter> in shell should check for jobs.
+                * OTOH, in non-interactive shell this is useless
+                * and only leads to extra job checks */
+               if (pi->num_cmds == 0) {
+                       if (G_interactive_fd)
+                               goto check_jobs_and_continue;
+                       continue;
+               }
+
+               /* After analyzing all keywords and conditions, we decided
+                * to execute this pipe. NB: have to do checkjobs(NULL)
+                * after run_pipe to collect any background children,
+                * even if list execution is to be stopped. */
+               debug_printf_exec(": run_pipe with %d members\n", pi->num_cmds);
+               {
+                       int r;
+#if ENABLE_HUSH_LOOPS
+                       G.flag_break_continue = 0;
+#endif
+                       rcode = r = run_pipe(pi); /* NB: rcode is a smallint */
+                       if (r != -1) {
+                               /* We ran a builtin, function, or group.
+                                * rcode is already known
+                                * and we don't need to wait for anything. */
+                               G.last_exitcode = rcode;
+                               debug_printf_exec(": builtin/func exitcode %d\n", rcode);
+                               check_and_run_traps(0);
+#if ENABLE_HUSH_LOOPS
+                               /* Was it "break" or "continue"? */
+                               if (G.flag_break_continue) {
+                                       smallint fbc = G.flag_break_continue;
+                                       /* We might fall into outer *loop*,
+                                        * don't want to break it too */
+                                       if (loop_top) {
+                                               G.depth_break_continue--;
+                                               if (G.depth_break_continue == 0)
+                                                       G.flag_break_continue = 0;
+                                               /* else: e.g. "continue 2" should *break* once, *then* continue */
+                                       } /* else: "while... do... { we are here (innermost list is not a loop!) };...done" */
+                                       if (G.depth_break_continue != 0 || fbc == BC_BREAK)
+                                               goto check_jobs_and_break;
+                                       /* "continue": simulate end of loop */
+                                       rword = RES_DONE;
+                                       continue;
+                               }
 #endif
-                       if (done_word(&dest, &ctx)) {
-                               goto parse_error;
-                       }
-                       done_pipe(&ctx, PIPE_SEQ);
-#if ENABLE_HUSH_CASE
-                       /* Eat multiple semicolons, detect
-                        * whether it means something special */
-                       while (1) {
-                               ch = i_peek(input);
-                               if (ch != ';')
-                                       break;
-                               ch = i_getch(input);
-                               nommu_addchr(&ctx.as_string, ch);
-                               if (ctx.ctx_res_w == RES_CASE_BODY) {
-                                       ctx.ctx_dsemicolon = 1;
-                                       ctx.ctx_res_w = RES_MATCH;
+#if ENABLE_HUSH_FUNCTIONS
+                               if (G.flag_return_in_progress == 1) {
+                                       /* same as "goto check_jobs_and_break" */
+                                       checkjobs(NULL);
                                        break;
                                }
-                       }
 #endif
- new_cmd:
-                       /* We just finished a cmd. New one may start
-                        * with an assignment */
-                       dest.o_assignment = MAYBE_ASSIGNMENT;
-                       break;
-               case '&':
-                       if (done_word(&dest, &ctx)) {
-                               goto parse_error;
-                       }
-                       if (next == '&') {
-                               ch = i_getch(input);
-                               nommu_addchr(&ctx.as_string, ch);
-                               done_pipe(&ctx, PIPE_AND);
-                       } else {
-                               done_pipe(&ctx, PIPE_BG);
-                       }
-                       goto new_cmd;
-               case '|':
-                       if (done_word(&dest, &ctx)) {
-                               goto parse_error;
-                       }
-#if ENABLE_HUSH_CASE
-                       if (ctx.ctx_res_w == RES_MATCH)
-                               break; /* we are in case's "word | word)" */
+                       } else if (pi->followup == PIPE_BG) {
+                               /* What does bash do with attempts to background builtins? */
+                               /* even bash 3.2 doesn't do that well with nested bg:
+                                * try "{ { sleep 10; echo DEEP; } & echo HERE; } &".
+                                * I'm NOT treating inner &'s as jobs */
+                               check_and_run_traps(0);
+#if ENABLE_HUSH_JOB
+                               if (G.run_list_level == 1)
+                                       insert_bg_job(pi);
 #endif
-                       if (next == '|') { /* || */
-                               ch = i_getch(input);
-                               nommu_addchr(&ctx.as_string, ch);
-                               done_pipe(&ctx, PIPE_OR);
+                               /* Last command's pid goes to $! */
+                               G.last_bg_pid = pi->cmds[pi->num_cmds - 1].pid;
+                               G.last_exitcode = rcode = EXIT_SUCCESS;
+                               debug_printf_exec(": cmd&: exitcode EXIT_SUCCESS\n");
                        } else {
-                               /* we could pick up a file descriptor choice here
-                                * with redirect_opt_num(), but bash doesn't do it.
-                                * "echo foo 2| cat" yields "foo 2". */
-                               done_command(&ctx);
-#if !BB_MMU
-                               o_reset_to_empty_unquoted(&ctx.as_string);
+#if ENABLE_HUSH_JOB
+                               if (G.run_list_level == 1 && G_interactive_fd) {
+                                       /* Waits for completion, then fg's main shell */
+                                       rcode = checkjobs_and_fg_shell(pi);
+                                       debug_printf_exec(": checkjobs_and_fg_shell exitcode %d\n", rcode);
+                                       check_and_run_traps(0);
+                               } else
 #endif
+                               { /* This one just waits for completion */
+                                       rcode = checkjobs(pi);
+                                       debug_printf_exec(": checkjobs exitcode %d\n", rcode);
+                                       check_and_run_traps(0);
+                               }
+                               G.last_exitcode = rcode;
                        }
-                       goto new_cmd;
-               case '(':
-#if ENABLE_HUSH_CASE
-                       /* "case... in [(]word)..." - skip '(' */
-                       if (ctx.ctx_res_w == RES_MATCH
-                        && ctx.command->argv == NULL /* not (word|(... */
-                        && dest.length == 0 /* not word(... */
-                        && dest.o_quoted == 0 /* not ""(... */
-                       ) {
-                               continue;
-                       }
+               }
+
+               /* Analyze how result affects subsequent commands */
+#if ENABLE_HUSH_IF
+               if (rword == RES_IF || rword == RES_ELIF)
+                       cond_code = rcode;
 #endif
-               case '{':
-                       if (parse_group(&dest, &ctx, input, ch) != 0) {
-                               goto parse_error;
+#if ENABLE_HUSH_LOOPS
+               /* Beware of "while false; true; do ..."! */
+               if (pi->next && pi->next->res_word == RES_DO) {
+                       if (rword == RES_WHILE) {
+                               if (rcode) {
+                                       /* "while false; do...done" - exitcode 0 */
+                                       G.last_exitcode = rcode = EXIT_SUCCESS;
+                                       debug_printf_exec(": while expr is false: breaking (exitcode:EXIT_SUCCESS)\n");
+                                       goto check_jobs_and_break;
+                               }
+                       }
+                       if (rword == RES_UNTIL) {
+                               if (!rcode) {
+                                       debug_printf_exec(": until expr is true: breaking\n");
+ check_jobs_and_break:
+                                       checkjobs(NULL);
+                                       break;
+                               }
                        }
-                       goto new_cmd;
-               case ')':
-#if ENABLE_HUSH_CASE
-                       if (ctx.ctx_res_w == RES_MATCH)
-                               goto case_semi;
-#endif
-               case '}':
-                       /* proper use of this character is caught by end_trigger:
-                        * if we see {, we call parse_group(..., end_trigger='}')
-                        * and it will match } earlier (not here). */
-                       syntax_error_unexpected_ch(ch);
-                       goto parse_error;
-               default:
-                       if (HUSH_DEBUG)
-                               bb_error_msg_and_die("BUG: unexpected %c\n", ch);
                }
-       } /* while (1) */
+#endif
 
- parse_error:
-       {
-               struct parse_context *pctx;
-               IF_HAS_KEYWORDS(struct parse_context *p2;)
+ check_jobs_and_continue:
+               checkjobs(NULL);
+       } /* for (pi) */
 
-               /* Clean up allocated tree.
-                * Sample for finding leaks on syntax error recovery path.
-                * Run it from interactive shell, watch pmap `pidof hush`.
-                * while if false; then false; fi; do break; fi
-                * Samples to catch leaks at execution:
-                * while if (true | {true;}); then echo ok; fi; do break; done
-                * while if (true | {true;}); then echo ok; fi; do (if echo ok; break; then :; fi) | cat; break; done
-                */
-               pctx = &ctx;
-               do {
-                       /* Update pipe/command counts,
-                        * otherwise freeing may miss some */
-                       done_pipe(pctx, PIPE_SEQ);
-                       debug_printf_clean("freeing list %p from ctx %p\n",
-                                       pctx->list_head, pctx);
-                       debug_print_tree(pctx->list_head, 0);
-                       free_pipe_list(pctx->list_head);
-                       debug_printf_clean("freed list %p\n", pctx->list_head);
-#if !BB_MMU
-                       o_free_unsafe(&pctx->as_string);
+#if ENABLE_HUSH_JOB
+       G.run_list_level--;
 #endif
-                       IF_HAS_KEYWORDS(p2 = pctx->stack;)
-                       if (pctx != &ctx) {
-                               free(pctx);
-                       }
-                       IF_HAS_KEYWORDS(pctx = p2;)
-               } while (HAS_KEYWORDS && pctx);
-               /* Free text, clear all dest fields */
-               o_free(&dest);
-               /* If we are not in top-level parse, we return,
-                * our caller will propagate error.
-                */
-               if (end_trigger != ';') {
-#if !BB_MMU
-                       if (pstring)
-                               *pstring = NULL;
+#if ENABLE_HUSH_LOOPS
+       if (loop_top)
+               G.depth_of_loop--;
+       free(for_list);
 #endif
-                       debug_leave();
-                       return ERR_PTR;
-               }
-               /* Discard cached input, force prompt */
-               input->p = NULL;
-               IF_HUSH_INTERACTIVE(input->promptme = 1;)
-               goto reset;
-       }
+#if ENABLE_HUSH_CASE
+       free(case_word);
+#endif
+       debug_leave();
+       debug_printf_exec("run_list lvl %d return %d\n", G.run_list_level + 1, rcode);
+       return rcode;
 }
 
-/* Executing from string: eval, sh -c '...'
- *          or from file: /etc/profile, . file, sh <script>, sh (intereactive)
- * end_trigger controls how often we stop parsing
- * NUL: parse all, execute, return
- * ';': parse till ';' or newline, execute, repeat till EOF
- */
-static void parse_and_run_stream(struct in_str *inp, int end_trigger)
+/* Select which version we will use */
+static int run_and_free_list(struct pipe *pi)
 {
-       /* Why we need empty flag?
-        * An obscure corner case "false; ``; echo $?":
-        * empty command in `` should still set $? to 0.
-        * But we can't just set $? to 0 at the start,
-        * this breaks "false; echo `echo $?`" case.
-        */
-       bool empty = 1;
-       while (1) {
-               struct pipe *pipe_list;
-
-               pipe_list = parse_stream(NULL, inp, end_trigger);
-               if (!pipe_list) { /* EOF */
-                       if (empty)
-                               G.last_exitcode = 0;
-                       break;
-               }
-               debug_print_tree(pipe_list, 0);
-               debug_printf_exec("parse_and_run_stream: run_and_free_list\n");
-               run_and_free_list(pipe_list);
-               empty = 0;
+       int rcode = 0;
+       debug_printf_exec("run_and_free_list entered\n");
+       if (!G.n_mode) {
+               debug_printf_exec(": run_list: 1st pipe with %d cmds\n", pi->num_cmds);
+               rcode = run_list(pi);
        }
+       /* free_pipe_list has the side effect of clearing memory.
+        * In the long run that function can be merged with run_list,
+        * but doing that now would hobble the debugging effort. */
+       free_pipe_list(pi);
+       debug_printf_exec("run_and_free_list return %d\n", rcode);
+       return rcode;
 }
 
-static void parse_and_run_string(const char *s)
-{
-       struct in_str input;
-       setup_string_in_str(&input, s);
-       parse_and_run_stream(&input, '\0');
-}
-
-static void parse_and_run_file(FILE *f)
-{
-       struct in_str input;
-       setup_file_in_str(&input, f);
-       parse_and_run_stream(&input, ';');
-}
 
 /* Called a few times only (or even once if "sh -c") */
 static void init_sigmasks(void)
@@ -6969,8 +7290,8 @@ static int set_mode(const char cstate, const char mode)
 {
        int state = (cstate == '-' ? 1 : 0);
        switch (mode) {
-               case 'n': G.fake_mode = state; break;
-               case 'x': G.x_mode = state; break;
+               case 'n': G.n_mode = state; break;
+               case 'x': IF_HUSH_MODE_X(G_x_mode = state;) break;
                default:  return EXIT_FAILURE;
        }
        return EXIT_SUCCESS;
@@ -6979,13 +7300,6 @@ static int set_mode(const char cstate, const char mode)
 int hush_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int hush_main(int argc, char **argv)
 {
-       static const struct variable const_shell_ver = {
-               .next = NULL,
-               .varstr = (char*)hush_version_str,
-               .max_len = 1, /* 0 can provoke free(name) */
-               .flg_export = 1,
-               .flg_read_only = 1,
-       };
        int opt;
        unsigned builtin_argc;
        char **e;
@@ -6998,12 +7312,16 @@ int hush_main(int argc, char **argv)
        G.argv0_for_re_execing = argv[0];
 #endif
        /* Deal with HUSH_VERSION */
-       G.shell_ver = const_shell_ver; /* copying struct here */
+       G.shell_ver.flg_export = 1;
+       G.shell_ver.flg_read_only = 1;
+       /* Code which handles ${var/P/R} needs writable values for all variables,
+        * therefore we xstrdup: */
+       G.shell_ver.varstr = xstrdup(hush_version_str),
        G.top_var = &G.shell_ver;
+       /* Create shell local variables from the values
+        * currently living in the environment */
        debug_printf_env("unsetenv '%s'\n", "HUSH_VERSION");
        unsetenv("HUSH_VERSION"); /* in case it exists in initial env */
-       /* Initialize our shell local variables with the values
-        * currently living in the environment */
        cur_var = G.top_var;
        e = environ;
        if (e) while (*e) {
@@ -7017,9 +7335,9 @@ int hush_main(int argc, char **argv)
                }
                e++;
        }
-       /* reinstate HUSH_VERSION */
-       debug_printf_env("putenv '%s'\n", hush_version_str);
-       putenv((char *)hush_version_str);
+       /* (Re)insert HUSH_VERSION into env (AFTER we scanned the env!) */
+       debug_printf_env("putenv '%s'\n", G.shell_ver.varstr);
+       putenv(G.shell_ver.varstr);
 
        /* Export PWD */
        set_pwd_var(/*exp:*/ 1);
@@ -7382,15 +7700,6 @@ int hush_main(int argc, char **argv)
 }
 
 
-#if ENABLE_LASH
-int lash_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
-int lash_main(int argc, char **argv)
-{
-       bb_error_msg("lash is deprecated, please use hush instead");
-       return hush_main(argc, argv);
-}
-#endif
-
 #if ENABLE_MSH
 int msh_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int msh_main(int argc, char **argv)
@@ -7561,13 +7870,16 @@ static void helper_export_local(char **argv, int exp, int lvl)
 {
        do {
                char *name = *argv;
+               char *name_end = strchrnul(name, '=');
 
                /* So far we do not check that name is valid (TODO?) */
 
-               if (strchr(name, '=') == NULL) {
-                       struct variable *var;
+               if (*name_end == '\0') {
+                       struct variable *var, **vpp;
+
+                       vpp = get_ptr_to_local_var(name, name_end - name);
+                       var = vpp ? *vpp : NULL;
 
-                       var = get_local_var(name);
                        if (exp == -1) { /* unexporting? */
                                /* export -n NAME (without =VALUE) */
                                if (var) {