hush: move variable expansion into a separate function. No logic changes

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>
This commit is contained in:
Denys Vlasenko 2010-09-05 14:48:11 +02:00
parent 36f774a0cd
commit f2dc20c2d5

View File

@ -2664,6 +2664,263 @@ static char *replace_pattern(char *val, const char *pattern, const char *repl, c
} }
#endif #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[] /* 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 * 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 * 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; break;
} }
#endif #endif
default: { /* <SPECIAL_VAR_SYMBOL>varname<SPECIAL_VAR_SYMBOL> */ default:
//TODO: move to a subroutine? val = expand_one_var(&to_be_freed, arg, &p, first_ch);
char *var; IF_HUSH_TICK(store_val:)
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
if (!(first_ch & 0x80)) { /* unquoted $VAR */ if (!(first_ch & 0x80)) { /* unquoted $VAR */
debug_printf_expand("unquoted '%s', output->o_escape:%d\n", val, output->o_escape); 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 */ /* unquoted var's contents should be globbed, so don't escape */
smallint sv = output->o_escape; smallint sv = output->o_escape;
output->o_escape = 0; 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 */ } else { /* quoted $VAR, val will be appended below */
debug_printf_expand("quoted '%s', output->o_escape:%d\n", val, output->o_escape); debug_printf_expand("quoted '%s', output->o_escape:%d\n", val, output->o_escape);
} }
} /* default: */ break;
} /* switch (char after <SPECIAL_VAR_SYMBOL>) */ } /* switch (char after <SPECIAL_VAR_SYMBOL>) */
if (val) { if (val && val[0]) {
o_addQstr(output, val, strlen(val)); o_addQstr(output, val, strlen(val));
} }
free(to_be_freed); free(to_be_freed);