* 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"
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
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;
#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";
#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';
}
}
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;
}
}
}
- var[exp_off] = exp_save;
- }
+ *exp_saveptr = exp_save;
+ } /* if (exp_op) */
arg[0] = first_ch;
#if ENABLE_HUSH_TICK
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;
* 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);
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);
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;
}
# 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);
# 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);