hush: make "set -x" output don't redirectable when fd#2 redirected

function                                             old     new   delta
x_mode_print_optionally_squoted                        -     120    +120
x_mode_flush                                           -      68     +68
save_fd_on_redirect                                  208     243     +35
x_mode_prefix                                          -      27     +27
x_mode_addblock                                        -      23     +23
x_mode_addchr                                          -      17     +17
dump_cmd_in_x_mode                                   110      85     -25
run_pipe                                            1919    1890     -29
print_optionally_squoted                             145       -    -145
------------------------------------------------------------------------------
(add/remove: 5/1 grow/shrink: 1/2 up/down: 290/-199)           Total: 91 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/shell/hush.c b/shell/hush.c
index ac8467f..9676819 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -955,9 +955,6 @@
 # endif
 	struct function *top_func;
 #endif
-#if ENABLE_HUSH_MODE_X
-	unsigned x_mode_depth;
-#endif
 	/* Signal and trap handling */
 #if ENABLE_HUSH_FAST
 	unsigned count_SIGCHLD;
@@ -993,6 +990,15 @@
 #if ENABLE_HUSH_MEMLEAK
 	unsigned long memleak_value;
 #endif
+#if ENABLE_HUSH_MODE_X
+	unsigned x_mode_depth;
+	/* "set -x" output should not be redirectable with subsequent 2>FILE.
+	 * We dup fd#2 to x_mode_fd when "set -x" is executed, and use it
+	 * for all subsequent output.
+	 */
+	int x_mode_fd;
+	o_string x_mode_buf;
+#endif
 #if HUSH_DEBUG
 	int debug_indent;
 #endif
@@ -1660,6 +1666,12 @@
 		}
 		fl = fl->next_hfile;
 	}
+#if ENABLE_HUSH_MODE_X
+	if (G.x_mode_fd > 0 && fd == G.x_mode_fd) {
+		G.x_mode_fd = xdup_CLOEXEC_and_close(fd, avoid_fd);
+		return 1; /* "found and moved" */
+	}
+#endif
 	return 0; /* "not in the list" */
 }
 #if ENABLE_FEATURE_SH_STANDALONE && BB_MMU
@@ -2903,6 +2915,11 @@
 	o_addblock(o, str, strlen(str));
 }
 
+static void o_addstr_with_NUL(o_string *o, const char *str)
+{
+	o_addblock(o, str, strlen(str) + 1);
+}
+
 #if !BB_MMU
 static void nommu_addchr(o_string *o, int ch)
 {
@@ -2913,10 +2930,36 @@
 # define nommu_addchr(o, str) ((void)0)
 #endif
 
-static void o_addstr_with_NUL(o_string *o, const char *str)
+#if ENABLE_HUSH_MODE_X
+static void x_mode_addchr(int ch)
 {
-	o_addblock(o, str, strlen(str) + 1);
+	o_addchr(&G.x_mode_buf, ch);
 }
+static void x_mode_addstr(const char *str)
+{
+	o_addstr(&G.x_mode_buf, str);
+}
+static void x_mode_addblock(const char *str, int len)
+{
+	o_addblock(&G.x_mode_buf, str, len);
+}
+static void x_mode_prefix(void)
+{
+	int n = G.x_mode_depth;
+	do x_mode_addchr('+'); while (--n >= 0);
+}
+static void x_mode_flush(void)
+{
+	int len = G.x_mode_buf.length;
+	if (len <= 0)
+		return;
+	if (G.x_mode_fd > 0) {
+		G.x_mode_buf.data[len] = '\n';
+		full_write(G.x_mode_fd, G.x_mode_buf.data, len + 1);
+	}
+	G.x_mode_buf.length = 0;
+}
+#endif
 
 /*
  * HUSH_BRACE_EXPANSION code needs corresponding quoting on variable expansion side.
@@ -8030,31 +8073,26 @@
 }
 
 #if ENABLE_HUSH_MODE_X
-static void print_optionally_squoted(FILE *fp, const char *str)
+static void x_mode_print_optionally_squoted(const char *str)
 {
 	unsigned len;
 	const char *cp;
 
 	cp = str;
-	if (str[0] != '{' && str[0] != '(') for (;;) {
-		if (!*cp) {
-			/* string has no special chars */
-			fputs(str, fp);
-			return;
-		}
-		if (*cp == '\\') break;
-		if (*cp == '\'') break;
-		if (*cp == '"') break;
-		if (*cp == '$') break;
-		if (*cp == '!') break;
-		if (*cp == '*') break;
-		if (*cp == '[') break;
-		if (*cp == ']') break;
-#if ENABLE_HUSH_TICK
-		if (*cp == '`') break;
-#endif
-		if (isspace(*cp)) break;
-		cp++;
+
+	/* the set of chars which-cause-string-to-be-squoted mimics bash */
+	/* test a char with: bash -c 'set -x; echo "CH"' */
+	if (str[strcspn(str, "\\\"'`$(){}[]<>;#&|~*?!^"
+			" " "\001\002\003\004\005\006\007"
+			"\010\011\012\013\014\015\016\017"
+			"\020\021\022\023\024\025\026\027"
+			"\030\031\032\033\034\035\036\037"
+			)
+		] == '\0'
+	) {
+		/* string has no special chars */
+		x_mode_addstr(str);
+		return;
 	}
 
 	cp = str;
@@ -8062,13 +8100,16 @@
 		/* print '....' up to EOL or first squote */
 		len = (int)(strchrnul(cp, '\'') - cp);
 		if (len != 0) {
-			fprintf(fp, "'%.*s'", len, cp);
+			x_mode_addchr('\'');
+			x_mode_addblock(cp, len);
+			x_mode_addchr('\'');
 			cp += len;
 		}
 		if (*cp == '\0')
 			break;
 		/* string contains squote(s), print them as \' */
-		fprintf(fp, "\\'");
+		x_mode_addchr('\\');
+		x_mode_addchr('\'');
 		cp++;
 	}
 }
@@ -8078,19 +8119,19 @@
 		unsigned n;
 
 		/* "+[+++...][ cmd...]\n\0" */
-		n = G.x_mode_depth;
-		do bb_putchar_stderr('+'); while ((int)(--n) >= 0);
+		x_mode_prefix();
 		n = 0;
 		while (argv[n]) {
-			if (argv[n][0] == '\0')
-				fputs(" ''", stderr);
-			else {
-				bb_putchar_stderr(' ');
-				print_optionally_squoted(stderr, argv[n]);
+			x_mode_addchr(' ');
+			if (argv[n][0] == '\0') {
+				x_mode_addchr('\'');
+				x_mode_addchr('\'');
+			} else {
+				x_mode_print_optionally_squoted(argv[n]);
 			}
 			n++;
 		}
-		bb_putchar_stderr('\n');
+		x_mode_flush();
 	}
 }
 #else
@@ -8885,17 +8926,14 @@
 #if ENABLE_HUSH_MODE_X
 				if (G_x_mode) {
 					char *eq;
-					if (i == 0) {
-						unsigned n = G.x_mode_depth;
-						do
-							bb_putchar_stderr('+');
-						while ((int)(--n) >= 0);
-					}
+					if (i == 0)
+						x_mode_prefix();
+					x_mode_addchr(' ');
 					eq = strchrnul(p, '=');
-					fprintf(stderr, " %.*s=", (int)(eq - p), p);
-					if (*eq)
-						print_optionally_squoted(stderr, eq + 1);
-					bb_putchar_stderr('\n');
+					if (*eq) eq++;
+					x_mode_addblock(p, (eq - p));
+					x_mode_print_optionally_squoted(eq);
+					x_mode_flush();
 				}
 #endif
 				debug_printf_env("set shell var:'%s'->'%s'\n", *argv, p);
@@ -9691,6 +9729,7 @@
 		break;
 	case 'x':
 		IF_HUSH_MODE_X(G_x_mode = state;)
+		IF_HUSH_MODE_X(if (G.x_mode_fd <= 0) G.x_mode_fd = dup_CLOEXEC(2, 10);)
 		break;
 	case 'o':
 		if (!o_opt) {