hush: fix more obscure ${var%...} cases
[oweals/busybox.git] / shell / hush.c
index 9a9b5bb91c2a8a2acb5d908f08ad1cd2e006e291..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"
@@ -87,6 +90,7 @@
 
 #include "shell_common.h"
 #include "builtin_read.h"
+#include "builtin_ulimit.h"
 #include "math.h"
 #include "match.h"
 #if ENABLE_HUSH_RANDOM_SUPPORT
 # 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
@@ -621,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 }
@@ -669,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"),
@@ -2037,7 +2048,7 @@ static const char *next_brace_sub(const char *cp)
                                break;
                        cp++;
                        continue;
-               }
+               }
                 /*{*/ if ((*cp == '}' && depth-- == 0) || (*cp == ',' && depth == 0))
                        break;
                if (*cp++ == '{') /*}*/
@@ -2394,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
@@ -2418,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;
@@ -2513,27 +2541,19 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
                         * and $IFS-splitted */
                        debug_printf_subst("SUBST '%s' first_ch %x\n", arg, first_ch);
                        G.last_exitcode = process_command_subs(&subst_result, arg);
-                       debug_printf_subst("SUBST RES '%s'\n", subst_result.data);
+                       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 = set_local_var_from_halves;
-                       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";
@@ -2558,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';
                                }
                        }
 
@@ -2601,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;
                                                }
@@ -2656,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
@@ -2682,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;
@@ -3336,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;
                }
@@ -3547,7 +3639,7 @@ static void exec_builtin(char ***to_free,
                char **argv)
 {
 #if BB_MMU
-       int rcode = x->function(argv);
+       int rcode = x->b_function(argv);
        fflush_all();
        _exit(rcode);
 #else
@@ -4107,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 */
@@ -4118,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"
                         */
@@ -4159,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;
@@ -4175,8 +4273,8 @@ 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;
+                                               x->b_cmd, argv_expanded[1]);
+                                       rcode = x->b_function(argv_expanded) & 0xff;
                                        fflush_all();
                                }
 #if ENABLE_HUSH_FUNCTIONS
@@ -5790,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);
@@ -5823,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);
@@ -5883,84 +5994,52 @@ 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;
        }
@@ -5980,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);
@@ -5998,7 +6077,7 @@ 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);
@@ -6227,10 +6306,15 @@ static struct pipe *parse_stream(char **pstring,
                is_special = "{}<>;&|()#'" /* special outside of "str" */
                                "\\$\"" IF_HUSH_TICK("`"); /* always special */
                /* Are { and } special here? */
-               if (ctx.command->argv /* word [word]{... */
-                || dest.length /* word{... */
-                || dest.o_quoted /* ""{... */
-                || (next != ';' && next != ')' && !strchr(G.ifs, next)) /* {word */
+               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;
@@ -6798,7 +6882,7 @@ int hush_main(int argc, char **argv)
        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];
@@ -6908,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();
@@ -6930,7 +7014,7 @@ int hush_main(int argc, char **argv)
                                        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;
                        }
@@ -7242,11 +7326,20 @@ static int FAST_FUNC builtin_printf(char **argv)
 }
 #endif
 
+static char **skip_dash_dash(char **argv)
+{
+       argv++;
+       if (argv[0] && argv[0][0] == '-' && argv[0][1] == '-' && argv[0][2] == '\0')
+               argv++;
+       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):
@@ -7261,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
@@ -7285,7 +7381,8 @@ 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 */
 
        /* Careful: we can end up here after [v]fork. Do not restore
@@ -7318,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)
@@ -7651,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;
@@ -7835,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) {
@@ -7854,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;
@@ -7899,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;
@@ -7911,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;
@@ -7967,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 */