hush: move variable expansion into a separate function. No logic changes
authorDenys Vlasenko <dvlasenk@redhat.com>
Sun, 5 Sep 2010 12:48:11 +0000 (14:48 +0200)
committerDenys Vlasenko <dvlasenk@redhat.com>
Sun, 5 Sep 2010 12:48:11 +0000 (14:48 +0200)
function                                             old     new   delta
expand_one_var                                         -    1551   +1551
expand_vars_to_list                                 2833    1175   -1658
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 0/1 up/down: 1551/-1658)       Total: -107 bytes

Signed-off-by: Denys Vlasenko <dvlasenk@redhat.com>
shell/hush.c

index 9a08e90c97f46f56afe97c2f79c467310fbd1e54..821a7a77f38d4dcb997926e27bafc9dc82292e44 100644 (file)
@@ -2664,6 +2664,263 @@ static char *replace_pattern(char *val, const char *pattern, const char *repl, c
 }
 #endif
 
+/* Helper:
+ * Handles <SPECIAL_VAR_SYMBOL>varname...<SPECIAL_VAR_SYMBOL> construct.
+ */
+static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, char **pp, char first_ch)
+{
+       const char *val = NULL;
+       char *to_be_freed = NULL;
+       char *p = *pp;
+       char *var;
+       char first_char;
+       char exp_op;
+       char exp_save = exp_save; /* for compiler */
+       char *exp_saveptr; /* points to expansion operator */
+       char *exp_word = exp_word; /* for compiler */
+
+       var = arg;
+       *p = '\0';
+       exp_saveptr = arg[1] ? strchr(VAR_ENCODED_SUBST_OPS, arg[1]) : NULL;
+       first_char = arg[0] = first_ch & 0x7f;
+       exp_op = 0;
+
+       if (first_char == '#' && arg[1] && !exp_saveptr) {
+               /* handle length expansion ${#var} */
+               var++;
+               exp_op = 'L';
+       } else {
+               /* maybe handle parameter expansion */
+               if (exp_saveptr /* if 2nd char is one of expansion operators */
+                && strchr(NUMERIC_SPECVARS_STR, first_char) /* 1st char is special variable */
+               ) {
+                       /* ${?:0}, ${#[:]%0} etc */
+                       exp_saveptr = var + 1;
+               } else {
+                       /* ${?}, ${var}, ${var:0}, ${var[:]%0} etc */
+                       exp_saveptr = var+1 + strcspn(var+1, VAR_ENCODED_SUBST_OPS);
+               }
+               exp_op = exp_save = *exp_saveptr;
+               if (exp_op) {
+                       exp_word = exp_saveptr + 1;
+                       if (exp_op == ':') {
+                               exp_op = *exp_word++;
+                               if (ENABLE_HUSH_BASH_COMPAT
+                                && (exp_op == '\0' || !strchr(MINUS_PLUS_EQUAL_QUESTION, exp_op))
+                               ) {
+                                       /* oops... it's ${var:N[:M]}, not ${var:?xxx} or some such */
+                                       exp_op = ':';
+                                       exp_word--;
+                               }
+                       }
+                       *exp_saveptr = '\0';
+               } /* else: it's not an expansion op, but bare ${var} */
+       }
+
+       /* lookup the variable in question */
+       if (isdigit(var[0])) {
+               /* parse_dollar() should have vetted var for us */
+               int n = xatoi_positive(var);
+               if (n < G.global_argc)
+                       val = G.global_argv[n];
+               /* else val remains NULL: $N with too big N */
+       } else {
+               switch (var[0]) {
+               case '$': /* pid */
+                       val = utoa(G.root_pid);
+                       break;
+               case '!': /* bg pid */
+                       val = G.last_bg_pid ? utoa(G.last_bg_pid) : "";
+                       break;
+               case '?': /* exitcode */
+                       val = utoa(G.last_exitcode);
+                       break;
+               case '#': /* argc */
+                       val = utoa(G.global_argc ? G.global_argc-1 : 0);
+                       break;
+               default:
+                       val = get_local_var_value(var);
+               }
+       }
+
+       /* Handle any expansions */
+       if (exp_op == 'L') {
+               debug_printf_expand("expand: length(%s)=", val);
+               val = utoa(val ? strlen(val) : 0);
+               debug_printf_expand("%s\n", val);
+       } else if (exp_op) {
+               if (exp_op == '%' || exp_op == '#') {
+                       /* Standard-mandated substring removal ops:
+                        * ${parameter%word} - remove smallest suffix pattern
+                        * ${parameter%%word} - remove largest suffix pattern
+                        * ${parameter#word} - remove smallest prefix pattern
+                        * ${parameter##word} - remove largest prefix pattern
+                        *
+                        * Word is expanded to produce a glob pattern.
+                        * Then var's value is matched to it and matching part removed.
+                        */
+                       if (val && val[0]) {
+                               char *exp_exp_word;
+                               char *loc;
+                               unsigned scan_flags = pick_scan(exp_op, *exp_word);
+                               if (exp_op == *exp_word)        /* ## or %% */
+                                       exp_word++;
+//TODO: avoid xstrdup unless needed
+// (see HACK ALERT below)
+                               val = to_be_freed = xstrdup(val);
+                               exp_exp_word = expand_pseudo_dquoted(exp_word);
+                               if (exp_exp_word)
+                                       exp_word = exp_exp_word;
+                               loc = scan_and_match(to_be_freed, exp_word, scan_flags);
+                               //bb_error_msg("op:%c str:'%s' pat:'%s' res:'%s'",
+                               //              exp_op, to_be_freed, exp_word, loc);
+                               free(exp_exp_word);
+                               if (loc) { /* match was found */
+                                       if (scan_flags & SCAN_MATCH_LEFT_HALF) /* #[#] */
+                                               val = loc;
+                                       else /* %[%] */
+                                               *loc = '\0';
+                               }
+                       }
+               }
+#if ENABLE_HUSH_BASH_COMPAT
+               else if (exp_op == '/' || exp_op == '\\') {
+                       /* Empty variable always gives nothing: */
+                       // "v=''; echo ${v/*/w}" prints ""
+                       if (val && val[0]) {
+                               /* It's ${var/[/]pattern[/repl]} thing */
+                               char *pattern, *repl, *t;
+                               pattern = expand_pseudo_dquoted(exp_word);
+                               if (!pattern)
+                                       pattern = xstrdup(exp_word);
+                               debug_printf_varexp("pattern:'%s'->'%s'\n", exp_word, pattern);
+                               *p++ = SPECIAL_VAR_SYMBOL;
+                               exp_word = p;
+                               p = strchr(p, SPECIAL_VAR_SYMBOL);
+                               *p = '\0';
+                               repl = expand_pseudo_dquoted(exp_word);
+                               debug_printf_varexp("repl:'%s'->'%s'\n", exp_word, repl);
+                               /* HACK ALERT. We depend here on the fact that
+                                * G.global_argv and results of utoa and get_local_var_value
+                                * are actually in writable memory:
+                                * replace_pattern momentarily stores NULs there. */
+                               t = (char*)val;
+                               to_be_freed = replace_pattern(t,
+                                               pattern,
+                                               (repl ? repl : exp_word),
+                                               exp_op);
+                               if (to_be_freed) /* at least one replace happened */
+                                       val = to_be_freed;
+                               free(pattern);
+                               free(repl);
+                       }
+               }
+#endif
+               else if (exp_op == ':') {
+#if ENABLE_HUSH_BASH_COMPAT && ENABLE_SH_MATH_SUPPORT
+                       /* It's ${var:N[:M]} bashism.
+                        * Note that in encoded form it has TWO parts:
+                        * var:N<SPECIAL_VAR_SYMBOL>M<SPECIAL_VAR_SYMBOL>
+                        */
+                       arith_t beg, len;
+                       int errcode = 0;
+
+                       beg = expand_and_evaluate_arith(exp_word, &errcode);
+                       debug_printf_varexp("beg:'%s'=%lld\n", exp_word, (long long)beg);
+                       *p++ = SPECIAL_VAR_SYMBOL;
+                       exp_word = p;
+                       p = strchr(p, SPECIAL_VAR_SYMBOL);
+                       *p = '\0';
+                       len = expand_and_evaluate_arith(exp_word, &errcode);
+                       debug_printf_varexp("len:'%s'=%lld\n", exp_word, (long long)len);
+
+                       if (errcode >= 0 && len >= 0) { /* bash compat: len < 0 is illegal */
+                               if (beg < 0) /* bash compat */
+                                       beg = 0;
+                               debug_printf_varexp("from val:'%s'\n", val);
+                               if (len == 0 || !val || beg >= strlen(val))
+                                       val = "";
+                               else {
+                                       /* Paranoia. What if user entered 9999999999999
+                                        * which fits in arith_t but not int? */
+                                       if (len >= INT_MAX)
+                                               len = INT_MAX;
+                                       val = to_be_freed = xstrndup(val + beg, len);
+                               }
+                               debug_printf_varexp("val:'%s'\n", val);
+                       } else
+#endif
+                       {
+                               die_if_script("malformed ${%s:...}", var);
+                               val = "";
+                       }
+               } else { /* one of "-=+?" */
+                       /* Standard-mandated substitution ops:
+                        * ${var?word} - indicate error if unset
+                        *      If var is unset, word (or a message indicating it is unset
+                        *      if word is null) is written to standard error
+                        *      and the shell exits with a non-zero exit status.
+                        *      Otherwise, the value of var is substituted.
+                        * ${var-word} - use default value
+                        *      If var is unset, word is substituted.
+                        * ${var=word} - assign and use default value
+                        *      If var is unset, word is assigned to var.
+                        *      In all cases, final value of var is substituted.
+                        * ${var+word} - use alternative value
+                        *      If var is unset, null is substituted.
+                        *      Otherwise, word is substituted.
+                        *
+                        * Word is subjected to tilde expansion, parameter expansion,
+                        * command substitution, and arithmetic expansion.
+                        * If word is not needed, it is not expanded.
+                        *
+                        * Colon forms (${var:-word}, ${var:=word} etc) do the same,
+                        * but also treat null var as if it is unset.
+                        */
+                       int use_word = (!val || ((exp_save == ':') && !val[0]));
+                       if (exp_op == '+')
+                               use_word = !use_word;
+                       debug_printf_expand("expand: op:%c (null:%s) test:%i\n", exp_op,
+                                       (exp_save == ':') ? "true" : "false", use_word);
+                       if (use_word) {
+                               to_be_freed = expand_pseudo_dquoted(exp_word);
+                               if (to_be_freed)
+                                       exp_word = to_be_freed;
+                               if (exp_op == '?') {
+                                       /* mimic bash message */
+                                       die_if_script("%s: %s",
+                                               var,
+                                               exp_word[0] ? exp_word : "parameter null or not set"
+                                       );
+//TODO: how interactive bash aborts expansion mid-command?
+                               } else {
+                                       val = exp_word;
+                               }
+
+                               if (exp_op == '=') {
+                                       /* ${var=[word]} or ${var:=[word]} */
+                                       if (isdigit(var[0]) || var[0] == '#') {
+                                               /* mimic bash message */
+                                               die_if_script("$%s: cannot assign in this way", var);
+                                               val = NULL;
+                                       } else {
+                                               char *new_var = xasprintf("%s=%s", var, val);
+                                               set_local_var(new_var, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+                                       }
+                               }
+                       }
+               } /* one of "-=+?" */
+
+               *exp_saveptr = exp_save;
+       } /* if (exp_op) */
+
+       arg[0] = first_ch;
+
+       *pp = p;
+       *to_be_freed_pp = to_be_freed;
+       return val;
+}
+
 /* Expand all variable references in given string, adding words to list[]
  * at n, n+1,... positions. Return updated n (so that list[n] is next one
  * to be filled). This routine is extremely tricky: has to deal with
@@ -2803,255 +3060,12 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
                        break;
                }
 #endif
-               default: { /* <SPECIAL_VAR_SYMBOL>varname<SPECIAL_VAR_SYMBOL> */
-//TODO: move to a subroutine?
-                       char *var;
-                       char first_char;
-                       char exp_op;
-                       char exp_save = exp_save; /* for compiler */
-                       char *exp_saveptr; /* points to expansion operator */
-                       char *exp_word = exp_word; /* for compiler */
-
-                       var = arg;
-                       *p = '\0';
-                       exp_saveptr = arg[1] ? strchr(VAR_ENCODED_SUBST_OPS, arg[1]) : NULL;
-                       first_char = arg[0] = first_ch & 0x7f;
-                       exp_op = 0;
-
-                       if (first_char == '#' && arg[1] && !exp_saveptr) {
-                               /* handle length expansion ${#var} */
-                               var++;
-                               exp_op = 'L';
-                       } else {
-                               /* maybe handle parameter expansion */
-                               if (exp_saveptr /* if 2nd char is one of expansion operators */
-                                && strchr(NUMERIC_SPECVARS_STR, first_char) /* 1st char is special variable */
-                               ) {
-                                       /* ${?:0}, ${#[:]%0} etc */
-                                       exp_saveptr = var + 1;
-                               } else {
-                                       /* ${?}, ${var}, ${var:0}, ${var[:]%0} etc */
-                                       exp_saveptr = var+1 + strcspn(var+1, VAR_ENCODED_SUBST_OPS);
-                               }
-                               exp_op = exp_save = *exp_saveptr;
-                               if (exp_op) {
-                                       exp_word = exp_saveptr + 1;
-                                       if (exp_op == ':') {
-                                               exp_op = *exp_word++;
-                                               if (ENABLE_HUSH_BASH_COMPAT
-                                                && (exp_op == '\0' || !strchr(MINUS_PLUS_EQUAL_QUESTION, exp_op))
-                                               ) {
-                                                       /* oops... it's ${var:N[:M]}, not ${var:?xxx} or some such */
-                                                       exp_op = ':';
-                                                       exp_word--;
-                                               }
-                                       }
-                                       *exp_saveptr = '\0';
-                               } /* else: it's not an expansion op, but bare ${var} */
-                       }
-
-                       /* lookup the variable in question */
-                       if (isdigit(var[0])) {
-                               /* parse_dollar() should have vetted var for us */
-                               i = xatoi_positive(var);
-                               if (i < G.global_argc)
-                                       val = G.global_argv[i];
-                               /* else val remains NULL: $N with too big N */
-                       } else {
-                               switch (var[0]) {
-                               case '$': /* pid */
-                                       val = utoa(G.root_pid);
-                                       break;
-                               case '!': /* bg pid */
-                                       val = G.last_bg_pid ? utoa(G.last_bg_pid) : "";
-                                       break;
-                               case '?': /* exitcode */
-                                       val = utoa(G.last_exitcode);
-                                       break;
-                               case '#': /* argc */
-                                       val = utoa(G.global_argc ? G.global_argc-1 : 0);
-                                       break;
-                               default:
-                                       val = get_local_var_value(var);
-                               }
-                       }
-
-                       /* handle any expansions */
-                       if (exp_op == 'L') {
-                               debug_printf_expand("expand: length(%s)=", val);
-                               val = utoa(val ? strlen(val) : 0);
-                               debug_printf_expand("%s\n", val);
-                       } else if (exp_op) {
-                               if (exp_op == '%' || exp_op == '#') {
-       /* Standard-mandated substring removal ops:
-        * ${parameter%word} - remove smallest suffix pattern
-        * ${parameter%%word} - remove largest suffix pattern
-        * ${parameter#word} - remove smallest prefix pattern
-        * ${parameter##word} - remove largest prefix pattern
-        *
-        * Word is expanded to produce a glob pattern.
-        * Then var's value is matched to it and matching part removed.
-        */
-                                       if (val) {
-                                               char *exp_exp_word;
-                                               char *loc;
-                                               unsigned scan_flags = pick_scan(exp_op, *exp_word);
-                                               if (exp_op == *exp_word)        /* ## or %% */
-                                                       exp_word++;
-                                               val = to_be_freed = xstrdup(val);
-                                               exp_exp_word = expand_pseudo_dquoted(exp_word);
-                                               if (exp_exp_word)
-                                                       exp_word = exp_exp_word;
-                                               loc = scan_and_match(to_be_freed, exp_word, scan_flags);
-                                               //bb_error_msg("op:%c str:'%s' pat:'%s' res:'%s'",
-                                               //              exp_op, to_be_freed, exp_word, loc);
-                                               free(exp_exp_word);
-                                               if (loc) { /* match was found */
-                                                       if (scan_flags & SCAN_MATCH_LEFT_HALF) /* #[#] */
-                                                               val = loc;
-                                                       else /* %[%] */
-                                                               *loc = '\0';
-                                               }
-                                       }
-                               }
-#if ENABLE_HUSH_BASH_COMPAT
-                               else if (exp_op == '/' || exp_op == '\\') {
-                                       /* Empty variable always gives nothing: */
-                                       // "v=''; echo ${v/*/w}" prints ""
-                                       if (val && val[0]) {
-                                               /* It's ${var/[/]pattern[/repl]} thing */
-                                               char *pattern, *repl, *t;
-                                               pattern = expand_pseudo_dquoted(exp_word);
-                                               if (!pattern)
-                                                       pattern = xstrdup(exp_word);
-                                               debug_printf_varexp("pattern:'%s'->'%s'\n", exp_word, pattern);
-                                               *p++ = SPECIAL_VAR_SYMBOL;
-                                               exp_word = p;
-                                               p = strchr(p, SPECIAL_VAR_SYMBOL);
-                                               *p = '\0';
-                                               repl = expand_pseudo_dquoted(exp_word);
-                                               debug_printf_varexp("repl:'%s'->'%s'\n", exp_word, repl);
-                                               /* HACK ALERT. We depend here on the fact that
-                                                * G.global_argv and results of utoa and get_local_var_value
-                                                * are actually in writable memory:
-                                                * replace_pattern momentarily stores NULs there. */
-                                               t = (char*)val;
-                                               to_be_freed = replace_pattern(t,
-                                                               pattern,
-                                                               (repl ? repl : exp_word),
-                                                               exp_op);
-                                               if (to_be_freed) /* at least one replace happened */
-                                                       val = to_be_freed;
-                                               free(pattern);
-                                               free(repl);
-                                       }
-                               }
-#endif
-                               else if (exp_op == ':') {
-#if ENABLE_HUSH_BASH_COMPAT && ENABLE_SH_MATH_SUPPORT
-       /* It's ${var:N[:M]} bashism.
-        * Note that in encoded form it has TWO parts:
-        * var:N<SPECIAL_VAR_SYMBOL>M<SPECIAL_VAR_SYMBOL>
-        */
-                                       arith_t beg, len;
-                                       int errcode = 0;
-
-                                       beg = expand_and_evaluate_arith(exp_word, &errcode);
-                                       debug_printf_varexp("beg:'%s'=%lld\n", exp_word, (long long)beg);
-                                       *p++ = SPECIAL_VAR_SYMBOL;
-                                       exp_word = p;
-                                       p = strchr(p, SPECIAL_VAR_SYMBOL);
-                                       *p = '\0';
-                                       len = expand_and_evaluate_arith(exp_word, &errcode);
-                                       debug_printf_varexp("len:'%s'=%lld\n", exp_word, (long long)len);
-
-                                       if (errcode >= 0 && len >= 0) { /* bash compat: len < 0 is illegal */
-                                               if (beg < 0) /* bash compat */
-                                                       beg = 0;
-                                               debug_printf_varexp("from val:'%s'\n", val);
-                                               if (len == 0 || !val || beg >= strlen(val))
-                                                       val = "";
-                                               else {
-                                                       /* Paranoia. What if user entered 9999999999999
-                                                        * which fits in arith_t but not int? */
-                                                       if (len >= INT_MAX)
-                                                               len = INT_MAX;
-                                                       val = to_be_freed = xstrndup(val + beg, len);
-                                               }
-                                               debug_printf_varexp("val:'%s'\n", val);
-                                       } else
-#endif
-                                       {
-                                               die_if_script("malformed ${%s:...}", var);
-                                               val = "";
-                                       }
-                               } else { /* one of "-=+?" */
-       /* Standard-mandated substitution ops:
-        * ${var?word} - indicate error if unset
-        *      If var is unset, word (or a message indicating it is unset
-        *      if word is null) is written to standard error
-        *      and the shell exits with a non-zero exit status.
-        *      Otherwise, the value of var is substituted.
-        * ${var-word} - use default value
-        *      If var is unset, word is substituted.
-        * ${var=word} - assign and use default value
-        *      If var is unset, word is assigned to var.
-        *      In all cases, final value of var is substituted.
-        * ${var+word} - use alternative value
-        *      If var is unset, null is substituted.
-        *      Otherwise, word is substituted.
-        *
-        * Word is subjected to tilde expansion, parameter expansion,
-        * command substitution, and arithmetic expansion.
-        * If word is not needed, it is not expanded.
-        *
-        * Colon forms (${var:-word}, ${var:=word} etc) do the same,
-        * but also treat null var as if it is unset.
-        */
-                                       int use_word = (!val || ((exp_save == ':') && !val[0]));
-                                       if (exp_op == '+')
-                                               use_word = !use_word;
-                                       debug_printf_expand("expand: op:%c (null:%s) test:%i\n", exp_op,
-                                               (exp_save == ':') ? "true" : "false", use_word);
-                                       if (use_word) {
-                                               to_be_freed = expand_pseudo_dquoted(exp_word);
-                                               if (to_be_freed)
-                                                       exp_word = to_be_freed;
-                                               if (exp_op == '?') {
-                                                       /* mimic bash message */
-                                                       die_if_script("%s: %s",
-                                                               var,
-                                                               exp_word[0] ? exp_word : "parameter null or not set"
-                                                       );
-//TODO: how interactive bash aborts expansion mid-command?
-                                               } else {
-                                                       val = exp_word;
-                                               }
-
-                                               if (exp_op == '=') {
-                                                       /* ${var=[word]} or ${var:=[word]} */
-                                                       if (isdigit(var[0]) || var[0] == '#') {
-                                                               /* mimic bash message */
-                                                               die_if_script("$%s: cannot assign in this way", var);
-                                                               val = NULL;
-                                                       } else {
-                                                               char *new_var = xasprintf("%s=%s", var, val);
-                                                               set_local_var(new_var, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
-                                                       }
-                                               }
-                                       }
-                               } /* one of "-=+?" */
-
-                               *exp_saveptr = exp_save;
-                       } /* if (exp_op) */
-
-                       arg[0] = first_ch;
-#if ENABLE_HUSH_TICK
- store_val:
-#endif
+               default:
+                       val = expand_one_var(&to_be_freed, arg, &p, first_ch);
+ IF_HUSH_TICK(store_val:)
                        if (!(first_ch & 0x80)) { /* unquoted $VAR */
                                debug_printf_expand("unquoted '%s', output->o_escape:%d\n", val, output->o_escape);
-                               if (val) {
+                               if (val && val[0]) {
                                        /* unquoted var's contents should be globbed, so don't escape */
                                        smallint sv = output->o_escape;
                                        output->o_escape = 0;
@@ -3062,10 +3076,11 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg, char
                        } else { /* quoted $VAR, val will be appended below */
                                debug_printf_expand("quoted '%s', output->o_escape:%d\n", val, output->o_escape);
                        }
-               } /* default: */
+                       break;
+
                } /* switch (char after <SPECIAL_VAR_SYMBOL>) */
 
-               if (val) {
+               if (val && val[0]) {
                        o_addQstr(output, val, strlen(val));
                }
                free(to_be_freed);