hush: add support for "set -o pipefail"

function                                             old     new   delta
checkjobs                                            467     517     +50
builtin_set                                          259     286     +27
o_opt_strings                                          -      10     +10
hush_main                                           1011    1013      +2
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 3/0 up/down: 89/0)               Total: 89 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/shell/hush.c b/shell/hush.c
index 126aaf0..50e9ce3 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -507,6 +507,7 @@
 # define CMD_FUNCDEF 3
 #endif
 
+	smalluint cmd_exitcode;
 	/* if non-NULL, this "command" is { list }, ( list ), or a compound statement */
 	struct pipe *group;
 #if !BB_MMU
@@ -637,6 +638,43 @@
 #endif
 
 
+/* set -/+o OPT support. (TODO: make it optional)
+ * bash supports the following opts:
+ * allexport       off
+ * braceexpand     on
+ * emacs           on
+ * errexit         off
+ * errtrace        off
+ * functrace       off
+ * hashall         on
+ * histexpand      off
+ * history         on
+ * ignoreeof       off
+ * interactive-comments    on
+ * keyword         off
+ * monitor         on
+ * noclobber       off
+ * noexec          off
+ * noglob          off
+ * nolog           off
+ * notify          off
+ * nounset         off
+ * onecmd          off
+ * physical        off
+ * pipefail        off
+ * posix           off
+ * privileged      off
+ * verbose         off
+ * vi              off
+ * xtrace          off
+ */
+static const char o_opt_strings[] ALIGN1 = "pipefail\0";
+enum {
+	OPT_O_PIPEFAIL,
+	NUM_OPT_O
+};
+
+
 /* "Globals" within this file */
 /* Sorted roughly by size (smaller offsets == smaller code) */
 struct globals {
@@ -675,6 +713,7 @@
 	int last_jobid;
 	pid_t saved_tty_pgrp;
 	struct pipe *job_list;
+	char o_opt[NUM_OPT_O];
 # define G_saved_tty_pgrp (G.saved_tty_pgrp)
 #else
 # define G_saved_tty_pgrp 0
@@ -6315,24 +6354,23 @@
 				if (fg_pipe->cmds[i].pid != childpid)
 					continue;
 				if (dead) {
+					int ex;
 					fg_pipe->cmds[i].pid = 0;
 					fg_pipe->alive_cmds--;
-					if (i == fg_pipe->num_cmds - 1) {
-						/* last process gives overall exitstatus */
-						rcode = WEXITSTATUS(status);
-						/* bash prints killer signal's name for *last*
-						 * process in pipe (prints just newline for SIGINT).
-						 * Mimic this. Example: "sleep 5" + (^\ or kill -QUIT)
-						 */
-						if (WIFSIGNALED(status)) {
-							int sig = WTERMSIG(status);
+					ex = WEXITSTATUS(status);
+					/* bash prints killer signal's name for *last*
+					 * process in pipe (prints just newline for SIGINT).
+					 * Mimic this. Example: "sleep 5" + (^\ or kill -QUIT)
+					 */
+					if (WIFSIGNALED(status)) {
+						int sig = WTERMSIG(status);
+						if (i == fg_pipe->num_cmds-1)
 							printf("%s\n", sig == SIGINT ? "" : get_signame(sig));
-							/* TODO: MIPS has 128 sigs (1..128), what if sig==128 here?
-							 * Maybe we need to use sig | 128? */
-							rcode = sig + 128;
-						}
-						IF_HAS_KEYWORDS(if (fg_pipe->pi_inverted) rcode = !rcode;)
+						/* TODO: MIPS has 128 sigs (1..128), what if sig==128 here?
+						 * Maybe we need to use sig | 128? */
+						ex = sig + 128;
 					}
+					fg_pipe->cmds[i].cmd_exitcode = ex;
 				} else {
 					fg_pipe->cmds[i].is_stopped = 1;
 					fg_pipe->stopped_cmds++;
@@ -6341,6 +6379,15 @@
 						fg_pipe->alive_cmds, fg_pipe->stopped_cmds);
 				if (fg_pipe->alive_cmds == fg_pipe->stopped_cmds) {
 					/* All processes in fg pipe have exited or stopped */
+					i = fg_pipe->num_cmds;
+					while (--i >= 0) {
+						rcode = fg_pipe->cmds[i].cmd_exitcode;
+						/* usually last process gives overall exitstatus,
+						 * but with "set -o pipefail", last *failed* process does */
+						if (G.o_opt[OPT_O_PIPEFAIL] == 0 || rcode != 0)
+							break;
+					}
+					IF_HAS_KEYWORDS(if (fg_pipe->pi_inverted) rcode = !rcode;)
 /* Note: *non-interactive* bash does not continue if all processes in fg pipe
  * are stopped. Testcase: "cat | cat" in a script (not on command line!)
  * and "killall -STOP cat" */
@@ -7340,13 +7387,41 @@
 }
 #endif
 
-static int set_mode(const char cstate, const char mode)
+static int set_mode(int state, char mode, const char *o_opt)
 {
-	int state = (cstate == '-' ? 1 : 0);
+	int idx;
 	switch (mode) {
-		case 'n': G.n_mode = state; break;
-		case 'x': IF_HUSH_MODE_X(G_x_mode = state;) break;
-		default:  return EXIT_FAILURE;
+	case 'n':
+		G.n_mode = state;
+		break;
+	case 'x':
+		IF_HUSH_MODE_X(G_x_mode = state;)
+		break;
+	case 'o':
+		if (!o_opt) {
+			/* "set -+o" without parameter.
+			 * in bash, set -o produces this output:
+			 *  pipefail        off
+			 * and set +o:
+			 *  set +o pipefail
+			 * We always use the second form.
+			 */
+			const char *p = o_opt_strings;
+			idx = 0;
+			while (*p) {
+				printf("set %co %s\n", (G.o_opt[idx] ? '-' : '+'), p);
+				idx++;
+				p += strlen(p) + 1;
+			}
+			break;
+		}
+		idx = index_in_strings(o_opt_strings, o_opt);
+		if (idx >= 0) {
+			G.o_opt[idx] = state;
+			break;
+		}
+	default:
+		return EXIT_FAILURE;
 	}
 	return EXIT_SUCCESS;
 }
@@ -7586,7 +7661,7 @@
 #endif
 		case 'n':
 		case 'x':
-			if (set_mode('-', opt) == 0) /* no error */
+			if (set_mode(1, opt, NULL) == 0) /* no error */
 				break;
 		default:
 #ifndef BB_VER
@@ -8376,15 +8451,18 @@
 	}
 
 	do {
-		if (!strcmp(arg, "--")) {
+		if (strcmp(arg, "--") == 0) {
 			++argv;
 			goto set_argv;
 		}
 		if (arg[0] != '+' && arg[0] != '-')
 			break;
-		for (n = 1; arg[n]; ++n)
-			if (set_mode(arg[0], arg[n]))
+		for (n = 1; arg[n]; ++n) {
+			if (set_mode((arg[0] == '-'), arg[n], argv[1]))
 				goto error;
+			if (arg[n] == 'o' && argv[1])
+				argv++;
+		}
 	} while ((arg = *++argv) != NULL);
 	/* Now argv[0] is 1st argument */
 
diff --git a/shell/hush_test/hush-misc/pipefail.right b/shell/hush_test/hush-misc/pipefail.right
new file mode 100644
index 0000000..5845d89
--- /dev/null
+++ b/shell/hush_test/hush-misc/pipefail.right
@@ -0,0 +1,40 @@
+Default:
+true | true:
+0
+1
+true | false:
+1
+0
+false | true:
+0
+1
+exit 2 | exit 3 | exit 4:
+4
+0
+Pipefail on:
+true | true:
+0
+1
+true | false:
+1
+0
+false | true:
+1
+0
+exit 2 | exit 3 | exit 4:
+4
+0
+Pipefail off:
+true | true:
+0
+1
+true | false:
+1
+0
+false | true:
+0
+1
+exit 2 | exit 3 | exit 4:
+4
+0
+Done
diff --git a/shell/hush_test/hush-misc/pipefail.tests b/shell/hush_test/hush-misc/pipefail.tests
new file mode 100755
index 0000000..9df8418
--- /dev/null
+++ b/shell/hush_test/hush-misc/pipefail.tests
@@ -0,0 +1,45 @@
+echo Default:
+echo "true | true:"
+  true | true; echo $?
+! true | true; echo $?
+echo "true | false:"
+  true | false; echo $?
+! true | false; echo $?
+echo "false | true:"
+  false | true; echo $?
+! false | true; echo $?
+echo "exit 2 | exit 3 | exit 4:"
+  exit 2 | exit 3 | exit 4; echo $?
+! exit 2 | exit 3 | exit 4; echo $?
+
+echo Pipefail on:
+set -o pipefail
+echo "true | true:"
+  true | true; echo $?
+! true | true; echo $?
+echo "true | false:"
+  true | false; echo $?
+! true | false; echo $?
+echo "false | true:"
+  false | true; echo $?
+! false | true; echo $?
+echo "exit 2 | exit 3 | exit 4:"
+  exit 2 | exit 3 | exit 4; echo $?
+! exit 2 | exit 3 | exit 4; echo $?
+
+echo Pipefail off:
+set +o pipefail
+echo "true | true:"
+  true | true; echo $?
+! true | true; echo $?
+echo "true | false:"
+  true | false; echo $?
+! true | false; echo $?
+echo "false | true:"
+  false | true; echo $?
+! false | true; echo $?
+echo "exit 2 | exit 3 | exit 4:"
+  exit 2 | exit 3 | exit 4; echo $?
+! exit 2 | exit 3 | exit 4; echo $?
+
+echo Done