ash: improve set -x to quote strings as necessary

Basen on the patch from Martijn Dekker <martijn@inlv.org>

function                                             old     new   delta
evalcommand                                         1161    1302    +141
maybe_single_quote                                     -      60     +60
getoptscmd                                           527     546     +19
readtoken1                                          2819    2823      +4
localcmd                                             366     364      -2
evaltreenr                                           495     479     -16
evaltree                                             495     479     -16
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 3/3 up/down: 224/-34)           Total: 190 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/shell/ash.c b/shell/ash.c
index b28731e..a461bb7 100644
--- a/shell/ash.c
+++ b/shell/ash.c
@@ -1742,7 +1742,7 @@
 }
 
 /*
- * Produce a possibly single quoted string suitable as input to the shell.
+ * Produce a single quoted string suitable as input to the shell.
  * The return string is allocated on the stack.
  */
 static char *
@@ -1786,6 +1786,47 @@
 	return stackblock();
 }
 
+/*
+ * Produce a possibly single quoted string suitable as input to the shell.
+ * If 'conditional' is nonzero, quoting is only done if the string contains
+ * non-shellsafe characters, or is identical to a shell keyword (reserved
+ * word); if it is zero, quoting is always done.
+ * If quoting was done, the return string is allocated on the stack,
+ * otherwise a pointer to the original string is returned.
+ */
+static const char *
+maybe_single_quote(const char *s)
+{
+	const char *p = s;
+
+	while (*p) {
+		/* Assuming ACSII */
+		/* quote ctrl_chars space !"#$%&'()* */
+		if (*p < '+')
+			goto need_quoting;
+		/* quote ;<=>? */
+		if (*p >= ';' && *p <= '?')
+			goto need_quoting;
+		/* quote `[\ */
+		if (*p == '`')
+			goto need_quoting;
+		if (*p == '[')
+			goto need_quoting;
+		if (*p == '\\')
+			goto need_quoting;
+		/* quote {|}~ DEL and high bytes */
+		if (*p > 'z')
+			goto need_quoting;
+		/* Not quoting these: +,-./ 0-9 :@ A-Z ]^_ a-z */
+		/* TODO: maybe avoid quoting % */
+		p++;
+	}
+	return s;
+
+ need_quoting:
+	return single_quote(s);
+}
+
 
 /* ============ nextopt */
 
@@ -9700,18 +9741,36 @@
 
 	/* Print the command if xflag is set. */
 	if (xflag) {
-		int n;
-		const char *p = " %s" + 1;
+		const char *pfx = "";
 
-		fdprintf(preverrout_fd, p, expandstr(ps4val()));
+		fdprintf(preverrout_fd, "%s", expandstr(ps4val()));
+
 		sp = varlist.list;
-		for (n = 0; n < 2; n++) {
-			while (sp) {
-				fdprintf(preverrout_fd, p, sp->text);
-				sp = sp->next;
-				p = " %s";
-			}
-			sp = arglist.list;
+		while (sp) {
+			char *varval = sp->text;
+			char *eq = strchrnul(varval, '=');
+			if (*eq)
+				eq++;
+			fdprintf(preverrout_fd, "%s%.*s%s",
+				pfx,
+				(int)(eq - varval), varval,
+				maybe_single_quote(eq)
+			);
+			sp = sp->next;
+			pfx = " ";
+		}
+
+		sp = arglist.list;
+		while (sp) {
+			fdprintf(preverrout_fd, "%s%s",
+				pfx,
+				/* always quote if matches reserved word: */
+				findkwd(sp->text)
+				? single_quote(sp->text)
+				: maybe_single_quote(sp->text)
+			);
+			sp = sp->next;
+			pfx = " ";
 		}
 		safe_write(preverrout_fd, "\n", 1);
 	}
diff --git a/shell/ash_test/ash-quoting/mode_x.right b/shell/ash_test/ash-quoting/mode_x.right
new file mode 100644
index 0000000..c2dd355
--- /dev/null
+++ b/shell/ash_test/ash-quoting/mode_x.right
@@ -0,0 +1,10 @@
++ var1=val
++ var2='one two'
++ true '%s\n' one 'two '"'"'three' four
++ this=command
++ 'this=command'
+./mode_x.tests: line 1: this=command: not found
++ true
++ true
++ 'if' true
+./mode_x.tests: line 1: if: not found
diff --git a/shell/ash_test/ash-quoting/mode_x.tests b/shell/ash_test/ash-quoting/mode_x.tests
new file mode 100755
index 0000000..16dae3f
--- /dev/null
+++ b/shell/ash_test/ash-quoting/mode_x.tests
@@ -0,0 +1,14 @@
+set -x
+
+var1=val
+var2='one two'
+true %s\\n one "two 'three" four
+
+# assignment:
+this=command
+# NOT assignment, +x code should show it quoted:
+"this=command"
+
+if true; then true; fi
+# +x code should quote 'if' here:
+"if" true