hush: fix improper handling of newline and hash chars in few corner cases

Signed-off-by: Denys Vlasenko <dvlasenk@redhat.com>
diff --git a/shell/hush.c b/shell/hush.c
index 50e9ce3..da32c24 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -543,7 +543,6 @@
 #define IS_NULL_CMD(cmd) \
 	(!(cmd)->group && !(cmd)->argv && !(cmd)->redirects)
 
-
 struct pipe {
 	struct pipe *next;
 	int num_cmds;               /* total number of commands in pipe */
@@ -2622,6 +2621,94 @@
 
 /*** Parsing routines ***/
 
+#ifndef debug_print_tree
+static void debug_print_tree(struct pipe *pi, int lvl)
+{
+	static const char *const PIPE[] = {
+		[PIPE_SEQ] = "SEQ",
+		[PIPE_AND] = "AND",
+		[PIPE_OR ] = "OR" ,
+		[PIPE_BG ] = "BG" ,
+	};
+	static const char *RES[] = {
+		[RES_NONE ] = "NONE" ,
+# if ENABLE_HUSH_IF
+		[RES_IF   ] = "IF"   ,
+		[RES_THEN ] = "THEN" ,
+		[RES_ELIF ] = "ELIF" ,
+		[RES_ELSE ] = "ELSE" ,
+		[RES_FI   ] = "FI"   ,
+# endif
+# if ENABLE_HUSH_LOOPS
+		[RES_FOR  ] = "FOR"  ,
+		[RES_WHILE] = "WHILE",
+		[RES_UNTIL] = "UNTIL",
+		[RES_DO   ] = "DO"   ,
+		[RES_DONE ] = "DONE" ,
+# endif
+# if ENABLE_HUSH_LOOPS || ENABLE_HUSH_CASE
+		[RES_IN   ] = "IN"   ,
+# endif
+# if ENABLE_HUSH_CASE
+		[RES_CASE ] = "CASE" ,
+		[RES_CASE_IN ] = "CASE_IN" ,
+		[RES_MATCH] = "MATCH",
+		[RES_CASE_BODY] = "CASE_BODY",
+		[RES_ESAC ] = "ESAC" ,
+# endif
+		[RES_XXXX ] = "XXXX" ,
+		[RES_SNTX ] = "SNTX" ,
+	};
+	static const char *const CMDTYPE[] = {
+		"{}",
+		"()",
+		"[noglob]",
+# if ENABLE_HUSH_FUNCTIONS
+		"func()",
+# endif
+	};
+
+	int pin, prn;
+
+	pin = 0;
+	while (pi) {
+		fprintf(stderr, "%*spipe %d res_word=%s followup=%d %s\n", lvl*2, "",
+				pin, RES[pi->res_word], pi->followup, PIPE[pi->followup]);
+		prn = 0;
+		while (prn < pi->num_cmds) {
+			struct command *command = &pi->cmds[prn];
+			char **argv = command->argv;
+
+			fprintf(stderr, "%*s cmd %d assignment_cnt:%d",
+					lvl*2, "", prn,
+					command->assignment_cnt);
+			if (command->group) {
+				fprintf(stderr, " group %s: (argv=%p)%s%s\n",
+						CMDTYPE[command->cmd_type],
+						argv
+# if !BB_MMU
+						, " group_as_string:", command->group_as_string
+# else
+						, "", ""
+# endif
+				);
+				debug_print_tree(command->group, lvl+1);
+				prn++;
+				continue;
+			}
+			if (argv) while (*argv) {
+				fprintf(stderr, " '%s'", *argv);
+				argv++;
+			}
+			fprintf(stderr, "\n");
+			prn++;
+		}
+		pi = pi->next;
+		pin++;
+	}
+}
+#endif /* debug_print_tree */
+
 static struct pipe *new_pipe(void)
 {
 	struct pipe *pi;
@@ -4011,15 +4098,16 @@
 				goto parse_error;
 			}
 			if (ch == '\n') {
-#if ENABLE_HUSH_CASE
-				/* "case ... in <newline> word) ..." -
-				 * newlines are ignored (but ';' wouldn't be) */
-				if (ctx.command->argv == NULL
-				 && ctx.ctx_res_w == RES_MATCH
+				/* Is this a case when newline is simply ignored?
+				 * Some examples:
+				 * "cmd | <newline> cmd ..."
+				 * "case ... in <newline> word) ..."
+				 */
+				if (IS_NULL_CMD(ctx.command)
+				 && dest.length == 0 && !dest.has_quoted_part
 				) {
 					continue;
 				}
-#endif
 				/* Treat newline as a command separator. */
 				done_pipe(&ctx, PIPE_SEQ);
 				debug_printf_parse("heredoc_cnt:%d\n", heredoc_cnt);
@@ -4151,6 +4239,31 @@
 			if (parse_redirect(&ctx, redir_fd, redir_style, input))
 				goto parse_error;
 			continue; /* back to top of while (1) */
+		case '#':
+			if (dest.length == 0 && !dest.has_quoted_part) {
+				/* skip "#comment" */
+				while (1) {
+					ch = i_peek(input);
+					if (ch == EOF || ch == '\n')
+						break;
+					i_getch(input);
+					/* note: we do not add it to &ctx.as_string */
+				}
+				nommu_addchr(&ctx.as_string, '\n');
+				continue; /* back to top of while (1) */
+			}
+			break;
+		case '\\':
+			if (next == '\n') {
+				/* It's "\<newline>" */
+#if !BB_MMU
+				/* Remove trailing '\' from ctx.as_string */
+				ctx.as_string.data[--ctx.as_string.length] = '\0';
+#endif
+				ch = i_getch(input); /* eat it */
+				continue; /* back to top of while (1) */
+			}
+			break;
 		}
 
 		if (dest.o_assignment == MAYBE_ASSIGNMENT
@@ -4165,19 +4278,8 @@
 		/* Note: nommu_addchr(&ctx.as_string, ch) is already done */
 
 		switch (ch) {
-		case '#':
-			if (dest.length == 0) {
-				while (1) {
-					ch = i_peek(input);
-					if (ch == EOF || ch == '\n')
-						break;
-					i_getch(input);
-					/* note: we do not add it to &ctx.as_string */
-				}
-				nommu_addchr(&ctx.as_string, '\n');
-			} else {
-				o_addQchr(&dest, ch);
-			}
+		case '#': /* non-comment #: "echo a#b" etc */
+			o_addQchr(&dest, ch);
 			break;
 		case '\\':
 			if (next == EOF) {
@@ -4185,21 +4287,14 @@
 				xfunc_die();
 			}
 			ch = i_getch(input);
-			if (ch != '\n') {
-				o_addchr(&dest, '\\');
-				/*nommu_addchr(&ctx.as_string, '\\'); - already done */
-				o_addchr(&dest, ch);
-				nommu_addchr(&ctx.as_string, ch);
-				/* Example: echo Hello \2>file
-				 * we need to know that word 2 is quoted */
-				dest.has_quoted_part = 1;
-			}
-#if !BB_MMU
-			else {
-				/* It's "\<newline>". Remove trailing '\' from ctx.as_string */
-				ctx.as_string.data[--ctx.as_string.length] = '\0';
-			}
-#endif
+			/* note: ch != '\n' (that case does not reach this place) */
+			o_addchr(&dest, '\\');
+			/*nommu_addchr(&ctx.as_string, '\\'); - already done */
+			o_addchr(&dest, ch);
+			nommu_addchr(&ctx.as_string, ch);
+			/* Example: echo Hello \2>file
+			 * we need to know that word 2 is quoted */
+			dest.has_quoted_part = 1;
 			break;
 		case '$':
 			if (parse_dollar(&ctx.as_string, &dest, input, /*quote_mask:*/ 0) != 0) {
@@ -6869,94 +6964,6 @@
 	return -1;
 }
 
-#ifndef debug_print_tree
-static void debug_print_tree(struct pipe *pi, int lvl)
-{
-	static const char *const PIPE[] = {
-		[PIPE_SEQ] = "SEQ",
-		[PIPE_AND] = "AND",
-		[PIPE_OR ] = "OR" ,
-		[PIPE_BG ] = "BG" ,
-	};
-	static const char *RES[] = {
-		[RES_NONE ] = "NONE" ,
-# if ENABLE_HUSH_IF
-		[RES_IF   ] = "IF"   ,
-		[RES_THEN ] = "THEN" ,
-		[RES_ELIF ] = "ELIF" ,
-		[RES_ELSE ] = "ELSE" ,
-		[RES_FI   ] = "FI"   ,
-# endif
-# if ENABLE_HUSH_LOOPS
-		[RES_FOR  ] = "FOR"  ,
-		[RES_WHILE] = "WHILE",
-		[RES_UNTIL] = "UNTIL",
-		[RES_DO   ] = "DO"   ,
-		[RES_DONE ] = "DONE" ,
-# endif
-# if ENABLE_HUSH_LOOPS || ENABLE_HUSH_CASE
-		[RES_IN   ] = "IN"   ,
-# endif
-# if ENABLE_HUSH_CASE
-		[RES_CASE ] = "CASE" ,
-		[RES_CASE_IN ] = "CASE_IN" ,
-		[RES_MATCH] = "MATCH",
-		[RES_CASE_BODY] = "CASE_BODY",
-		[RES_ESAC ] = "ESAC" ,
-# endif
-		[RES_XXXX ] = "XXXX" ,
-		[RES_SNTX ] = "SNTX" ,
-	};
-	static const char *const CMDTYPE[] = {
-		"{}",
-		"()",
-		"[noglob]",
-# if ENABLE_HUSH_FUNCTIONS
-		"func()",
-# endif
-	};
-
-	int pin, prn;
-
-	pin = 0;
-	while (pi) {
-		fprintf(stderr, "%*spipe %d res_word=%s followup=%d %s\n", lvl*2, "",
-				pin, RES[pi->res_word], pi->followup, PIPE[pi->followup]);
-		prn = 0;
-		while (prn < pi->num_cmds) {
-			struct command *command = &pi->cmds[prn];
-			char **argv = command->argv;
-
-			fprintf(stderr, "%*s cmd %d assignment_cnt:%d",
-					lvl*2, "", prn,
-					command->assignment_cnt);
-			if (command->group) {
-				fprintf(stderr, " group %s: (argv=%p)%s%s\n",
-						CMDTYPE[command->cmd_type],
-						argv
-# if !BB_MMU
-						, " group_as_string:", command->group_as_string
-# else
-						, "", ""
-# endif
-				);
-				debug_print_tree(command->group, lvl+1);
-				prn++;
-				continue;
-			}
-			if (argv) while (*argv) {
-				fprintf(stderr, " '%s'", *argv);
-				argv++;
-			}
-			fprintf(stderr, "\n");
-			prn++;
-		}
-		pi = pi->next;
-		pin++;
-	}
-}
-#endif /* debug_print_tree */
-
 /* NB: called by pseudo_exec, and therefore must not modify any
  * global data until exec/_exit (we can be a child after vfork!) */
 static int run_list(struct pipe *pi)
diff --git a/shell/hush_test/hush-misc/assignment3.right b/shell/hush_test/hush-misc/assignment3.right
new file mode 100644
index 0000000..0f02d7c
--- /dev/null
+++ b/shell/hush_test/hush-misc/assignment3.right
@@ -0,0 +1,2 @@
+Done:0
+abc=123
diff --git a/shell/hush_test/hush-misc/assignment3.tests b/shell/hush_test/hush-misc/assignment3.tests
new file mode 100755
index 0000000..790129b
--- /dev/null
+++ b/shell/hush_test/hush-misc/assignment3.tests
@@ -0,0 +1,5 @@
+# This must be interpreted as assignments
+a=1 b\
+=2 c=3
+echo Done:$?
+echo abc=$a$b$c
diff --git a/shell/hush_test/hush-parsing/comment1.right b/shell/hush_test/hush-parsing/comment1.right
new file mode 100644
index 0000000..a102b1d
--- /dev/null
+++ b/shell/hush_test/hush-parsing/comment1.right
@@ -0,0 +1,2 @@
+Nothing:
+String: #should-be-echoed
diff --git a/shell/hush_test/hush-parsing/comment1.tests b/shell/hush_test/hush-parsing/comment1.tests
new file mode 100755
index 0000000..d268860
--- /dev/null
+++ b/shell/hush_test/hush-parsing/comment1.tests
@@ -0,0 +1,2 @@
+echo Nothing: #should-not-be-echoed
+echo String: ""#should-be-echoed
diff --git a/shell/hush_test/hush-parsing/eol1.right b/shell/hush_test/hush-parsing/eol1.right
new file mode 100644
index 0000000..31c896f
--- /dev/null
+++ b/shell/hush_test/hush-parsing/eol1.right
@@ -0,0 +1 @@
+Done:0
diff --git a/shell/hush_test/hush-parsing/eol1.tests b/shell/hush_test/hush-parsing/eol1.tests
new file mode 100755
index 0000000..f1b55e8
--- /dev/null
+++ b/shell/hush_test/hush-parsing/eol1.tests
@@ -0,0 +1,18 @@
+# bug was that we treated <newline> as ';' in this line:
+true || echo foo |
+echo BAD1 | cat
+
+# variation on the same theme
+true || echo foo |
+# comment
+echo BAD2 | cat
+
+# variation on the same theme
+true || echo foo |
+
+echo BAD3 | cat
+
+# this should error out, but currently works in hush:
+#true || echo foo |;
+
+echo Done:$?