diff options
-rw-r--r-- | shell/hush.c | 513 |
1 files changed, 264 insertions, 249 deletions
diff --git a/shell/hush.c b/shell/hush.c index 9a08e90c9..821a7a77f 100644 --- a/shell/hush.c +++ b/shell/hush.c @@ -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); |