hush: fix misparsing of "... do eval a= ...". Closes 3721

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/shell/hush.c b/shell/hush.c
index 9cc86fa..c3a4afb 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -449,6 +449,15 @@
 /* Used for initialization: o_string foo = NULL_O_STRING; */
 #define NULL_O_STRING { NULL }
 
+#ifndef debug_printf_parse
+static const char *const assignment_flag[] = {
+	"MAYBE_ASSIGNMENT",
+	"DEFINITELY_ASSIGNMENT",
+	"NOT_ASSIGNMENT",
+	"WORD_IS_KEYWORD",
+};
+#endif
+
 typedef struct in_str {
 	const char *p;
 	/* eof_flag=1: last char in ->p is really an EOF */
@@ -3033,24 +3042,24 @@
 	 */
 	static const struct reserved_combo reserved_list[] = {
 # if ENABLE_HUSH_IF
-		{ "!",     RES_NONE,  NOT_ASSIGNMENT , 0 },
-		{ "if",    RES_IF,    WORD_IS_KEYWORD, FLAG_THEN | FLAG_START },
-		{ "then",  RES_THEN,  WORD_IS_KEYWORD, FLAG_ELIF | FLAG_ELSE | FLAG_FI },
-		{ "elif",  RES_ELIF,  WORD_IS_KEYWORD, FLAG_THEN },
-		{ "else",  RES_ELSE,  WORD_IS_KEYWORD, FLAG_FI   },
-		{ "fi",    RES_FI,    NOT_ASSIGNMENT , FLAG_END  },
+		{ "!",     RES_NONE,  NOT_ASSIGNMENT  , 0 },
+		{ "if",    RES_IF,    MAYBE_ASSIGNMENT, FLAG_THEN | FLAG_START },
+		{ "then",  RES_THEN,  MAYBE_ASSIGNMENT, FLAG_ELIF | FLAG_ELSE | FLAG_FI },
+		{ "elif",  RES_ELIF,  MAYBE_ASSIGNMENT, FLAG_THEN },
+		{ "else",  RES_ELSE,  MAYBE_ASSIGNMENT, FLAG_FI   },
+		{ "fi",    RES_FI,    NOT_ASSIGNMENT  , FLAG_END  },
 # endif
 # if ENABLE_HUSH_LOOPS
-		{ "for",   RES_FOR,   NOT_ASSIGNMENT , FLAG_IN | FLAG_DO | FLAG_START },
-		{ "while", RES_WHILE, WORD_IS_KEYWORD, FLAG_DO | FLAG_START },
-		{ "until", RES_UNTIL, WORD_IS_KEYWORD, FLAG_DO | FLAG_START },
-		{ "in",    RES_IN,    NOT_ASSIGNMENT , FLAG_DO   },
-		{ "do",    RES_DO,    WORD_IS_KEYWORD, FLAG_DONE },
-		{ "done",  RES_DONE,  NOT_ASSIGNMENT , FLAG_END  },
+		{ "for",   RES_FOR,   NOT_ASSIGNMENT  , FLAG_IN | FLAG_DO | FLAG_START },
+		{ "while", RES_WHILE, MAYBE_ASSIGNMENT, FLAG_DO | FLAG_START },
+		{ "until", RES_UNTIL, MAYBE_ASSIGNMENT, FLAG_DO | FLAG_START },
+		{ "in",    RES_IN,    NOT_ASSIGNMENT  , FLAG_DO   },
+		{ "do",    RES_DO,    MAYBE_ASSIGNMENT, FLAG_DONE },
+		{ "done",  RES_DONE,  NOT_ASSIGNMENT  , FLAG_END  },
 # endif
 # if ENABLE_HUSH_CASE
-		{ "case",  RES_CASE,  NOT_ASSIGNMENT , FLAG_MATCH | FLAG_START },
-		{ "esac",  RES_ESAC,  NOT_ASSIGNMENT , FLAG_END  },
+		{ "case",  RES_CASE,  NOT_ASSIGNMENT  , FLAG_MATCH | FLAG_START },
+		{ "esac",  RES_ESAC,  NOT_ASSIGNMENT  , FLAG_END  },
 # endif
 	};
 	const struct reserved_combo *r;
@@ -3116,6 +3125,7 @@
 	ctx->ctx_res_w = r->res;
 	ctx->old_flag = r->flag;
 	word->o_assignment = r->assignment_flag;
+	debug_printf_parse("word->o_assignment='%s'\n", assignment_flag[word->o_assignment]);
 
 	if (ctx->old_flag & FLAG_END) {
 		struct parse_context *old;
@@ -3182,18 +3192,6 @@
 		debug_printf_parse("word stored in rd_filename: '%s'\n", word->data);
 		ctx->pending_redirect = NULL;
 	} else {
-		/* If this word wasn't an assignment, next ones definitely
-		 * can't be assignments. Even if they look like ones. */
-		if (word->o_assignment != DEFINITELY_ASSIGNMENT
-		 && word->o_assignment != WORD_IS_KEYWORD
-		) {
-			word->o_assignment = NOT_ASSIGNMENT;
-		} else {
-			if (word->o_assignment == DEFINITELY_ASSIGNMENT)
-				command->assignment_cnt++;
-			word->o_assignment = MAYBE_ASSIGNMENT;
-		}
-
 #if HAS_KEYWORDS
 # if ENABLE_HUSH_CASE
 		if (ctx->ctx_dsemicolon
@@ -3213,8 +3211,9 @@
 		 && ctx->ctx_res_w != RES_CASE
 # endif
 		) {
-			debug_printf_parse("checking '%s' for reserved-ness\n", word->data);
-			if (reserved_word(word, ctx)) {
+			int reserved = reserved_word(word, ctx);
+			debug_printf_parse("checking for reserved-ness: %d\n", reserved);
+			if (reserved) {
 				o_reset_to_empty_unquoted(word);
 				debug_printf_parse("done_word return %d\n",
 						(ctx->ctx_res_w == RES_SNTX));
@@ -3235,6 +3234,23 @@
 					"groups and arglists don't mix\n");
 			return 1;
 		}
+
+		/* If this word wasn't an assignment, next ones definitely
+		 * can't be assignments. Even if they look like ones. */
+		if (word->o_assignment != DEFINITELY_ASSIGNMENT
+		 && word->o_assignment != WORD_IS_KEYWORD
+		) {
+			word->o_assignment = NOT_ASSIGNMENT;
+		} else {
+			if (word->o_assignment == DEFINITELY_ASSIGNMENT) {
+				command->assignment_cnt++;
+				debug_printf_parse("++assignment_cnt=%d\n", command->assignment_cnt);
+			}
+			debug_printf_parse("word->o_assignment was:'%s'\n", assignment_flag[word->o_assignment]);
+			word->o_assignment = MAYBE_ASSIGNMENT;
+		}
+		debug_printf_parse("word->o_assignment='%s'\n", assignment_flag[word->o_assignment]);
+
 		if (word->has_quoted_part
 		 /* optimization: and if it's ("" or '') or ($v... or `cmd`...): */
 		 && (word->data[0] == '\0' || word->data[0] == SPECIAL_VAR_SYMBOL)
@@ -4259,6 +4275,7 @@
 			 && is_well_formed_var_name(dest.data, '=')
 			) {
 				dest.o_assignment = DEFINITELY_ASSIGNMENT;
+				debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
 			}
 			continue;
 		}
@@ -4308,6 +4325,7 @@
 					heredoc_cnt = 0;
 				}
 				dest.o_assignment = MAYBE_ASSIGNMENT;
+				debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
 				ch = ';';
 				/* note: if (is_blank) continue;
 				 * will still trigger for us */
@@ -4357,6 +4375,7 @@
 			}
 			done_pipe(&ctx, PIPE_SEQ);
 			dest.o_assignment = MAYBE_ASSIGNMENT;
+			debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
 			/* Do we sit outside of any if's, loops or case's? */
 			if (!HAS_KEYWORDS
 			 IF_HAS_KEYWORDS(|| (ctx.ctx_res_w == RES_NONE && ctx.old_flag == 0))
@@ -4463,6 +4482,7 @@
 			/* ch is a special char and thus this word
 			 * cannot be an assignment */
 			dest.o_assignment = NOT_ASSIGNMENT;
+			debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
 		}
 
 		/* Note: nommu_addchr(&ctx.as_string, ch) is already done */
@@ -4561,6 +4581,7 @@
 			/* We just finished a cmd. New one may start
 			 * with an assignment */
 			dest.o_assignment = MAYBE_ASSIGNMENT;
+			debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
 			break;
 		case '&':
 			if (done_word(&dest, &ctx)) {