*
* Bash compat TODO:
* redirection of stdout+stderr: &> and >&
- * subst operator: ${var/[/]expr/expr}
* brace expansion: one/{two,three,four}
* reserved words: function select
* advanced test: [[ ]]
#define _SPECIAL_VARS_STR "_*@$!?#"
#define SPECIAL_VARS_STR ("_*@$!?#" + 1)
#define NUMERIC_SPECVARS_STR ("_*@$!?#" + 3)
+#if ENABLE_HUSH_BASH_COMPAT
+/* Support / and // replace ops */
+/* Note that // is stored as \ in "encoded" string representation */
+# define VAR_ENCODED_SUBST_OPS "\\/%#:-=+?"
+# define VAR_SUBST_OPS ("\\/%#:-=+?" + 1)
+# define MINUS_PLUS_EQUAL_QUESTION ("\\/%#:-=+?" + 5)
+#else
+# define VAR_ENCODED_SUBST_OPS "%#:-=+?"
+# define VAR_SUBST_OPS "%#:-=+?"
+# define MINUS_PLUS_EQUAL_QUESTION ("%#:-=+?" + 3)
+#endif
#define SPECIAL_VAR_SYMBOL 3
}
#endif
+#if ENABLE_HUSH_BASH_COMPAT
+/* ${var/[/]pattern[/repl]} helpers */
+static char *strstr_pattern(char *val, const char *pattern, int *size)
+{
+ while (1) {
+ char *end = scan_and_match(val, pattern, SCAN_MOVE_FROM_RIGHT + SCAN_MATCH_LEFT_HALF);
+ debug_printf_varexp("val:'%s' pattern:'%s' end:'%s'\n", val, pattern, end);
+ if (end) {
+ *size = end - val;
+ return val;
+ }
+ if (*val == '\0')
+ return NULL;
+ /* Optimization: if "*pat" did not match the start of "string",
+ * we know that "tring", "ring" etc will not match too:
+ */
+ if (pattern[0] == '*')
+ return NULL;
+ val++;
+ }
+}
+static char *replace_pattern(char *val, const char *pattern, const char *repl, char exp_op)
+{
+ char *result = NULL;
+ unsigned res_len = 0;
+ unsigned repl_len = strlen(repl);
+
+ while (1) {
+ int size;
+ char *s = strstr_pattern(val, pattern, &size);
+ if (!s)
+ break;
+
+ result = xrealloc(result, res_len + (s - val) + repl_len + 1);
+ memcpy(result + res_len, val, s - val);
+ res_len += s - val;
+ strcpy(result + res_len, repl);
+ res_len += repl_len;
+ debug_printf_varexp("val:'%s' s:'%s' result:'%s'\n", val, s, result);
+
+ val = s + size;
+ if (exp_op == '/')
+ break;
+ }
+ if (val[0] && result) {
+ result = xrealloc(result, res_len + strlen(val) + 1);
+ strcpy(result + res_len, val);
+ debug_printf_varexp("val:'%s' result:'%s'\n", val, result);
+ }
+ debug_printf_varexp("result:'%s'\n", result);
+ return result;
+}
+#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
var = arg;
*p = '\0';
- exp_saveptr = arg[1] ? strchr("%#:-=+?", arg[1]) : NULL;
+ exp_saveptr = arg[1] ? strchr(VAR_ENCODED_SUBST_OPS, arg[1]) : NULL;
first_char = arg[0] = first_ch & 0x7f;
exp_op = 0;
exp_saveptr = var + 1;
} else {
/* ${?}, ${var}, ${var:0}, ${var[:]%0} etc */
- exp_saveptr = var+1 + strcspn(var+1, "%#:-=+?");
+ exp_saveptr = var+1 + strcspn(var+1, VAR_ENCODED_SUBST_OPS);
}
exp_op = exp_save = *exp_saveptr;
if (exp_op) {
if (exp_op == ':') {
exp_op = *exp_word++;
if (ENABLE_HUSH_BASH_COMPAT
- && (exp_op == '\0' || !strchr("%#:-=+?"+3, exp_op))
+ && (exp_op == '\0' || !strchr(MINUS_PLUS_EQUAL_QUESTION, exp_op))
) {
/* oops... it's ${var:N[:M]}, not ${var:?xxx} or some such */
exp_op = ':';
val = utoa(G.root_pid);
break;
case '!': /* bg pid */
- val = G.last_bg_pid ? utoa(G.last_bg_pid) : (char*)"";
+ val = G.last_bg_pid ? utoa(G.last_bg_pid) : "";
break;
case '?': /* exitcode */
val = utoa(G.last_exitcode);
// exp_op, to_be_freed, exp_word, loc);
free(exp_exp_word);
if (loc) { /* match was found */
- if (scan_flags & SCAN_MATCH_LEFT_HALF) /* # or ## */
+ if (scan_flags & SCAN_MATCH_LEFT_HALF) /* #[#] */
val = loc;
- else /* % or %% */
+ else /* %[%] */
*loc = '\0';
}
}
- } else if (exp_op == ':') {
+ }
+#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:
{
char *argv[2], **list;
+ /* This is generally an optimization, but it also
+ * handles "", which otherwise trips over !list[0] check below.
+ * (is this ever happens that we actually get str="" here?)
+ */
+ if (!strchr(str, SPECIAL_VAR_SYMBOL) && !strchr(str, '\\')) {
+ //TODO: Can use on strings with \ too, just unbackslash() them?
+ debug_printf_expand("string_to_string(fast)='%s'\n", str);
+ return xstrdup(str);
+ }
+
argv[0] = (char*)str;
argv[1] = NULL;
list = expand_variables(argv, EXPVAR_FLAG_ESCAPE_VARS | EXPVAR_FLAG_SINGLEWORD);
*pp++ = (char *) G.argv0_for_re_execing;
*pp++ = param_buf;
for (cur = G.top_var; cur; cur = cur->next) {
- if (cur->varstr == hush_version_str)
+ if (strcmp(cur->varstr, hush_version_str) == 0)
continue;
if (cur->flg_read_only) {
*pp++ = (char *) "-R";
*
* Also adapted to eat ${var%...} and $((...)) constructs, since ... part
* can contain arbitrary constructs, just like $(cmd).
- * In bash compat mode, it needs to also be able to stop on '}' or ':'
- * for ${var:N[:M]} parsing.
+ * In bash compat mode, it needs to also be able to stop on ':' or '/'
+ * for ${var:N[:M]} and ${var/P[/R]} parsing.
*/
#define DOUBLE_CLOSE_CHAR_FLAG 0x80
static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsigned end_ch)
/* handle parameter expansions
* http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02
*/
- if (!strchr("%#:-=+?", ch)) /* ${var<bad_char>... */
+ if (!strchr(VAR_SUBST_OPS, ch)) /* ${var<bad_char>... */
goto bad_dollar_syntax;
- o_addchr(dest, ch);
/* Eat everything until closing '}' (or ':') */
end_ch = '}';
if (ENABLE_HUSH_BASH_COMPAT
&& ch == ':'
- && !strchr("%#:-=+?"+3, i_peek(input))
+ && !strchr(MINUS_PLUS_EQUAL_QUESTION, i_peek(input))
) {
/* It's ${var:N[:M]} thing */
end_ch = '}' * 0x100 + ':';
}
+ if (ENABLE_HUSH_BASH_COMPAT
+ && ch == '/'
+ ) {
+ /* It's ${var/[/]pattern[/repl]} thing */
+ if (i_peek(input) == '/') { /* ${var//pattern[/repl]}? */
+ i_getch(input);
+ nommu_addchr(as_string, '/');
+ ch = '\\';
+ }
+ end_ch = '}' * 0x100 + '/';
+ }
+ o_addchr(dest, ch);
again:
if (!BB_MMU)
pos = dest->length;
if (ENABLE_HUSH_BASH_COMPAT && (end_ch & 0xff00)) {
/* close the first block: */
o_addchr(dest, SPECIAL_VAR_SYMBOL);
- /* while parsing N from ${var:N[:M]}... */
+ /* while parsing N from ${var:N[:M]}
+ * or pattern from ${var/[/]pattern[/repl]} */
if ((end_ch & 0xff) == last_ch) {
- /* ...got ':' - parse the rest */
+ /* got ':' or '/'- parse the rest */
end_ch = '}';
goto again;
}
- /* ...got '}', not ':' - it's ${var:N}! emulate :999999999 */
- o_addstr(dest, "999999999");
+ /* got '}' */
+ if (end_ch == '}' * 0x100 + ':') {
+ /* it's ${var:N} - emulate :999999999 */
+ o_addstr(dest, "999999999");
+ } /* else: it's ${var/[/]pattern} */
}
break;
}
int hush_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int hush_main(int argc, char **argv)
{
- static const struct variable const_shell_ver = {
- .next = NULL,
- .varstr = (char*)hush_version_str,
- .max_len = 1, /* 0 can provoke free(name) */
- .flg_export = 1,
- .flg_read_only = 1,
- };
int opt;
unsigned builtin_argc;
char **e;
G.argv0_for_re_execing = argv[0];
#endif
/* Deal with HUSH_VERSION */
- G.shell_ver = const_shell_ver; /* copying struct here */
+ G.shell_ver.flg_export = 1;
+ G.shell_ver.flg_read_only = 1;
+ /* Code which handles ${var/P/R} needs writable values for all variables,
+ * therefore we xstrdup: */
+ G.shell_ver.varstr = xstrdup(hush_version_str),
G.top_var = &G.shell_ver;
debug_printf_env("unsetenv '%s'\n", "HUSH_VERSION");
unsetenv("HUSH_VERSION"); /* in case it exists in initial env */
+ /* reinstate HUSH_VERSION in environment */
+ debug_printf_env("putenv '%s'\n", G.shell_ver.varstr);
+ putenv(G.shell_ver.varstr);
+
/* Initialize our shell local variables with the values
* currently living in the environment */
cur_var = G.top_var;
}
e++;
}
- /* reinstate HUSH_VERSION */
- debug_printf_env("putenv '%s'\n", hush_version_str);
- putenv((char *)hush_version_str);
/* Export PWD */
set_pwd_var(/*exp:*/ 1);