hush: fix more obscure ${var%...} cases
[oweals/busybox.git] / shell / hush.c
index 8baccf246c41a42b4a92c25b3f5ff14e6d1ae0f7..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
- *      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 compat TODO:
  *      redirection of stdout+stderr: &> and >&
  *      brace expansion: one/{two,three,four}
  *      reserved words: function select
  *      advanced test: [[ ]]
- *      substrings: ${var:1:5}
  *      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 "EXPR".
+ *          The EXPR is evaluated according to ARITHMETIC EVALUATION.
+ *          This is exactly equivalent to let "EXPR".
  *      $[EXPR]: synonym for $((EXPR))
- *
- * TODOs:
- *      grep for "TODO" and fix (some of them are easy)
- *      special variables (done: PWD, PPID, RANDOM)
- *      follow IFS rules more precisely, including update semantics
  *      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"
@@ -2404,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
@@ -2428,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;
@@ -2523,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";
@@ -2568,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';
                                }
                        }
 
@@ -2611,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;
                                                }
@@ -2666,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
@@ -2692,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;
@@ -4117,9 +4199,9 @@ 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);
-//FIXME: "false; q=`false`; echo $?" should print 1
                        restore_redirects(squirrel);
                        /* Set shell variables */
                        while (*argv) {
@@ -4129,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"
                         */
@@ -4186,7 +4273,7 @@ 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]);
+                                               x->b_cmd, argv_expanded[1]);
                                        rcode = x->b_function(argv_expanded) & 0xff;
                                        fflush_all();
                                }
@@ -5801,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);
@@ -5834,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);
@@ -5894,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;
        }
@@ -5991,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);
@@ -6009,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);
@@ -6814,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];
@@ -7258,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):
@@ -7277,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
@@ -7301,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
@@ -7334,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)
@@ -7668,7 +7750,7 @@ static int FAST_FUNC builtin_help(char **argv UNUSED_PARAM)
                "------------------\n");
        for (x = bltins1; x != &bltins1[ARRAY_SIZE(bltins1)]; x++) {
                if (x->b_descr)
-                       printf("%s\t%s\n", x->b_cmd, x->b_descr);
+                       printf("%-10s%s\n", x->b_cmd, x->b_descr);
        }
        bb_putchar('\n');
        return EXIT_SUCCESS;
@@ -7851,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) {
@@ -7877,12 +7960,13 @@ static int FAST_FUNC builtin_source(char **argv)
        smallint sv_flg;
 #endif
 
-       arg_path = NULL;
-       filename = *++argv;
+       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)
@@ -7920,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;
@@ -7932,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;
@@ -7988,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 */