hush: fix more obscure ${var%...} cases
[oweals/busybox.git] / shell / hush.c
index 3028d795ce840a6e6697728b74cb9d92b891098f..32b90876fde26c265a53b61af9678d9ca8661173 100644 (file)
  * handle the recursion implicit in the various substitutions, especially
  * across continuation lines.
  *
- * POSIX syntax not implemented:
+ * TODOs:
+ *      grep for "TODO" and fix (some of them are easy)
+ *      special variables (done: PWD, PPID, RANDOM)
+ *      tilde expansion
  *      aliases
- *      <(list) and >(list) Process Substitution
- *      Tilde Expansion
+ *      follow IFS rules more precisely, including update semantics
+ *      builtins mandated by standards we don't support:
+ *          [un]alias, command, fc, getopts, newgrp, readonly, times
+ *      make complex ${var%...} constructs support optional
+ *      make here documents optional
  *
- * Bash stuff (optionally enabled):
- *      &> and >& redirection of stdout+stderr
- *      Brace Expansion
- *      reserved words: [[ ]] function select
- *      substrings ${var:1:5}
+ * Bash compat TODO:
+ *      redirection of stdout+stderr: &> and >&
+ *      brace expansion: one/{two,three,four}
+ *      reserved words: function select
+ *      advanced test: [[ ]]
+ *      process substitution: <(list) and >(list)
+ *      =~: regex operator
  *      let EXPR [EXPR...]
- *        Each EXPR is an arithmetic expression (ARITHMETIC EVALUATION)
- *        If the last arg evaluates to 0, let returns 1; 0 otherwise.
- *        NB: let `echo 'a=a + 1'` - error (IOW: multi-word expansion is used)
+ *          Each EXPR is an arithmetic expression (ARITHMETIC EVALUATION)
+ *          If the last arg evaluates to 0, let returns 1; 0 otherwise.
+ *          NB: let `echo 'a=a + 1'` - error (IOW: multi-word expansion is used)
  *      ((EXPR))
- *        The EXPR is evaluated according to ARITHMETIC EVALUATION.
- *        This is exactly equivalent to let "expression".
- *
- * TODOs:
- *      grep for "TODO" and fix (some of them are easy)
- *      builtins: ulimit
- *      special variables (done: PWD)
- *      follow IFS rules more precisely, including update semantics
+ *          The EXPR is evaluated according to ARITHMETIC EVALUATION.
+ *          This is exactly equivalent to let "EXPR".
+ *      $[EXPR]: synonym for $((EXPR))
  *      export builtin should be special, its arguments are assignments
  *          and therefore expansion of them should be "one-word" expansion:
  *              $ export i=`echo 'a  b'` # export has one arg: "i=a  b"
 #if ENABLE_HUSH_CASE
 # include <fnmatch.h>
 #endif
+
+#include "shell_common.h"
+#include "builtin_read.h"
+#include "builtin_ulimit.h"
 #include "math.h"
 #include "match.h"
+#if ENABLE_HUSH_RANDOM_SUPPORT
+# include "random.h"
+#else
+# define CLEAR_RANDOM_T(rnd) ((void)0)
+#endif
 #ifndef PIPE_BUF
 # define PIPE_BUF 4096  /* amount of buffering in a pipe */
 #endif
 # define USE_FOR_MMU(...)
 #endif
 
-#if defined SINGLE_APPLET_MAIN
+#define SKIP_definitions 1
+#include "applet_tables.h"
+#undef SKIP_definitions
+#if NUM_APPLETS == 1
 /* STANDALONE does not make sense, and won't compile */
 # undef CONFIG_FEATURE_SH_STANDALONE
 # undef ENABLE_FEATURE_SH_STANDALONE
 # undef IF_FEATURE_SH_STANDALONE
+# undef IF_NOT_FEATURE_SH_STANDALONE
+# define ENABLE_FEATURE_SH_STANDALONE 0
 # define IF_FEATURE_SH_STANDALONE(...)
 # define IF_NOT_FEATURE_SH_STANDALONE(...) __VA_ARGS__
-# define ENABLE_FEATURE_SH_STANDALONE 0
 #endif
 
 #if !ENABLE_HUSH_INTERACTIVE
@@ -449,9 +465,9 @@ struct function {
        char *name;
        struct command *parent_cmd;
        struct pipe *body;
-#if !BB_MMU
+# if !BB_MMU
        char *body_as_string;
-#endif
+# endif
 };
 #endif
 
@@ -486,6 +502,9 @@ struct globals {
        pid_t root_pid;
        pid_t root_ppid;
        pid_t last_bg_pid;
+#if ENABLE_HUSH_RANDOM_SUPPORT
+       random_t random_gen;
+#endif
 #if ENABLE_HUSH_JOB
        int run_list_level;
        int last_jobid;
@@ -512,6 +531,7 @@ struct globals {
        smalluint last_exitcode;
        /* are global_argv and global_argv[1..n] malloced? (note: not [0]) */
        smalluint global_args_malloced;
+       smalluint inherited_set_is_saved;
        /* how many non-NULL argv's we have. NB: $# + 1 */
        int global_argc;
        char **global_argv;
@@ -548,7 +568,7 @@ struct globals {
        unsigned long memleak_value;
        int debug_indent;
 #endif
-       char user_input_buf[ENABLE_FEATURE_EDITING ? BUFSIZ : 2];
+       char user_input_buf[ENABLE_FEATURE_EDITING ? CONFIG_FEATURE_EDITING_MAX_LEN : 2];
 };
 #define G (*ptr_to_globals)
 /* Not #defining name to G.name - this quickly gets unwieldy
@@ -579,6 +599,9 @@ static int builtin_local(char **argv) FAST_FUNC;
 #if HUSH_DEBUG
 static int builtin_memleak(char **argv) FAST_FUNC;
 #endif
+#if ENABLE_PRINTF
+static int builtin_printf(char **argv) FAST_FUNC;
+#endif
 static int builtin_pwd(char **argv) FAST_FUNC;
 static int builtin_read(char **argv) FAST_FUNC;
 static int builtin_set(char **argv) FAST_FUNC;
@@ -606,10 +629,10 @@ static int builtin_return(char **argv) FAST_FUNC;
  * For example, 'unset foo | whatever' will parse and run, but foo will
  * still be set at the end. */
 struct built_in_command {
-       const char *cmd;
-       int (*function)(char **argv) FAST_FUNC;
+       const char *b_cmd;
+       int (*b_function)(char **argv) FAST_FUNC;
 #if ENABLE_HUSH_HELP
-       const char *descr;
+       const char *b_descr;
 # define BLTIN(cmd, func, help) { cmd, func, help }
 #else
 # define BLTIN(cmd, func, help) { cmd, func }
@@ -654,9 +677,12 @@ static const struct built_in_command bltins1[] = {
 #endif
        BLTIN("set"      , builtin_set     , "Set/unset positional parameters"),
        BLTIN("shift"    , builtin_shift   , "Shift positional parameters"),
+#if ENABLE_HUSH_BASH_COMPAT
+       BLTIN("source"   , builtin_source  , "Run commands in a file"),
+#endif
        BLTIN("trap"     , builtin_trap    , "Trap signals"),
-       BLTIN("type"     , builtin_type    , "Write a description of command type"),
-//     BLTIN("ulimit"   , builtin_ulimit  , "Control resource limits"),
+       BLTIN("type"     , builtin_type    , "Show command type"),
+       BLTIN("ulimit"   , shell_builtin_ulimit  , "Control resource limits"),
        BLTIN("umask"    , builtin_umask   , "Set file creation mask"),
        BLTIN("unset"    , builtin_unset   , "Unset variables"),
        BLTIN("wait"     , builtin_wait    , "Wait for process"),
@@ -666,6 +692,9 @@ static const struct built_in_command bltins1[] = {
 static const struct built_in_command bltins2[] = {
        BLTIN("["        , builtin_test    , NULL),
        BLTIN("echo"     , builtin_echo    , NULL),
+#if ENABLE_PRINTF
+       BLTIN("printf"   , builtin_printf  , NULL),
+#endif
        BLTIN("pwd"      , builtin_pwd     , NULL),
        BLTIN("test"     , builtin_test    , NULL),
 };
@@ -875,28 +904,6 @@ static void cmdedit_update_prompt(void);
 
 /* Utility functions
  */
-static int glob_needed(const char *s)
-{
-       while (*s) {
-               if (*s == '\\')
-                       s++;
-               if (*s == '*' || *s == '[' || *s == '?')
-                       return 1;
-               s++;
-       }
-       return 0;
-}
-
-static int is_well_formed_var_name(const char *s, char terminator)
-{
-       if (!s || !(isalpha(*s) || *s == '_'))
-               return 0;
-       s++;
-       while (isalnum(*s) || *s == '_')
-               s++;
-       return *s == terminator;
-}
-
 /* Replace each \x with x in place, return ptr past NUL. */
 static char *unbackslash(char *src)
 {
@@ -1048,7 +1055,8 @@ static void restore_G_args(save_arg_t *sv, char **argv)
  *
  * Trap handlers will execute even within trap handlers. (right?)
  *
- * User trap handlers are forgotten when subshell ("(cmd)") is entered.
+ * User trap handlers are forgotten when subshell ("(cmd)") is entered,
+ * except for handlers set to '' (empty string).
  *
  * If job control is off, backgrounded commands ("cmd &")
  * have SIGINT, SIGQUIT set to SIG_IGN.
@@ -1104,7 +1112,7 @@ static void restore_G_args(save_arg_t *sv, char **argv)
  * after [v]fork, if we plan to be a shell:
  *    unblock signals with special interactive handling
  *    (child shell is not interactive),
- *    unset all traps (note: regardless of child shell's type - {}, (), etc)
+ *    unset all traps except '' (note: regardless of child shell's type - {}, (), etc)
  * after [v]fork, if we plan to exec:
  *    POSIX says fork clears pending signal mask in child - no need to clear it.
  *    Restore blocked signal set to one inherited by shell just prior to exec.
@@ -1116,7 +1124,7 @@ static void restore_G_args(save_arg_t *sv, char **argv)
  * Note 2 (compat):
  * Standard says "When a subshell is entered, traps that are not being ignored
  * are set to the default actions". bash interprets it so that traps which
- * are set to "" (ignore) are NOT reset to defaults. We do the same.
+ * are set to '' (ignore) are NOT reset to defaults. We do the same.
  */
 enum {
        SPECIAL_INTERACTIVE_SIGS = 0
@@ -1192,7 +1200,7 @@ static void hush_exit(int exitcode)
        }
 
 #if ENABLE_HUSH_JOB
-       fflush(NULL); /* flush all streams */
+       fflush_all();
        sigexit(- (exitcode & 0xff));
 #else
        exit(exitcode);
@@ -1303,7 +1311,7 @@ static struct variable *get_local_var(const char *name)
        return NULL;
 }
 
-static const char *get_local_var_value(const char *name)
+static const char* FAST_FUNC get_local_var_value(const char *name)
 {
        struct variable **pp = get_ptr_to_local_var(name);
        if (pp)
@@ -1311,6 +1319,11 @@ static const char *get_local_var_value(const char *name)
        if (strcmp(name, "PPID") == 0)
                return utoa(G.root_ppid);
        // bash compat: UID? EUID?
+#if ENABLE_HUSH_RANDOM_SUPPORT
+       if (strcmp(name, "RANDOM") == 0) {
+               return utoa(next_random(&G.random_gen));
+       }
+#endif
        return NULL;
 }
 
@@ -1501,7 +1514,7 @@ static void unset_vars(char **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 *endofname(const char *name)
+static char* FAST_FUNC endofname(const char *name)
 {
        char *p;
 
@@ -1514,14 +1527,13 @@ static char *endofname(const char *name)
        }
        return p;
 }
+#endif
 
-static void arith_set_local_var(const char *name, const char *val, int flags)
+static void FAST_FUNC set_local_var_from_halves(const char *name, const char *val)
 {
-       /* arith code doesnt malloc space, so do it for it */
        char *var = xasprintf("%s=%s", name, val);
-       set_local_var(var, flags, /*lvl:*/ 0, /*ro:*/ 0);
+       set_local_var(var, /*flags:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
 }
-#endif
 
 
 /*
@@ -1647,7 +1659,7 @@ static void get_user_input(struct in_str *i)
                G.flag_SIGINT = 0;
                /* buglet: SIGINT will not make new prompt to appear _at once_,
                 * only after <Enter>. (^C will work) */
-               r = read_line_input(prompt_str, G.user_input_buf, BUFSIZ-1, G.line_input_state);
+               r = read_line_input(prompt_str, G.user_input_buf, CONFIG_FEATURE_EDITING_MAX_LEN-1, G.line_input_state);
                /* catch *SIGINT* etc (^C is handled by read_line_input) */
                check_and_run_traps(0);
        } while (r == 0 || G.flag_SIGINT); /* repeat if ^C or SIGINT */
@@ -1660,7 +1672,7 @@ static void get_user_input(struct in_str *i)
        do {
                G.flag_SIGINT = 0;
                fputs(prompt_str, stdout);
-               fflush(stdout);
+               fflush_all();
                G.user_input_buf[0] = r = fgetc(i->file);
                /*G.user_input_buf[1] = '\0'; - already is and never changed */
 //do we need check_and_run_traps(0)? (maybe only if stdin)
@@ -1837,13 +1849,31 @@ static void o_addblock_duplicate_backslash(o_string *o, const char *str, int len
        }
 }
 
+#undef HUSH_BRACE_EXP
+/*
+ * HUSH_BRACE_EXP code needs corresponding quoting on variable expansion side.
+ * Currently, "v='{q,w}'; echo $v" erroneously expands braces in $v.
+ * Apparently, on unquoted $v bash still does globbing
+ * ("v='*.txt'; echo $v" prints all .txt files),
+ * but NOT brace expansion! Thus, there should be TWO independent
+ * quoting mechanisms on $v expansion side: one protects
+ * $v from brace expansion, and other additionally protects "$v" against globbing.
+ * We have only second one.
+ */
+
+#ifdef HUSH_BRACE_EXP
+# define MAYBE_BRACES "{}"
+#else
+# define MAYBE_BRACES ""
+#endif
+
 /* My analysis of quoting semantics tells me that state information
  * is associated with a destination, not a source.
  */
 static void o_addqchr(o_string *o, int ch)
 {
        int sz = 1;
-       char *found = strchr("*?[\\", ch);
+       char *found = strchr("*?[\\" MAYBE_BRACES, ch);
        if (found)
                sz++;
        o_grow_by(o, sz);
@@ -1859,7 +1889,7 @@ 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("*?[\\", ch)) {
+       if (o->o_escape && strchr("*?[\\" MAYBE_BRACES, ch)) {
                sz++;
                o->data[o->length] = '\\';
                o->length++;
@@ -1879,7 +1909,7 @@ static void o_addQstr(o_string *o, const char *str, int len)
        while (len) {
                char ch;
                int sz;
-               int ordinary_cnt = strcspn(str, "*?[\\");
+               int ordinary_cnt = strcspn(str, "*?[\\" MAYBE_BRACES);
                if (ordinary_cnt > len) /* paranoia */
                        ordinary_cnt = len;
                o_addblock(o, str, ordinary_cnt);
@@ -1890,7 +1920,7 @@ static void o_addQstr(o_string *o, const char *str, int len)
 
                ch = *str++;
                sz = 1;
-               if (ch) { /* it is necessarily one of "*?[\\" */
+               if (ch) { /* it is necessarily one of "*?[\\" MAYBE_BRACES */
                        sz++;
                        o->data[o->length] = '\\';
                        o->length++;
@@ -1984,8 +2014,223 @@ static int o_get_last_ptr(o_string *o, int n)
        return ((int)(ptrdiff_t)list[n-1]) + string_start;
 }
 
-/* o_glob performs globbing on last list[], saving each result
- * as a new list[]. */
+#ifdef HUSH_BRACE_EXP
+/* There in a GNU extension, GLOB_BRACE, but it is not usable:
+ * first, it processes even {a} (no commas), second,
+ * I didn't manage to make it return strings when they don't match
+ * existing files. Need to re-implement it.
+ */
+
+/* Helper */
+static int glob_needed(const char *s)
+{
+       while (*s) {
+               if (*s == '\\') {
+                       if (!s[1])
+                               return 0;
+                       s += 2;
+                       continue;
+               }
+               if (*s == '*' || *s == '[' || *s == '?' || *s == '{')
+                       return 1;
+               s++;
+       }
+       return 0;
+}
+/* Return pointer to next closing brace or to comma */
+static const char *next_brace_sub(const char *cp)
+{
+       unsigned depth = 0;
+       cp++;
+       while (*cp != '\0') {
+               if (*cp == '\\') {
+                       if (*++cp == '\0')
+                               break;
+                       cp++;
+                       continue;
+               }
+                /*{*/ if ((*cp == '}' && depth-- == 0) || (*cp == ',' && depth == 0))
+                       break;
+               if (*cp++ == '{') /*}*/
+                       depth++;
+       }
+
+       return *cp != '\0' ? cp : NULL;
+}
+/* Recursive brace globber. Note: may garble pattern[]. */
+static int glob_brace(char *pattern, o_string *o, int n)
+{
+       char *new_pattern_buf;
+       const char *begin;
+       const char *next;
+       const char *rest;
+       const char *p;
+       size_t rest_len;
+
+       debug_printf_glob("glob_brace('%s')\n", pattern);
+
+       begin = pattern;
+       while (1) {
+               if (*begin == '\0')
+                       goto simple_glob;
+               if (*begin == '{') /*}*/ {
+                       /* Find the first sub-pattern and at the same time
+                        * find the rest after the closing brace */
+                       next = next_brace_sub(begin);
+                       if (next == NULL) {
+                               /* An illegal expression */
+                               goto simple_glob;
+                       }
+                       /*{*/ if (*next == '}') {
+                               /* "{abc}" with no commas - illegal
+                                * brace expr, disregard and skip it */
+                               begin = next + 1;
+                               continue;
+                       }
+                       break;
+               }
+               if (*begin == '\\' && begin[1] != '\0')
+                       begin++;
+               begin++;
+       }
+       debug_printf_glob("begin:%s\n", begin);
+       debug_printf_glob("next:%s\n", next);
+
+       /* Now find the end of the whole brace expression */
+       rest = next;
+       /*{*/ while (*rest != '}') {
+               rest = next_brace_sub(rest);
+               if (rest == NULL) {
+                       /* An illegal expression */
+                       goto simple_glob;
+               }
+               debug_printf_glob("rest:%s\n", rest);
+       }
+       rest_len = strlen(++rest) + 1;
+
+       /* We are sure the brace expression is well-formed */
+
+       /* Allocate working buffer large enough for our work */
+       new_pattern_buf = xmalloc(strlen(pattern));
+
+       /* We have a brace expression.  BEGIN points to the opening {,
+        * NEXT points past the terminator of the first element, and REST
+        * points past the final }.  We will accumulate result names from
+        * recursive runs for each brace alternative in the buffer using
+        * GLOB_APPEND.  */
+
+       p = begin + 1;
+       while (1) {
+               /* Construct the new glob expression */
+               memcpy(
+                       mempcpy(
+                               mempcpy(new_pattern_buf,
+                                       /* We know the prefix for all sub-patterns */
+                                       pattern, begin - pattern),
+                               p, next - p),
+                       rest, rest_len);
+
+               /* Note: glob_brace() may garble new_pattern_buf[].
+                * That's why we re-copy prefix every time (1st memcpy above).
+                */
+               n = glob_brace(new_pattern_buf, o, n);
+               /*{*/ if (*next == '}') {
+                       /* We saw the last entry */
+                       break;
+               }
+               p = next + 1;
+               next = next_brace_sub(next);
+       }
+       free(new_pattern_buf);
+       return n;
+
+ simple_glob:
+       {
+               int gr;
+               glob_t globdata;
+
+               memset(&globdata, 0, sizeof(globdata));
+               gr = glob(pattern, 0, NULL, &globdata);
+               debug_printf_glob("glob('%s'):%d\n", pattern, gr);
+               if (gr != 0) {
+                       if (gr == GLOB_NOMATCH) {
+                               globfree(&globdata);
+                               /* NB: garbles parameter */
+                               unbackslash(pattern);
+                               o_addstr_with_NUL(o, pattern);
+                               debug_printf_glob("glob pattern '%s' is literal\n", pattern);
+                               return o_save_ptr_helper(o, n);
+                       }
+                       if (gr == GLOB_NOSPACE)
+                               bb_error_msg_and_die(bb_msg_memory_exhausted);
+                       /* GLOB_ABORTED? Only happens with GLOB_ERR flag,
+                        * but we didn't specify it. Paranoia again. */
+                       bb_error_msg_and_die("glob error %d on '%s'", gr, pattern);
+               }
+               if (globdata.gl_pathv && globdata.gl_pathv[0]) {
+                       char **argv = globdata.gl_pathv;
+                       while (1) {
+                               o_addstr_with_NUL(o, *argv);
+                               n = o_save_ptr_helper(o, n);
+                               argv++;
+                               if (!*argv)
+                                       break;
+                       }
+               }
+               globfree(&globdata);
+       }
+       return n;
+}
+/* Performs globbing on last list[],
+ * saving each result as a new list[].
+ */
+static int o_glob(o_string *o, int n)
+{
+       char *pattern, *copy;
+
+       debug_printf_glob("start o_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);
+       debug_printf_glob("glob pattern '%s'\n", pattern);
+       if (!glob_needed(pattern)) {
+               /* unbackslash last string in o in place, fix length */
+               o->length = unbackslash(pattern) - o->data;
+               debug_printf_glob("glob pattern '%s' is literal\n", pattern);
+               return o_save_ptr_helper(o, n);
+       }
+
+       copy = xstrdup(pattern);
+       /* "forget" pattern in o */
+       o->length = pattern - o->data;
+       n = glob_brace(copy, o, n);
+       free(copy);
+       if (DEBUG_GLOB)
+               debug_print_list("o_glob returning", o, n);
+       return n;
+}
+
+#else
+
+/* Helper */
+static int glob_needed(const char *s)
+{
+       while (*s) {
+               if (*s == '\\') {
+                       if (!s[1])
+                               return 0;
+                       s += 2;
+                       continue;
+               }
+               if (*s == '*' || *s == '[' || *s == '?')
+                       return 1;
+               s++;
+       }
+       return 0;
+}
+/* Performs globbing on last list[],
+ * saving each result as a new list[].
+ */
 static int o_glob(o_string *o, int n)
 {
        glob_t globdata;
@@ -1999,27 +2244,35 @@ static int o_glob(o_string *o, int n)
        debug_printf_glob("glob pattern '%s'\n", pattern);
        if (!glob_needed(pattern)) {
  literal:
+               /* unbackslash last string in o in place, fix length */
                o->length = unbackslash(pattern) - o->data;
                debug_printf_glob("glob pattern '%s' is literal\n", pattern);
                return o_save_ptr_helper(o, n);
        }
 
        memset(&globdata, 0, sizeof(globdata));
+       /* Can't use GLOB_NOCHECK: it does not unescape the string.
+        * If we glob "*.\*" and don't find anything, we need
+        * to fall back to using literal "*.*", but GLOB_NOCHECK
+        * will return "*.\*"!
+        */
        gr = glob(pattern, 0, NULL, &globdata);
        debug_printf_glob("glob('%s'):%d\n", pattern, gr);
-       if (gr == GLOB_NOSPACE)
-               bb_error_msg_and_die("out of memory during glob");
-       if (gr == GLOB_NOMATCH) {
-               globfree(&globdata);
-               goto literal;
-       }
-       if (gr != 0) { /* GLOB_ABORTED ? */
-               /* TODO: testcase for bad glob pattern behavior */
-               bb_error_msg("glob(3) error %d on '%s'", gr, pattern);
+       if (gr != 0) {
+               if (gr == GLOB_NOMATCH) {
+                       globfree(&globdata);
+                       goto literal;
+               }
+               if (gr == GLOB_NOSPACE)
+                       bb_error_msg_and_die(bb_msg_memory_exhausted);
+               /* GLOB_ABORTED? Only happens with GLOB_ERR flag,
+                * but we didn't specify it. Paranoia again. */
+               bb_error_msg_and_die("glob error %d on '%s'", gr, pattern);
        }
        if (globdata.gl_pathv && globdata.gl_pathv[0]) {
                char **argv = globdata.gl_pathv;
-               o->length = pattern - o->data; /* "forget" pattern */
+               /* "forget" pattern in o */
+               o->length = pattern - o->data;
                while (1) {
                        o_addstr_with_NUL(o, *argv);
                        n = o_save_ptr_helper(o, n);
@@ -2034,6 +2287,8 @@ static int o_glob(o_string *o, int n)
        return n;
 }
 
+#endif
+
 /* If o->o_glob == 1, glob the string so far remembered.
  * Otherwise, just finish current list[] and start new */
 static int o_save_ptr(o_string *o, int n)
@@ -2150,6 +2405,23 @@ static char *expand_pseudo_dquoted(const char *str)
        return exp_str;
 }
 
+#if ENABLE_SH_MATH_SUPPORT
+static arith_t expand_and_evaluate_arith(const char *arg, int *errcode_p)
+{
+       arith_eval_hooks_t hooks;
+       arith_t res;
+       char *exp_str;
+
+       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
+
 /* 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
@@ -2166,7 +2438,7 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
 
        ored_ch = 0;
 
-       debug_printf_expand("expand_vars_to_list: arg '%s'\n", arg);
+       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);
@@ -2174,7 +2446,7 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
        while ((p = strchr(arg, SPECIAL_VAR_SYMBOL)) != NULL) {
                char first_ch;
                int i;
-               char *dyn_val = NULL;
+               char *to_be_freed = NULL;
                const char *val = NULL;
 #if ENABLE_HUSH_TICK
                o_string subst_result = NULL_O_STRING;
@@ -2268,28 +2540,20 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
                         * expanded result may need to be globbed
                         * and $IFS-splitted */
                        debug_printf_subst("SUBST '%s' first_ch %x\n", arg, first_ch);
-                       process_command_subs(&subst_result, arg);
-                       debug_printf_subst("SUBST RES '%s'\n", subst_result.data);
+                       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_eval_hooks_t hooks;
                        arith_t res;
                        int errcode;
-                       char *exp_str;
 
                        arg++; /* skip '+' */
                        *p = '\0'; /* replace trailing <SPECIAL_VAR_SYMBOL> */
                        debug_printf_subst("ARITH '%s' first_ch %x\n", arg, first_ch);
-
-                       exp_str = expand_pseudo_dquoted(arg);
-                       hooks.lookupvar = get_local_var_value;
-                       hooks.setvar = arith_set_local_var;
-                       hooks.endofname = endofname;
-                       res = arith(exp_str ? exp_str : arg, &errcode, &hooks);
-                       free(exp_str);
+                       res = expand_and_evaluate_arith(arg, &errcode);
 
                        if (errcode < 0) {
                                const char *msg = "error in arithmetic";
@@ -2314,35 +2578,32 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
 #endif
                default: /* <SPECIAL_VAR_SYMBOL>varname<SPECIAL_VAR_SYMBOL> */
                case_default: {
-                       bool exp_len = false;
-                       bool exp_null = false;
                        char *var = arg;
+                       char exp_len; /* '#' if it's ${#var} */
+                       char exp_op;
                        char exp_save = exp_save; /* for compiler */
-                       char exp_op = exp_op; /* for compiler */
+                       char *exp_saveptr = exp_saveptr; /* points to expansion operator */
                        char *exp_word = exp_word; /* for compiler */
-                       size_t exp_off = 0;
 
                        *p = '\0';
                        arg[0] = first_ch & 0x7f;
 
                        /* prepare for expansions */
-                       if (var[0] == '#') {
+                       exp_op = 0;
+                       exp_len = var[0];
+                       if (exp_len == '#') {
                                /* handle length expansion ${#var} */
-                               exp_len = true;
-                               ++var;
+                               var++;
                        } else {
                                /* maybe handle parameter expansion */
-                               exp_off = strcspn(var, ":-=+?%#");
-                               if (!var[exp_off])
-                                       exp_off = 0;
-                               if (exp_off) {
-                                       exp_save = var[exp_off];
-                                       exp_null = exp_save == ':';
-                                       exp_word = var + exp_off;
-                                       if (exp_null)
-                                               ++exp_word;
+                               exp_saveptr = var + strcspn(var, "%#:-=+?");
+                               exp_save = *exp_saveptr;
+                               if (exp_save) {
+                                       exp_word = exp_saveptr;
+                                       if (exp_save == ':')
+                                               exp_word++;
                                        exp_op = *exp_word++;
-                                       var[exp_off] = '\0';
+                                       *exp_saveptr = '\0';
                                }
                        }
 
@@ -2357,43 +2618,118 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
                                val = get_local_var_value(var);
 
                        /* handle any expansions */
-                       if (exp_len) {
-                               debug_printf_expand("expand: length of '%s' = ", val);
+                       if (exp_len == '#') {
+                               debug_printf_expand("expand: length(%s)=", val);
                                val = utoa(val ? strlen(val) : 0);
                                debug_printf_expand("%s\n", val);
-                       } else if (exp_off) {
+                       } 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) {
-                                               /* we need to do a pattern match */
                                                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 = dyn_val = xstrdup(val);
-                                               loc = scan(dyn_val, exp_word, match_at_left);
-                                               if (match_at_left) /* # or ## */
-                                                       val = loc;
-                                               else if (loc) /* % or %% and match was found */
-                                                       *loc = '\0';
+                                                       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 {
-                                       /* we need to do an expansion */
-                                       int exp_test = (!val || (exp_null && !val[0]));
+                               } else if (!strchr("%#:-=+?"+3, exp_op)) {
+#if ENABLE_HUSH_BASH_COMPAT
+       /* exp_op is ':' and next char isn't a subst operator.
+        * Assuming it's ${var:[N][:M]} bashism.
+        * TODO: N, M can be expressions similar to $((EXPR)): 2+2, 2+var etc
+        */
+                                       char *end;
+                                       unsigned len = INT_MAX;
+                                       unsigned beg = 0;
+                                       end = --exp_word;
+                                       if (*exp_word != ':') /* not ${var::...} */
+                                               beg = bb_strtou(exp_word, &end, 0);
+                                       //bb_error_msg("beg:'%s'=%u end:'%s'", exp_word, beg, end);
+                                       if (*end == ':') {
+                                               if (end[1] != '\0') /* not ${var:NUM:} */
+                                                       len = bb_strtou(end + 1, &end, 0);
+                                               else {
+                                                       len = 0;
+                                                       end++;
+                                               }
+                                               //bb_error_msg("len:%u end:'%s'", len, end);
+                                       }
+                                       if (*end == '\0') {
+                                               //bb_error_msg("from val:'%s'", val);
+                                               if (len == 0 || !val || beg >= strlen(val))
+                                                       val = "";
+                                               else
+                                                       val = to_be_freed = xstrndup(val + beg, len);
+                                               //bb_error_msg("val:'%s'", 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 == '+')
-                                               exp_test = !exp_test;
+                                               use_word = !use_word;
                                        debug_printf_expand("expand: op:%c (null:%s) test:%i\n", exp_op,
-                                               exp_null ? "true" : "false", exp_test);
-                                       if (exp_test) {
+                                               (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 == '?') {
-//TODO: how interactive bash aborts expansion mid-command?
-                                                       /* ${var?[error_msg_if_unset]} */
-                                                       /* ${var:?[error_msg_if_unset_or_null]} */
                                                        /* 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;
                                                }
@@ -2412,8 +2748,8 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
                                        }
                                }
 
-                               var[exp_off] = exp_save;
-                       }
+                               *exp_saveptr = exp_save;
+                       } /* if (exp_op) */
 
                        arg[0] = first_ch;
 #if ENABLE_HUSH_TICK
@@ -2438,7 +2774,7 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
                if (val) {
                        o_addQstr(output, val, strlen(val));
                }
-               free(dyn_val);
+               free(to_be_freed);
                /* Do the check to avoid writing to a const string */
                if (*p != SPECIAL_VAR_SYMBOL)
                        *p = SPECIAL_VAR_SYMBOL;
@@ -2667,14 +3003,17 @@ static void re_execute_shell(char ***to_free, const char *s,
                char *g_argv0, char **g_argv,
                char **builtin_argv)
 {
-       char param_buf[sizeof("-$%x:%x:%x:%x:%x") + sizeof(unsigned) * 2];
+#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
+# if ENABLE_HUSH_FUNCTIONS
        struct function *funcp;
-#endif
+# endif
        char **argv, **pp;
        unsigned cnt;
+       unsigned long long empty_trap_mask;
 
        if (!g_argv0) { /* heredoc */
                argv = heredoc_argv;
@@ -2691,15 +3030,26 @@ static void re_execute_shell(char ***to_free, const char *s,
        if (pp) while (*pp++)
                cnt++;
 
-       sprintf(param_buf, "-$%x:%x:%x:%x:%x" IF_HUSH_LOOPS(":%x")
+       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)
                        );
-       /* 1:hush 2:-$<pid>:<pid>:<exitcode>:<depth> <vars...> <funcs...>
+#undef NOMMU_HACK_FMT
+       /* 1:hush 2:-$<pid>:<pid>:<exitcode>:<etc...> <vars...> <funcs...>
         * 3:-c 4:<cmd> 5:<arg0> <argN...> 6:NULL
         */
        cnt += 6;
@@ -2707,10 +3057,10 @@ static void re_execute_shell(char ***to_free, const char *s,
                if (!cur->flg_export || cur->flg_read_only)
                        cnt += 2;
        }
-#if ENABLE_HUSH_FUNCTIONS
+# if ENABLE_HUSH_FUNCTIONS
        for (funcp = G.top_func; funcp; funcp = funcp->next)
                cnt += 3;
-#endif
+# endif
        pp = g_argv;
        while (*pp++)
                cnt++;
@@ -2728,13 +3078,13 @@ static void re_execute_shell(char ***to_free, const char *s,
                        *pp++ = cur->varstr;
                }
        }
-#if ENABLE_HUSH_FUNCTIONS
+# 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
+# endif
        /* We can pass activated traps here. Say, -Tnn:trap_string
         *
         * However, POSIX says that subshells reset signals with traps
@@ -2752,7 +3102,9 @@ static void re_execute_shell(char ***to_free, const char *s,
         * _inside_ group (just before echo 1), it works.
         *
         * I conclude it means we don't need to pass active traps here.
-        * exec syscall below resets them to SIG_DFL for us.
+        * 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;
@@ -3076,7 +3428,7 @@ static const struct built_in_command* find_builtin_helper(const char *name,
                const struct built_in_command *end)
 {
        while (x != end) {
-               if (strcmp(name, x->cmd) != 0) {
+               if (strcmp(name, x->b_cmd) != 0) {
                        x++;
                        continue;
                }
@@ -3201,7 +3553,7 @@ static void exec_function(char ***to_free,
        G.global_argc = n;
        /* On MMU, funcp->body is always non-NULL */
        n = run_list(funcp->body);
-       fflush(NULL);
+       fflush_all();
        _exit(n);
 # else
        re_execute_shell(to_free,
@@ -3223,9 +3575,9 @@ static int run_function(const struct function *funcp, char **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
+# if ENABLE_HUSH_LOCAL
        G.func_nest_level++;
-#endif
+# endif
 
        /* On MMU, funcp->body is always non-NULL */
 # if !BB_MMU
@@ -3239,7 +3591,7 @@ static int run_function(const struct function *funcp, char **argv)
                rc = run_list(funcp->body);
        }
 
-#if ENABLE_HUSH_LOCAL
+# if ENABLE_HUSH_LOCAL
        {
                struct variable *var;
                struct variable **var_pp;
@@ -3262,7 +3614,7 @@ static int run_function(const struct function *funcp, char **argv)
                }
                G.func_nest_level--;
        }
-#endif
+# endif
        G.flag_return_in_progress = sv_flg;
 
        restore_G_args(&sv, argv);
@@ -3272,13 +3624,13 @@ static int run_function(const struct function *funcp, char **argv)
 #endif /* ENABLE_HUSH_FUNCTIONS */
 
 
-# if BB_MMU
+#if BB_MMU
 #define exec_builtin(to_free, x, argv) \
        exec_builtin(x, argv)
-# else
+#else
 #define exec_builtin(to_free, x, argv) \
        exec_builtin(to_free, argv)
-# endif
+#endif
 static void exec_builtin(char ***to_free,
                const struct built_in_command *x,
                char **argv) NORETURN;
@@ -3286,11 +3638,11 @@ static void exec_builtin(char ***to_free,
                const struct built_in_command *x,
                char **argv)
 {
-# if BB_MMU
-       int rcode = x->function(argv);
-       fflush(NULL);
+#if BB_MMU
+       int rcode = x->b_function(argv);
+       fflush_all();
        _exit(rcode);
-# else
+#else
        /* On NOMMU, we must never block!
         * Example: { sleep 99 | read line; } & echo Ok
         */
@@ -3299,10 +3651,20 @@ static void exec_builtin(char ***to_free,
                        G.global_argv[0],
                        G.global_argv + 1,
                        argv);
-# endif
+#endif
 }
 
 
+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 */
+}
+
 #if BB_MMU
 #define pseudo_exec_argv(nommu_save, argv, assignment_cnt, argv_expanded) \
        pseudo_exec_argv(argv, assignment_cnt, argv_expanded)
@@ -3318,7 +3680,7 @@ static void exec_builtin(char ***to_free,
 static void pseudo_exec_argv(nommu_save_t *nommu_save,
                char **argv, int assignment_cnt,
                char **argv_expanded) NORETURN;
-static void pseudo_exec_argv(nommu_save_t *nommu_save,
+static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
                char **argv, int assignment_cnt,
                char **argv_expanded)
 {
@@ -3402,11 +3764,7 @@ static void pseudo_exec_argv(nommu_save_t *nommu_save,
 #if ENABLE_FEATURE_SH_STANDALONE || BB_MMU
  skip:
 #endif
-       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(EXIT_FAILURE);
+       execvp_or_die(argv);
 }
 
 /* Called after [v]fork() in run_pipe
@@ -3640,9 +3998,7 @@ static int checkjobs(struct pipe* fg_pipe)
                                        fg_pipe->alive_cmds--;
                                        if (i == fg_pipe->num_cmds - 1) {
                                                /* last process gives overall exitstatus */
-                                               /* Note: is WIFSIGNALED, WEXITSTATUS = sig + 128 */
                                                rcode = WEXITSTATUS(status);
-                                               IF_HAS_KEYWORDS(if (fg_pipe->pi_inverted) rcode = !rcode;)
                                                /* bash prints killer signal's name for *last*
                                                 * process in pipe (prints just newline for SIGINT).
                                                 * Mimic this. Example: "sleep 5" + (^\ or kill -QUIT)
@@ -3650,7 +4006,11 @@ static int checkjobs(struct pipe* fg_pipe)
                                                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;
@@ -3839,7 +4199,8 @@ static NOINLINE int run_pipe(struct pipe *pi)
 
                if (argv[command->assignment_cnt] == NULL) {
                        /* Assignments, but no command */
-                       /* Ensure redirects take effect. Try "a=t >file" */
+                       /* Ensure redirects take effect (that is, create files).
+                        * Try "a=t >file": */
                        rcode = setup_redirects(command, squirrel);
                        restore_redirects(squirrel);
                        /* Set shell variables */
@@ -3850,6 +4211,11 @@ static NOINLINE int run_pipe(struct pipe *pi)
                                set_local_var(p, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
                                argv++;
                        }
+                       /* Redirect error sets $? to 1. Othervise,
+                        * 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"
                         */
@@ -3876,6 +4242,13 @@ static NOINLINE int run_pipe(struct pipe *pi)
                        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;
@@ -3884,7 +4257,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 #endif
                if (x || funcp) {
                        if (!funcp) {
-                               if (x->function == builtin_exec && argv_expanded[1] == NULL) {
+                               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;
@@ -3900,9 +4273,9 @@ static NOINLINE int run_pipe(struct pipe *pi)
                                old_vars = set_vars_and_save_old(new_env);
                                if (!funcp) {
                                        debug_printf_exec(": builtin '%s' '%s'...\n",
-                                               x->cmd, argv_expanded[1]);
-                                       rcode = x->function(argv_expanded) & 0xff;
-                                       fflush(NULL);
+                                               x->b_cmd, argv_expanded[1]);
+                                       rcode = x->b_function(argv_expanded) & 0xff;
+                                       fflush_all();
                                }
 #if ENABLE_HUSH_FUNCTIONS
                                else {
@@ -3987,6 +4360,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
                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 */
@@ -4089,30 +4463,30 @@ static void debug_print_tree(struct pipe *pi, int lvl)
        };
        static const char *RES[] = {
                [RES_NONE ] = "NONE" ,
-#if ENABLE_HUSH_IF
+# if ENABLE_HUSH_IF
                [RES_IF   ] = "IF"   ,
                [RES_THEN ] = "THEN" ,
                [RES_ELIF ] = "ELIF" ,
                [RES_ELSE ] = "ELSE" ,
                [RES_FI   ] = "FI"   ,
-#endif
-#if ENABLE_HUSH_LOOPS
+# 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
+# endif
+# if ENABLE_HUSH_LOOPS || ENABLE_HUSH_CASE
                [RES_IN   ] = "IN"   ,
-#endif
-#if ENABLE_HUSH_CASE
+# 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
+# endif
                [RES_XXXX ] = "XXXX" ,
                [RES_SNTX ] = "SNTX" ,
        };
@@ -4120,9 +4494,9 @@ static void debug_print_tree(struct pipe *pi, int lvl)
                "{}",
                "()",
                "[noglob]",
-#if ENABLE_HUSH_FUNCTIONS
+# if ENABLE_HUSH_FUNCTIONS
                "func()",
-#endif
+# endif
        };
 
        int pin, prn;
@@ -4140,9 +4514,15 @@ static void debug_print_tree(struct pipe *pi, int lvl)
                                        lvl*2, "", prn,
                                        command->assignment_cnt);
                        if (command->group) {
-                               fprintf(stderr, " group %s: (argv=%p)\n",
+                               fprintf(stderr, " group %s: (argv=%p)%s%s\n",
                                                CMDTYPE[command->cmd_type],
-                                               argv);
+                                               argv
+#if !BB_MMU
+                                               , " group_as_string:", command->group_as_string
+#else
+                                               , "", ""
+#endif
+                               );
                                debug_print_tree(command->group, lvl+1);
                                prn++;
                                continue;
@@ -4158,7 +4538,7 @@ static void debug_print_tree(struct pipe *pi, int lvl)
                pin++;
        }
 }
-#endif
+#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!) */
@@ -4632,25 +5012,25 @@ struct reserved_combo {
 };
 enum {
        FLAG_END   = (1 << RES_NONE ),
-#if ENABLE_HUSH_IF
+# 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
+# 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
+# endif
+# if ENABLE_HUSH_CASE
        FLAG_MATCH = (1 << RES_MATCH),
        FLAG_ESAC  = (1 << RES_ESAC ),
-#endif
+# endif
        FLAG_START = (1 << RES_XXXX ),
 };
 
@@ -4662,26 +5042,26 @@ static const struct reserved_combo* match_reserved_word(o_string *word)
         * FLAG_START means the word must start a new compound list.
         */
        static const struct reserved_combo reserved_list[] = {
-#if ENABLE_HUSH_IF
+# 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
+# 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
+# endif
+# if ENABLE_HUSH_CASE
                { "case",  RES_CASE,  NOT_ASSIGNMENT , FLAG_MATCH | FLAG_START },
                { "esac",  RES_ESAC,  NOT_ASSIGNMENT , FLAG_END  },
-#endif
+# endif
        };
        const struct reserved_combo *r;
 
@@ -4695,11 +5075,11 @@ static const struct reserved_combo* match_reserved_word(o_string *word)
  */
 static int reserved_word(o_string *word, struct parse_context *ctx)
 {
-#if ENABLE_HUSH_CASE
+# if ENABLE_HUSH_CASE
        static const struct reserved_combo reserved_match = {
                "",        RES_MATCH, NOT_ASSIGNMENT , FLAG_MATCH | FLAG_ESAC
        };
-#endif
+# endif
        const struct reserved_combo *r;
 
        if (word->o_quoted)
@@ -4709,12 +5089,12 @@ static int reserved_word(o_string *word, struct parse_context *ctx)
                return 0;
 
        debug_printf("found reserved word %s, res %d\n", r->literal, r->res);
-#if ENABLE_HUSH_CASE
+# 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
+# endif
        if (r->flag == 0) { /* '!' */
                if (ctx->ctx_inverted) { /* bash doesn't accept '! ! true' */
                        syntax_error("! ! command");
@@ -4755,19 +5135,19 @@ static int reserved_word(o_string *word, struct parse_context *ctx)
                old = ctx->stack;
                old->command->group = ctx->list_head;
                old->command->cmd_type = CMD_NORMAL;
-#if !BB_MMU
+# 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
+# endif
                *ctx = *old;   /* physical copy */
                free(old);
        }
        return 1;
 }
-#endif
+#endif /* HAS_KEYWORDS */
 
 /* Word is complete, look at it and update parsing context.
  * Normal return is 0. Syntax errors return 1.
@@ -5170,13 +5550,13 @@ static int fetch_heredocs(int heredoc_cnt, struct parse_context *ctx, struct in_
 
 
 #if ENABLE_HUSH_TICK
-static FILE *generate_stream_from_string(const char *s)
+static FILE *generate_stream_from_string(const char *s, pid_t *pid_p)
 {
-       FILE *pf;
-       int pid, channel[2];
-#if !BB_MMU
-       char **to_free;
-#endif
+       pid_t pid;
+       int channel[2];
+# if !BB_MMU
+       char **to_free = NULL;
+# endif
 
        xpipe(channel);
        pid = BB_MMU ? fork() : vfork();
@@ -5194,6 +5574,7 @@ static FILE *generate_stream_from_string(const char *s)
                        + (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 */
@@ -5239,11 +5620,11 @@ static FILE *generate_stream_from_string(const char *s)
                        builtin_trap((char**)argv);
                        exit(0); /* not _exit() - we need to fflush */
                }
-#if BB_MMU
+# if BB_MMU
                reset_traps_to_defaults();
                parse_and_run_string(s);
                _exit(G.last_exitcode);
-#else
+# 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
@@ -5254,37 +5635,36 @@ static FILE *generate_stream_from_string(const char *s)
                                G.global_argv[0],
                                G.global_argv + 1,
                                NULL);
-#endif
+# endif
        }
 
        /* parent */
-#if ENABLE_HUSH_FAST
+       *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
+# endif
        enable_restore_tty_pgrp_on_exit();
-#if !BB_MMU
+# if !BB_MMU
        free(to_free);
-#endif
+# endif
        close(channel[1]);
-       pf = fdopen(channel[0], "r");
-       return pf;
+       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 *pf;
+       FILE *fp;
        struct in_str pipe_str;
-       int ch, eol_cnt;
+       pid_t pid;
+       int status, ch, eol_cnt;
 
-       pf = generate_stream_from_string(s);
-       if (pf == NULL)
-               return 1;
-       close_on_exec_on(fileno(pf));
+       fp = generate_stream_from_string(s, &pid);
 
        /* Now send results of command back into original context */
-       setup_file_in_str(&pipe_str, pf);
+       setup_file_in_str(&pipe_str, fp);
        eol_cnt = 0;
        while ((ch = i_getch(&pipe_str)) != EOF) {
                if (ch == '\n') {
@@ -5298,19 +5678,21 @@ static int process_command_subs(o_string *dest, const char *s)
                o_addQchr(dest, ch);
        }
 
-       debug_printf("done reading from pipe, pclose()ing\n");
-       /* Note: we got EOF, and we just close the read end of the pipe.
-        * We do not wait for the `cmd` child to terminate. bash and ash do.
-        * Try these:
-        * echo `echo Hi; exec 1>&-; sleep 2` - bash waits 2 sec
-        * `false`; echo $? - bash outputs "1"
-        */
-       fclose(pf);
-       debug_printf("closed FILE from child. return 0\n");
-       return 0;
+       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
+#endif /* ENABLE_HUSH_TICK */
 
+#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)
 {
@@ -5354,6 +5736,8 @@ static int parse_group(o_string *dest, struct parse_context *ctx,
                goto skip;
        }
 #endif
+
+#if 0 /* Prevented by caller */
        if (command->argv /* word [word]{... */
         || dest->length /* word{... */
         || dest->o_quoted /* ""{... */
@@ -5363,6 +5747,7 @@ static int parse_group(o_string *dest, struct parse_context *ctx,
                        "syntax error, groups and arglists don't mix\n");
                return 1;
        }
+#endif
 
 #if ENABLE_HUSH_FUNCTIONS
  skip:
@@ -5503,29 +5888,37 @@ static void add_till_backquote(o_string *dest, struct in_str *input)
  * echo $(echo '(TEST)' BEST)           (TEST) BEST
  * echo $(echo 'TEST)' BEST)            TEST) BEST
  * echo $(echo \(\(TEST\) BEST)         ((TEST) BEST
+ *
+ * Also adapted to eat ${var%...} constructs, since ... part
+ * can contain arbitrary constructs, just like $(cmd).
  */
-static void add_till_closing_paren(o_string *dest, struct in_str *input, bool dbl)
+#define DOUBLE_CLOSE_CHAR_FLAG 0x80
+static void add_till_closing_paren(o_string *dest, struct in_str *input, char end_ch)
 {
-       int count = 0;
+       char dbl = end_ch & DOUBLE_CLOSE_CHAR_FLAG;
+       end_ch &= (DOUBLE_CLOSE_CHAR_FLAG-1);
        while (1) {
                int ch = i_getch(input);
                if (ch == EOF) {
-                       syntax_error_unterm_ch(')');
+                       syntax_error_unterm_ch(end_ch);
                        /*xfunc_die(); - redundant */
                }
-               if (ch == '(')
-                       count++;
-               if (ch == ')') {
-                       if (--count < 0) {
-                               if (!dbl)
-                                       break;
-                               if (i_peek(input) == ')') {
-                                       i_getch(input);
-                                       break;
-                               }
+               if (ch == end_ch) {
+                       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_paren(dest, input, ch);
+                       o_addchr(dest, ch);
+                       continue;
+               }
                if (ch == '\'') {
                        add_till_single_quote(dest, input);
                        o_addchr(dest, ch);
@@ -5536,6 +5929,11 @@ static void add_till_closing_paren(o_string *dest, struct in_str *input, bool db
                        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);
@@ -5596,88 +5994,56 @@ static int handle_dollar(o_string *as_string,
        case '@': /* args */
                goto make_one_char_var;
        case '{': {
-               bool first_char, all_digits;
-               int expansion;
+               o_addchr(dest, SPECIAL_VAR_SYMBOL);
 
-               ch = i_getch(input);
+               ch = i_getch(input); /* eat '{' */
                nommu_addchr(as_string, ch);
-               o_addchr(dest, SPECIAL_VAR_SYMBOL);
 
-               /* TODO: maybe someone will try to escape the '}' */
-               expansion = 0;
-               first_char = true;
-               all_digits = false;
+               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("$!?#*@_", ch) && !isalnum(ch)) { /* not one of those */
+ bad_dollar_syntax:
+                       syntax_error_unterm_str("${name}");
+                       debug_printf_parse("handle_dollar return 1: unterminated ${name}\n");
+                       return 1;
+               }
+               ch |= quote_mask;
+
+               /* It's possible to just call add_till_closing_paren() 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... */
+
                while (1) {
+                       o_addchr(dest, ch);
+                       debug_printf_parse(": '%c'\n", ch);
+
                        ch = i_getch(input);
                        nommu_addchr(as_string, ch);
-                       if (ch == '}') {
+                       if (ch == '}')
                                break;
-                       }
-
-                       if (first_char) {
-                               if (ch == '#') {
-                                       /* ${#var}: length of var contents */
-                                       goto char_ok;
-                               }
-                               if (isdigit(ch)) {
-                                       all_digits = true;
-                                       goto char_ok;
-                               }
-                               /* They're being verbose and doing ${?} */
-                               if (i_peek(input) == '}' && strchr("$!?#*@_", ch))
-                                       goto char_ok;
-                       }
 
-                       if (expansion < 2
-                        && (  (all_digits && !isdigit(ch))
-                           || (!all_digits && !isalnum(ch) && ch != '_')
-                           )
-                       ) {
+                       if (!isalnum(ch) && ch != '_') {
                                /* handle parameter expansions
                                 * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02
                                 */
-                               if (first_char)
-                                       goto case_default;
-                               switch (ch) {
-                               case ':': /* null modifier */
-                                       if (expansion == 0) {
-                                               debug_printf_parse(": null modifier\n");
-                                               ++expansion;
-                                               break;
-                                       }
-                                       goto case_default;
-                               case '#': /* remove prefix */
-                               case '%': /* remove suffix */
-                                       if (expansion == 0) {
-                                               debug_printf_parse(": remove suffix/prefix\n");
-                                               expansion = 2;
-                                               break;
-                                       }
-                                       goto case_default;
-                               case '-': /* default value */
-                               case '=': /* assign default */
-                               case '+': /* alternative */
-                               case '?': /* error indicate */
-                                       debug_printf_parse(": parameter expansion\n");
-                                       expansion = 2;
-                                       break;
-                               default:
-                               case_default:
-                                       syntax_error_unterm_str("${name}");
-                                       debug_printf_parse("handle_dollar return 1: unterminated ${name}\n");
-                                       return 1;
-                               }
+                               if (!strchr("%#:-=+?", ch)) /* ${var<bad_char>... */
+                                       goto bad_dollar_syntax;
+                               /* Eat everything until closing '}' */
+                               o_addchr(dest, ch);
+//TODO: add nommu_addchr hack here
+                               add_till_closing_paren(dest, input, '}');
+                               break;
                        }
- char_ok:
-                       debug_printf_parse(": '%c'\n", ch);
-                       o_addchr(dest, ch | quote_mask);
-                       quote_mask = 0;
-                       first_char = false;
-               } /* while (1) */
+               }
                o_addchr(dest, SPECIAL_VAR_SYMBOL);
                break;
        }
-#if (ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_TICK)
+#if ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_TICK
        case '(': {
 # if !BB_MMU
                int pos;
@@ -5693,7 +6059,7 @@ static int handle_dollar(o_string *as_string,
 #  if !BB_MMU
                        pos = dest->length;
 #  endif
-                       add_till_closing_paren(dest, input, true);
+                       add_till_closing_paren(dest, input, ')' | DOUBLE_CLOSE_CHAR_FLAG);
 #  if !BB_MMU
                        if (as_string) {
                                o_addstr(as_string, dest->data + pos);
@@ -5711,11 +6077,11 @@ static int handle_dollar(o_string *as_string,
 #  if !BB_MMU
                pos = dest->length;
 #  endif
-               add_till_closing_paren(dest, input, false);
+               add_till_closing_paren(dest, input, ')');
 #  if !BB_MMU
                if (as_string) {
                        o_addstr(as_string, dest->data + pos);
-                       o_addchr(as_string, '`');
+                       o_addchr(as_string, ')');
                }
 #  endif
                o_addchr(dest, SPECIAL_VAR_SYMBOL);
@@ -5775,7 +6141,7 @@ static int parse_stream_dquoted(o_string *as_string,
        if (ch != '\n') {
                next = i_peek(input);
        }
-       debug_printf_parse(": ch=%c (%d) escape=%d\n",
+       debug_printf_parse("\" ch=%c (%d) escape=%d\n",
                                        ch, ch, dest->o_escape);
        if (ch == '\\') {
                if (next == EOF) {
@@ -5855,9 +6221,14 @@ static struct pipe *parse_stream(char **pstring,
                        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 = " \t\n";
+               G.ifs = defifs;
 
  reset:
 #if ENABLE_HUSH_INTERACTIVE
@@ -5927,10 +6298,29 @@ static struct pipe *parse_stream(char **pstring,
                        return pi;
                }
                nommu_addchr(&ctx.as_string, ch);
+
+               next = '\0';
+               if (ch != '\n')
+                       next = i_peek(input);
+
+               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;
+               }
+               is_special = strchr(is_special, ch);
                is_ifs = strchr(G.ifs, ch);
-               is_special = strchr("<>;&|(){}#'" /* special outside of "str" */
-                               "\\$\"" IF_HUSH_TICK("`") /* always special */
-                               , ch);
 
                if (!is_special && !is_ifs) { /* ordinary char */
  ordinary_char:
@@ -6041,11 +6431,6 @@ static struct pipe *parse_stream(char **pstring,
                if (is_ifs)
                        continue;
 
-               next = '\0';
-               if (ch != '\n') {
-                       next = i_peek(input);
-               }
-
                /* Catch <, > before deciding whether this word is
                 * an assignment. a=1 2>z b=2: b=2 is still assignment */
                switch (ch) {
@@ -6137,12 +6522,13 @@ static struct pipe *parse_stream(char **pstring,
                                /* Example: echo Hello \2>file
                                 * we need to know that word 2 is quoted */
                                dest.o_quoted = 1;
-                       } else {
+                       }
 #if !BB_MMU
+                       else {
                                /* It's "\<newline>". Remove trailing '\' from ctx.as_string */
                                ctx.as_string.data[--ctx.as_string.length] = '\0';
-#endif
                        }
+#endif
                        break;
                case '$':
                        if (handle_dollar(&ctx.as_string, &dest, input) != 0) {
@@ -6249,6 +6635,9 @@ static struct pipe *parse_stream(char **pstring,
                                 * 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);
+#endif
                        }
                        goto new_cmd;
                case '(':
@@ -6344,15 +6733,26 @@ static struct pipe *parse_stream(char **pstring,
  */
 static void parse_and_run_stream(struct in_str *inp, int end_trigger)
 {
+       /* 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 (!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;
        }
 }
 
@@ -6371,10 +6771,17 @@ static void parse_and_run_file(FILE *f)
 }
 
 /* Called a few times only (or even once if "sh -c") */
-static void block_signals(int second_time)
+static void init_sigmasks(void)
 {
        unsigned sig;
        unsigned mask;
+       sigset_t old_blocked_set;
+
+       if (!G.inherited_set_is_saved) {
+               sigprocmask(SIG_SETMASK, NULL, &G.blocked_set);
+               G.inherited_set = G.blocked_set;
+       }
+       old_blocked_set = G.blocked_set;
 
        mask = (1 << SIGQUIT);
        if (G_interactive_fd) {
@@ -6384,8 +6791,6 @@ static void block_signals(int second_time)
        }
        G.non_DFL_mask = mask;
 
-       if (!second_time)
-               sigprocmask(SIG_SETMASK, NULL, &G.blocked_set);
        sig = 0;
        while (mask) {
                if (mask & 1)
@@ -6395,18 +6800,21 @@ static void block_signals(int second_time)
        }
        sigdelset(&G.blocked_set, SIGCHLD);
 
-       sigprocmask(SIG_SETMASK, &G.blocked_set,
-                       second_time ? NULL : &G.inherited_set);
+       if (memcmp(&old_blocked_set, &G.blocked_set, sizeof(old_blocked_set)) != 0)
+               sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
+
        /* POSIX allows shell to re-enable SIGCHLD
         * even if it was SIG_IGN on entry */
 #if ENABLE_HUSH_FAST
        G.count_SIGCHLD++; /* ensure it is != G.handled_SIGCHLD */
-       if (!second_time)
+       if (!G.inherited_set_is_saved)
                signal(SIGCHLD, SIGCHLD_handler);
 #else
-       if (!second_time)
+       if (!G.inherited_set_is_saved)
                signal(SIGCHLD, SIG_DFL);
 #endif
+
+       G.inherited_set_is_saved = 1;
 }
 
 #if ENABLE_HUSH_JOB
@@ -6468,14 +6876,13 @@ int hush_main(int argc, char **argv)
                .flg_export = 1,
                .flg_read_only = 1,
        };
-       int signal_mask_is_inited = 0;
        int opt;
        unsigned builtin_argc;
        char **e;
        struct variable *cur_var;
 
        INIT_G();
-       if (EXIT_SUCCESS) /* if EXIT_SUCCESS == 0, is already done */
+       if (EXIT_SUCCESS) /* if EXIT_SUCCESS == 0, it is already done */
                G.last_exitcode = EXIT_SUCCESS;
 #if !BB_MMU
        G.argv0_for_re_execing = argv[0];
@@ -6559,10 +6966,9 @@ int hush_main(int argc, char **argv)
        }
 
        /* Shell is non-interactive at first. We need to call
-        * block_signals(0) if we are going to execute "sh <script>",
+        * init_sigmasks() if we are going to execute "sh <script>",
         * "sh -c <cmds>" or login shell's /etc/profile and friends.
-        * If we later decide that we are interactive, we run block_signals(0)
-        * (or re-run block_signals(1) if we ran block_signals(0) before)
+        * If we later decide that we are interactive, we run init_sigmasks()
         * in order to intercept (more) signals.
         */
 
@@ -6586,11 +6992,11 @@ int hush_main(int argc, char **argv)
                         * sh ... -c 'script'
                         * sh ... -c 'script' ARG0 [ARG1...]
                         * On NOMMU, if builtin_argc != 0,
-                        * sh ... -c 'builtin' [BARGV...] "" ARG0 [ARG1...]
+                        * sh ... -c 'builtin' BARGV... "" ARG0 [ARG1...]
                         * "" needs to be replaced with NULL
                         * and BARGV vector fed to builtin function.
-                        * Note: this form never happens:
-                        * sh ... -c 'builtin' [BARGV...] ""
+                        * Note: the form without ARG0 never happens:
+                        * sh ... -c 'builtin' BARGV... ""
                         */
                        if (!G.root_pid) {
                                G.root_pid = getpid();
@@ -6602,13 +7008,13 @@ int hush_main(int argc, char **argv)
                                /* -c 'builtin' [BARGV...] "" ARG0 [ARG1...] */
                                const struct built_in_command *x;
 
-                               block_signals(0); /* 0: called 1st time */
+                               init_sigmasks();
                                x = find_builtin(optarg);
                                if (x) { /* paranoia */
                                        G.global_argc -= builtin_argc; /* skip [BARGV...] "" */
                                        G.global_argv += builtin_argc;
                                        G.global_argv[-1] = NULL; /* replace "" */
-                                       G.last_exitcode = x->function(argv + optind - 1);
+                                       G.last_exitcode = x->b_function(argv + optind - 1);
                                }
                                goto final_return;
                        }
@@ -6618,7 +7024,7 @@ int hush_main(int argc, char **argv)
                                G.global_argv[0] = argv[0];
                                G.global_argc--;
                        } /* else -c 'script' ARG0 [ARG1...]: $0 is ARG0 */
-                       block_signals(0); /* 0: called 1st time */
+                       init_sigmasks();
                        parse_and_run_string(optarg);
                        goto final_return;
                case 'i':
@@ -6634,7 +7040,9 @@ int hush_main(int argc, char **argv)
                case '<': /* "big heredoc" support */
                        full_write(STDOUT_FILENO, optarg, strlen(optarg));
                        _exit(0);
-               case '$':
+               case '$': {
+                       unsigned long long empty_trap_mask;
+
                        G.root_pid = bb_strtou(optarg, &optarg, 16);
                        optarg++;
                        G.root_ppid = bb_strtou(optarg, &optarg, 16);
@@ -6644,11 +7052,26 @@ int hush_main(int argc, char **argv)
                        G.last_exitcode = bb_strtou(optarg, &optarg, 16);
                        optarg++;
                        builtin_argc = bb_strtou(optarg, &optarg, 16);
+                       optarg++;
+                       empty_trap_mask = bb_strtoull(optarg, &optarg, 16);
+                       if (empty_trap_mask != 0) {
+                               int sig;
+                               init_sigmasks();
+                               G.traps = xzalloc(sizeof(G.traps[0]) * NSIG);
+                               for (sig = 1; sig < NSIG; sig++) {
+                                       if (empty_trap_mask & (1LL << sig)) {
+                                               G.traps[sig] = xzalloc(1); /* == xstrdup(""); */
+                                               sigaddset(&G.blocked_set, sig);
+                                       }
+                               }
+                               sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
+                       }
 # if ENABLE_HUSH_LOOPS
                        optarg++;
                        G.depth_of_loop = bb_strtou(optarg, &optarg, 16);
 # endif
                        break;
+               }
                case 'R':
                case 'V':
                        set_local_var(xstrdup(optarg), /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ opt == 'R');
@@ -6691,8 +7114,7 @@ int hush_main(int argc, char **argv)
                input = fopen_for_read("/etc/profile");
                if (input != NULL) {
                        close_on_exec_on(fileno(input));
-                       block_signals(0); /* 0: called 1st time */
-                       signal_mask_is_inited = 1;
+                       init_sigmasks();
                        parse_and_run_file(input);
                        fclose(input);
                }
@@ -6717,8 +7139,7 @@ int hush_main(int argc, char **argv)
                G.global_argc = argc - optind;
                input = xfopen_for_read(argv[optind]);
                close_on_exec_on(fileno(input));
-               if (!signal_mask_is_inited)
-                       block_signals(0); /* 0: called 1st time */
+               init_sigmasks();
                parse_and_run_file(input);
 #if ENABLE_FEATURE_CLEAN_UP
                fclose(input);
@@ -6727,7 +7148,7 @@ int hush_main(int argc, char **argv)
        }
 
        /* Up to here, shell was non-interactive. Now it may become one.
-        * NB: don't forget to (re)run block_signals(0/1) as needed.
+        * NB: don't forget to (re)run init_sigmasks() as needed.
         */
 
        /* A shell is interactive if the '-i' flag was given,
@@ -6780,7 +7201,7 @@ int hush_main(int argc, char **argv)
                }
 
                /* Block some signals */
-               block_signals(signal_mask_is_inited);
+               init_sigmasks();
 
                if (G_saved_tty_pgrp) {
                        /* Set other signals to restore saved_tty_pgrp */
@@ -6794,9 +7215,9 @@ int hush_main(int argc, char **argv)
                /* -1 is special - makes xfuncs longjmp, not exit
                 * (we reset die_sleep = 0 whereever we [v]fork) */
                enable_restore_tty_pgrp_on_exit(); /* sets die_sleep = -1 */
-       } else if (!signal_mask_is_inited) {
-               block_signals(0); /* 0: called 1st time */
-       } /* else: block_signals(0) was done before */
+       } else {
+               init_sigmasks();
+       }
 #elif ENABLE_HUSH_INTERACTIVE
        /* No job control compiled in, only prompt/line editing */
        if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO)) {
@@ -6811,15 +7232,11 @@ int hush_main(int argc, char **argv)
        }
        if (G_interactive_fd) {
                close_on_exec_on(G_interactive_fd);
-               block_signals(signal_mask_is_inited);
-       } else if (!signal_mask_is_inited) {
-               block_signals(0);
        }
+       init_sigmasks();
 #else
        /* We have interactiveness code disabled */
-       if (!signal_mask_is_inited) {
-               block_signals(0);
-       }
+       init_sigmasks();
 #endif
        /* bash:
         * if interactive but not a login shell, sources ~/.bashrc
@@ -6882,31 +7299,47 @@ static int FAST_FUNC builtin_true(char **argv UNUSED_PARAM)
        return 0;
 }
 
-static int FAST_FUNC builtin_test(char **argv)
+static int run_applet_main(char **argv, int (*applet_main_func)(int argc, char **argv))
 {
        int argc = 0;
        while (*argv) {
                argc++;
                argv++;
        }
-       return test_main(argc, argv - argc);
+       return applet_main_func(argc, argv - argc);
+}
+
+static int FAST_FUNC builtin_test(char **argv)
+{
+       return run_applet_main(argv, test_main);
 }
 
 static int FAST_FUNC builtin_echo(char **argv)
 {
-       int argc = 0;
-       while (*argv) {
-               argc++;
+       return run_applet_main(argv, echo_main);
+}
+
+#if ENABLE_PRINTF
+static int FAST_FUNC builtin_printf(char **argv)
+{
+       return run_applet_main(argv, printf_main);
+}
+#endif
+
+static char **skip_dash_dash(char **argv)
+{
+       argv++;
+       if (argv[0] && argv[0][0] == '-' && argv[0][1] == '-' && argv[0][2] == '\0')
                argv++;
-       }
-       return echo_main(argc, argv - argc);
+       return argv;
 }
 
 static int FAST_FUNC builtin_eval(char **argv)
 {
        int rcode = EXIT_SUCCESS;
 
-       if (*++argv) {
+       argv = skip_dash_dash(argv);
+       if (*argv) {
                char *str = expand_strvec_to_string(argv);
                /* bash:
                 * eval "echo Hi; done" ("done" is syntax error):
@@ -6921,7 +7354,10 @@ static int FAST_FUNC builtin_eval(char **argv)
 
 static int FAST_FUNC builtin_cd(char **argv)
 {
-       const char *newdir = argv[1];
+       const char *newdir;
+
+       argv = skip_dash_dash(argv);
+       newdir = argv[0];
        if (newdir == NULL) {
                /* bash does nothing (exitcode 0) if HOME is ""; if it's unset,
                 * bash says "bash: cd: HOME not set" and does nothing
@@ -6945,16 +7381,20 @@ static int FAST_FUNC builtin_cd(char **argv)
 
 static int FAST_FUNC builtin_exec(char **argv)
 {
-       if (*++argv == NULL)
+       argv = skip_dash_dash(argv);
+       if (argv[0] == NULL)
                return EXIT_SUCCESS; /* bash does this */
-       {
-#if !BB_MMU
-               nommu_save_t dummy;
-#endif
-               /* TODO: if exec fails, bash does NOT exit! We do... */
-               pseudo_exec_argv(&dummy, argv, 0, NULL);
-               /* never returns */
-       }
+
+       /* Careful: we can end up here after [v]fork. Do not restore
+        * tty pgrp then, only top-level shell process does that */
+       if (G_saved_tty_pgrp && getpid() == G.root_pid)
+               tcsetpgrp(G_interactive_fd, G_saved_tty_pgrp);
+
+       /* TODO: if exec fails, bash does NOT exit! We do.
+        * We'll need to undo sigprocmask (it's inside execvp_or_die)
+        * and tcsetpgrp, and this is inherently racy.
+        */
+       execvp_or_die(argv);
 }
 
 static int FAST_FUNC builtin_exit(char **argv)
@@ -6975,12 +7415,13 @@ static int FAST_FUNC builtin_exit(char **argv)
         */
 
        /* note: EXIT trap is run by hush_exit */
-       if (*++argv == NULL)
+       argv = skip_dash_dash(argv);
+       if (argv[0] == NULL)
                hush_exit(G.last_exitcode);
        /* mimic bash: exit 123abc == exit 255 + error msg */
        xfunc_error_retval = 255;
        /* bash: exit -2 == exit 254, no error msg */
-       hush_exit(xatoi(*argv) & 0xff);
+       hush_exit(xatoi(argv[0]) & 0xff);
 }
 
 static void print_escaped(const char *s)
@@ -7087,7 +7528,7 @@ static int FAST_FUNC builtin_export(char **argv)
                                putchar('\n');
 #endif
                        }
-                       /*fflush(stdout); - done after each builtin anyway */
+                       /*fflush_all(); - done after each builtin anyway */
                }
                return EXIT_SUCCESS;
        }
@@ -7132,7 +7573,7 @@ static int FAST_FUNC builtin_trap(char **argv)
                                printf(" %s\n", get_signame(i));
                        }
                }
-               /*fflush(stdout); - done after each builtin anyway */
+               /*fflush_all(); - done after each builtin anyway */
                return EXIT_SUCCESS;
        }
 
@@ -7155,7 +7596,7 @@ static int FAST_FUNC builtin_trap(char **argv)
                        free(G.traps[sig]);
                        G.traps[sig] = xstrdup(new_cmd);
 
-                       debug_printf("trap: setting SIG%s (%i) to '%s'",
+                       debug_printf("trap: setting SIG%s (%i) to '%s'\n",
                                get_signame(sig), sig, G.traps[sig]);
 
                        /* There is no signal for 0 (EXIT) */
@@ -7308,8 +7749,8 @@ static int FAST_FUNC builtin_help(char **argv UNUSED_PARAM)
                "Built-in commands:\n"
                "------------------\n");
        for (x = bltins1; x != &bltins1[ARRAY_SIZE(bltins1)]; x++) {
-               if (x->descr)
-                       printf("%s\t%s\n", x->cmd, x->descr);
+               if (x->b_descr)
+                       printf("%-10s%s\n", x->b_cmd, x->b_descr);
        }
        bb_putchar('\n');
        return EXIT_SUCCESS;
@@ -7340,10 +7781,10 @@ static int FAST_FUNC builtin_memleak(char **argv UNUSED_PARAM)
        void *p;
        unsigned long l;
 
-#ifdef M_TRIM_THRESHOLD
+# ifdef M_TRIM_THRESHOLD
        /* Optional. Reduces probability of false positives */
        malloc_trim(0);
-#endif
+# endif
        /* Crude attempt to find where "free memory" starts,
         * sans fragmentation. */
        p = malloc(240);
@@ -7376,24 +7817,37 @@ static int FAST_FUNC builtin_pwd(char **argv UNUSED_PARAM)
 
 static int FAST_FUNC builtin_read(char **argv)
 {
-       char *string;
-       const char *name = "REPLY";
+       const char *r;
+       char *opt_n = NULL;
+       char *opt_p = NULL;
+       char *opt_t = NULL;
+       char *opt_u = NULL;
+       int read_flags;
 
-       if (argv[1]) {
-               name = argv[1];
-               /* bash (3.2.33(1)) bug: "read 0abcd" will execute,
-                * and _after_ that_ it will complain */
-               if (!is_well_formed_var_name(name, '\0')) {
-                       /* Mimic bash message */
-                       bb_error_msg("read: '%s': not a valid identifier", name);
-                       return 1;
-               }
-       }
+       /* "!": do not abort on errors.
+        * Option string must start with "sr" to match BUILTIN_READ_xxx
+        */
+       read_flags = getopt32(argv, "!srn:p:t:u:", &opt_n, &opt_p, &opt_t, &opt_u);
+       if (read_flags == (uint32_t)-1)
+               return EXIT_FAILURE;
+       argv += optind;
 
-//TODO: bash unbackslashes input, splits words and puts them in argv[i]
+       r = shell_builtin_read(set_local_var_from_halves,
+               argv,
+               get_local_var_value("IFS"), /* can be NULL */
+               read_flags,
+               opt_n,
+               opt_p,
+               opt_t,
+               opt_u
+       );
 
-       string = xmalloc_reads(STDIN_FILENO, xasprintf("%s=", name), NULL);
-       return set_local_var(string, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+       if ((uintptr_t)r > 1) {
+               bb_error_msg("%s", r);
+               r = (char*)(uintptr_t)1;
+       }
+
+       return (uintptr_t)r;
 }
 
 /* http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#set
@@ -7479,8 +7933,9 @@ static int FAST_FUNC builtin_set(char **argv)
 static int FAST_FUNC builtin_shift(char **argv)
 {
        int n = 1;
-       if (argv[1]) {
-               n = atoi(argv[1]);
+       argv = skip_dash_dash(argv);
+       if (argv[0]) {
+               n = atoi(argv[0]);
        }
        if (n >= 0 && n < G.global_argc) {
                if (G.global_args_malloced) {
@@ -7498,21 +7953,27 @@ static int FAST_FUNC builtin_shift(char **argv)
 
 static int FAST_FUNC builtin_source(char **argv)
 {
-       char *arg_path;
+       char *arg_path, *filename;
        FILE *input;
        save_arg_t sv;
 #if ENABLE_HUSH_FUNCTIONS
        smallint sv_flg;
 #endif
 
-       if (*++argv == NULL)
-               return EXIT_FAILURE;
-
-       if (strchr(*argv, '/') == NULL && (arg_path = find_in_path(*argv)) != NULL) {
-               input = fopen_for_read(arg_path);
-               free(arg_path);
-       } else
-               input = fopen_or_warn(*argv, "r");
+       argv = skip_dash_dash(argv);
+       filename = argv[0];
+       if (!filename) {
+               /* bash says: "bash: .: filename argument required" */
+               return 2; /* bash compat */
+       }
+       arg_path = NULL;
+       if (!strchr(filename, '/')) {
+               arg_path = find_in_path(filename);
+               if (arg_path)
+                       filename = arg_path;
+       }
+       input = fopen_or_warn(filename, "r");
+       free(arg_path);
        if (!input) {
                /* bb_perror_msg("%s", *argv); - done by fopen_or_warn */
                return EXIT_FAILURE;
@@ -7543,11 +8004,12 @@ static int FAST_FUNC builtin_umask(char **argv)
        mode_t mask;
 
        mask = umask(0);
-       if (argv[1]) {
+       argv = skip_dash_dash(argv);
+       if (argv[0]) {
                mode_t old_mask = mask;
 
                mask ^= 0777;
-               rc = bb_parse_mode(argv[1], &mask);
+               rc = bb_parse_mode(argv[0], &mask);
                mask ^= 0777;
                if (rc == 0) {
                        mask = old_mask;
@@ -7555,7 +8017,7 @@ static int FAST_FUNC builtin_umask(char **argv)
                         * bash: umask: 'q': invalid symbolic mode operator
                         * bash: umask: 999: octal number out of range
                         */
-                       bb_error_msg("%s: '%s' invalid mode", argv[0], argv[1]);
+                       bb_error_msg("%s: invalid mode '%s'", "umask", argv[0]);
                }
        } else {
                rc = 1;
@@ -7611,7 +8073,8 @@ static int FAST_FUNC builtin_wait(char **argv)
        int ret = EXIT_SUCCESS;
        int status, sig;
 
-       if (*++argv == NULL) {
+       argv = skip_dash_dash(argv);
+       if (argv[0] == NULL) {
                /* Don't care about wait results */
                /* Note 1: must wait until there are no more children */
                /* Note 2: must be interruptible */