aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--shell/hush.c513
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);