hush: fix globbing+backslashes in unquoted $var expansion

Signed-off-by: Denys Vlasenko <dvlasenk@redhat.com>
diff --git a/shell/hush.c b/shell/hush.c
index 2a4e80b..ef46372 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -2007,12 +2007,17 @@
 static void o_addblock_duplicate_backslash(o_string *o, const char *str, int len)
 {
 	while (len) {
-		o_addchr(o, *str);
-		if (*str == '\\') {
-			o_addchr(o, '\\');
-		}
-		str++;
 		len--;
+		o_addchr(o, *str);
+		if (*str++ == '\\') {
+			/* \z -> \\\z; \<eol> -> \\<eol> */
+			o_addchr(o, '\\');
+			if (len) {
+				len--;
+				o_addchr(o, '\\');
+				o_addchr(o, *str++);
+			}
+		}
 	}
 }
 
@@ -2067,12 +2072,8 @@
 	o->data[o->length] = '\0';
 }
 
-static void o_addQblock(o_string *o, const char *str, int len)
+static void o_addqblock(o_string *o, const char *str, int len)
 {
-	if (!o->o_escape) {
-		o_addblock(o, str, len);
-		return;
-	}
 	while (len) {
 		char ch;
 		int sz;
@@ -2099,6 +2100,15 @@
 	}
 }
 
+static void o_addQblock(o_string *o, const char *str, int len)
+{
+	if (!o->o_escape) {
+		o_addblock(o, str, len);
+		return;
+	}
+	o_addqblock(o, str, len);
+}
+
 static void o_addQstr(o_string *o, const char *str)
 {
 	o_addQblock(o, str, strlen(str));
@@ -2356,11 +2366,11 @@
 /* Performs globbing on last list[],
  * saving each result as a new list[].
  */
-static int o_glob(o_string *o, int n)
+static int perform_glob(o_string *o, int n)
 {
 	char *pattern, *copy;
 
-	debug_printf_glob("start o_glob: n:%d o->data:%p\n", n, o->data);
+	debug_printf_glob("start perform_glob: n:%d o->data:%p\n", n, o->data);
 	if (!o->data)
 		return o_save_ptr_helper(o, n);
 	pattern = o->data + o_get_last_ptr(o, n);
@@ -2378,7 +2388,7 @@
 	n = glob_brace(copy, o, n);
 	free(copy);
 	if (DEBUG_GLOB)
-		debug_print_list("o_glob returning", o, n);
+		debug_print_list("perform_glob returning", o, n);
 	return n;
 }
 
@@ -2403,13 +2413,13 @@
 /* Performs globbing on last list[],
  * saving each result as a new list[].
  */
-static int o_glob(o_string *o, int n)
+static int perform_glob(o_string *o, int n)
 {
 	glob_t globdata;
 	int gr;
 	char *pattern;
 
-	debug_printf_glob("start o_glob: n:%d o->data:%p\n", n, o->data);
+	debug_printf_glob("start perform_glob: n:%d o->data:%p\n", n, o->data);
 	if (!o->data)
 		return o_save_ptr_helper(o, n);
 	pattern = o->data + o_get_last_ptr(o, n);
@@ -2455,7 +2465,7 @@
 	}
 	globfree(&globdata);
 	if (DEBUG_GLOB)
-		debug_print_list("o_glob returning", o, n);
+		debug_print_list("perform_glob returning", o, n);
 	return n;
 }
 
@@ -2470,7 +2480,7 @@
 		 * (if it was requested back then when it was filled)
 		 * so don't do that again! */
 		if (!o->has_empty_slot)
-			return o_glob(o, n); /* o_save_ptr_helper is inside */
+			return perform_glob(o, n); /* o_save_ptr_helper is inside */
 	}
 	return o_save_ptr_helper(o, n);
 }
@@ -2927,15 +2937,6 @@
 						(ctx->ctx_res_w == RES_SNTX));
 				return (ctx->ctx_res_w == RES_SNTX);
 			}
-# ifdef CMD_SINGLEWORD_NOGLOB_COND
-			if (strcmp(word->data, "export") == 0
-#  if ENABLE_HUSH_LOCAL
-			 || strcmp(word->data, "local") == 0
-#  endif
-			) {
-				command->cmd_type = CMD_SINGLEWORD_NOGLOB_COND;
-			} else
-# endif
 # if ENABLE_HUSH_BASH_COMPAT
 			if (strcmp(word->data, "[[") == 0) {
 				command->cmd_type = CMD_SINGLEWORD_NOGLOB;
@@ -4371,10 +4372,19 @@
 	while (1) {
 		int word_len = strcspn(str, G.ifs);
 		if (word_len) {
-			if (output->o_escape || !output->o_glob)
-				o_addQblock(output, str, word_len);
-			else /* protect backslashes against globbing up :) */
+			if (output->o_escape)
+				o_addqblock(output, str, word_len);
+			else if (!output->o_glob)
+				o_addblock(output, str, word_len);
+			else /* if (!escape && glob) */ {
+				/* Protect backslashes against globbing up :)
+				 * Example: "v='\*'; echo b$v"
+				 */
 				o_addblock_duplicate_backslash(output, str, word_len);
+				/*/ Why can't we do it easier? */
+				/*o_addblock(output, str, word_len); - WRONG: "v='\*'; echo Z$v" prints "Z*" instead of "Z\*" */
+				/*o_addqblock(output, str, word_len); - WRONG: "v='*'; echo Z$v" prints "Z*" instead of Z* files */
+			}
 			str += word_len;
 		}
 		if (!*str)  /* EOL - do not finalize word */
@@ -4594,8 +4604,9 @@
 				if (exp_op == *exp_word)	/* ## or %% */
 					exp_word++;
 //TODO: avoid xstrdup unless needed
-// (see HACK ALERT below)
+// (see HACK ALERT below for an example)
 				val = to_be_freed = xstrdup(val);
+//TODO: fix expansion rules:
 				exp_exp_word = expand_pseudo_dquoted(exp_word);
 				if (exp_exp_word)
 					exp_word = exp_exp_word;
@@ -4613,10 +4624,26 @@
 		}
 #if ENABLE_HUSH_BASH_COMPAT
 		else if (exp_op == '/' || exp_op == '\\') {
+			/* It's ${var/[/]pattern[/repl]} thing.
+			 * Note that in encoded form it has TWO parts:
+			 * var/pattern<SPECIAL_VAR_SYMBOL>repl<SPECIAL_VAR_SYMBOL>
+			 */
 			/* Empty variable always gives nothing: */
-			// "v=''; echo ${v/*/w}" prints ""
+			// "v=''; echo ${v/*/w}" prints "", not "w"
 			if (val && val[0]) {
 				/* It's ${var/[/]pattern[/repl]} thing */
+				/*
+				 * Pattern is taken literally, while
+				 * repl should be de-backslased and globbed
+				 * by the usual expansion rules:
+				 * >az; >bz;
+				 * v='a bz'; echo "${v/a*z/a*z}" prints "a*z"
+				 * v='a bz'; echo "${v/a*z/\z}"  prints "\z"
+				 * v='a bz'; echo ${v/a*z/a*z}   prints "az"
+				 * v='a bz'; echo ${v/a*z/\z}    prints "z"
+				 * (note that a*z _pattern_ is never globbed!)
+				 */
+//TODO: fix expansion rules:
 				char *pattern, *repl, *t;
 				pattern = expand_pseudo_dquoted(exp_word);
 				if (!pattern)
@@ -4772,7 +4799,6 @@
 
 	while ((p = strchr(arg, SPECIAL_VAR_SYMBOL)) != NULL) {
 		char first_ch;
-		int i;
 		char *to_be_freed = NULL;
 		const char *val = NULL;
 #if ENABLE_HUSH_TICK
@@ -4795,10 +4821,11 @@
 		switch (first_ch & 0x7f) {
 		/* Highest bit in first_ch indicates that var is double-quoted */
 		case '*':
-		case '@':
-			i = 1;
-			if (!G.global_argv[i])
+		case '@': {
+			int i;
+			if (!G.global_argv[1])
 				break;
+			i = 1;
 			ored_ch |= first_ch; /* do it for "$@" _now_, when we know it's not empty */
 			if (!(first_ch & 0x80)) { /* unquoted $* or $@ */
 				smallint sv = output->o_escape;
@@ -4839,6 +4866,7 @@
 				}
 			}
 			break;
+		}
 		case SPECIAL_VAR_SYMBOL: /* <SPECIAL_VAR_SYMBOL><SPECIAL_VAR_SYMBOL> */
 			/* "Empty variable", used to make "" etc to not disappear */
 			arg++;
@@ -4984,41 +5012,6 @@
 }
 #endif
 
-#ifdef CMD_SINGLEWORD_NOGLOB_COND
-static char **expand_strvec_to_strvec_singleword_noglob_cond(char **argv)
-{
-	int n;
-	char **list;
-	char **v;
-	o_string output = NULL_O_STRING;
-
-	n = 0;
-	v = argv;
-	while (*v) {
-		int is_var = is_well_formed_var_name(*v, '=');
-		/* is_var * 0x80: singleword expansion for vars */
-		n = expand_vars_to_list(&output, n, *v, is_var * 0x80);
-
-		/* Subtle! expand_vars_to_list did not glob last word yet.
-		 * It does this only when fed with further data.
-		 * Therefore we set globbing flags AFTER it, not before:
-		 */
-
-		/* if it is not recognizably abc=...; then: */
-		output.o_escape = !is_var; /* protect against globbing for "$var" */
-		/* (unquoted $var will temporarily switch it off) */
-		output.o_glob = !is_var; /* and indeed do globbing */
-		v++;
-	}
-	debug_print_list("expand_cond", &output, n);
-
-	/* output.data (malloced in one block) gets returned in "list" */
-	list = o_finalize_list(&output, n);
-	debug_print_strings("expand_cond[1]", list);
-	return list;
-}
-#endif
-
 /* Used for expansion of right hand of assignments */
 /* NB: should NOT do globbing!
  * "export v=/bin/c*; env | grep ^v=" outputs "v=/bin/c*" */
@@ -6567,11 +6560,6 @@
 			argv_expanded = expand_strvec_to_strvec_singleword_noglob(argv + command->assignment_cnt);
 		}
 #endif
-#ifdef CMD_SINGLEWORD_NOGLOB_COND
-		else if (command->cmd_type == CMD_SINGLEWORD_NOGLOB_COND) {
-			argv_expanded = expand_strvec_to_strvec_singleword_noglob_cond(argv + command->assignment_cnt);
-		}
-#endif
 		else {
 			argv_expanded = expand_strvec_to_strvec(argv + command->assignment_cnt);
 		}