hush: fix "getopts" builtin to not be upset by other builtins calling getopt()

function                                             old     new   delta
builtin_getopts                                      363     403     +40
unset_local_var_len                                  185     215     +30
set_local_var                                        440     466     +26
reset_traps_to_defaults                              151     157      +6
pseudo_exec_argv                                     320     326      +6
install_special_sighandlers                           52      58      +6
pick_sighandler                                       62      65      +3
execvp_or_die                                         85      88      +3
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 8/0 up/down: 120/0)             Total: 120 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/shell/ash_test/ash-getopts/getopt_nested.right b/shell/ash_test/ash-getopts/getopt_nested.right
new file mode 100644
index 0000000..b0c339d
--- /dev/null
+++ b/shell/ash_test/ash-getopts/getopt_nested.right
@@ -0,0 +1,14 @@
+var:a
+var:b
+var:c
+var:a
+var:b
+var:c
+Illegal option -d
+var:?
+Illegal option -e
+var:?
+Illegal option -f
+var:?
+var:a
+End: var:? OPTIND:6
diff --git a/shell/ash_test/ash-getopts/getopt_nested.tests b/shell/ash_test/ash-getopts/getopt_nested.tests
new file mode 100755
index 0000000..1b48b40
--- /dev/null
+++ b/shell/ash_test/ash-getopts/getopt_nested.tests
@@ -0,0 +1,21 @@
+# Test that there is no interference of getopt()
+# in getopts and unset.
+# It's unclear what "correct" OPTIND values should be
+# for "b" and "c" results from "-bc": 2? 3?
+# What we focus on here is that all options are reported
+# correct number of times and in correct sequence.
+
+(
+
+loop=99
+while getopts "abc" var -a -bc -abc -def -a; do
+    echo "var:$var" #OPTIND:$OPTIND
+    # this may use getopt():
+    unset -ff func
+    test $((--loop)) = 0 && break  # BUG if this triggers
+done
+echo "End: var:$var OPTIND:$OPTIND"
+
+) 2>&1 \
+| sed   -e 's/ unrecognized option: / invalid option -- /' \
+        -e 's/ illegal option -- / invalid option -- /' \
diff --git a/shell/hush.c b/shell/hush.c
index cdc3a86..ceb8cbb 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -907,6 +907,9 @@
 	unsigned depth_break_continue;
 	unsigned depth_of_loop;
 #endif
+#if ENABLE_HUSH_GETOPTS
+	unsigned getopt_count;
+#endif
 	const char *ifs;
 	const char *cwd;
 	struct variable *top_var;
@@ -2214,6 +2217,10 @@
 		cur->flg_export = 1;
 	if (name_len == 4 && cur->varstr[0] == 'P' && cur->varstr[1] == 'S')
 		cmdedit_update_prompt();
+#if ENABLE_HUSH_GETOPTS
+	if (strncmp(cur->varstr, "OPTIND=", 7) == 0)
+		G.getopt_count = 0;
+#endif
 	if (cur->flg_export) {
 		if (flags & SETFLAG_UNEXPORT) {
 			cur->flg_export = 0;
@@ -2244,6 +2251,10 @@
 
 	if (!name)
 		return EXIT_SUCCESS;
+#if ENABLE_HUSH_GETOPTS
+	if (name_len == 6 && strncmp(name, "OPTIND", 6) == 0)
+		G.getopt_count = 0;
+#endif
 	var_pp = &G.top_var;
 	while ((cur = *var_pp) != NULL) {
 		if (strncmp(cur->varstr, name, name_len) == 0 && cur->varstr[name_len] == '=') {
@@ -9889,7 +9900,8 @@
 */
 	char cbuf[2];
 	const char *cp, *optstring, *var;
-	int c, exitcode;
+	int c, n, exitcode, my_opterr;
+	unsigned count;
 
 	optstring = *++argv;
 	if (!optstring || !(var = *++argv)) {
@@ -9897,17 +9909,18 @@
 		return EXIT_FAILURE;
 	}
 
-	c = 0;
+	if (argv[1])
+		argv[0] = G.global_argv[0]; /* for error messages in getopt() */
+	else
+		argv = G.global_argv;
+	cbuf[1] = '\0';
+
+	my_opterr = 0;
 	if (optstring[0] != ':') {
 		cp = get_local_var_value("OPTERR");
 		/* 0 if "OPTERR=0", 1 otherwise */
-		c = (!cp || NOT_LONE_CHAR(cp, '0'));
+		my_opterr = (!cp || NOT_LONE_CHAR(cp, '0'));
 	}
-	opterr = c;
-	cp = get_local_var_value("OPTIND");
-	optind = cp ? atoi(cp) : 0;
-	optarg = NULL;
-	cbuf[1] = '\0';
 
 	/* getopts stops on first non-option. Add "+" to force that */
 	/*if (optstring[0] != '+')*/ {
@@ -9916,11 +9929,47 @@
 		optstring = s;
 	}
 
-	if (argv[1])
-		argv[0] = G.global_argv[0]; /* for error messages */
-	else
-		argv = G.global_argv;
-	c = getopt(string_array_len(argv), argv, optstring);
+	/* Naively, now we should just
+	 *	cp = get_local_var_value("OPTIND");
+	 *	optind = cp ? atoi(cp) : 0;
+	 *	optarg = NULL;
+	 *	opterr = my_opterr;
+	 *	c = getopt(string_array_len(argv), argv, optstring);
+	 * and be done? Not so fast...
+	 * Unlike normal getopt() usage in C programs, here
+	 * each successive call will (usually) have the same argv[] CONTENTS,
+	 * but not the ADDRESSES. Worse yet, it's possible that between
+	 * invocations of "getopts", there will be calls to shell builtins
+	 * which use getopt() internally. Example:
+	 *	while getopts "abc" RES -a -bc -abc de; do
+	 *		unset -ff func
+	 *	done
+	 * This would not work correctly: getopt() call inside "unset"
+	 * modifies internal libc state which is tracking position in
+	 * multi-option strings ("-abc"). At best, it can skip options
+	 * or return the same option infinitely. With glibc implementation
+	 * of getopt(), it would use outright invalid pointers and return
+	 * garbage even _without_ "unset" mangling internal state.
+	 *
+	 * We resort to resetting getopt() state and calling it N times,
+	 * until we get Nth result (or failure).
+	 * (N == G.getopt_count is reset to 0 whenever OPTIND is [un]set).
+	 */
+	optind = 0; /* reset getopt() state */
+	count = 0;
+	n = string_array_len(argv);
+	do {
+		optarg = NULL;
+		opterr = (count < G.getopt_count) ? 0 : my_opterr;
+		c = getopt(n, argv, optstring);
+		if (c < 0)
+			break;
+		count++;
+	} while (count <= G.getopt_count);
+
+	/* Set OPTIND. Prevent resetting of the magic counter! */
+	set_local_var_from_halves("OPTIND", utoa(optind));
+	G.getopt_count = count; /* "next time, give me N+1'th result" */
 
 	/* Set OPTARG */
 	/* Always set or unset, never left as-is, even on exit/error:
@@ -9949,10 +9998,9 @@
 		c = '?';
 	}
 
-	/* Set VAR and OPTIND */
+	/* Set VAR */
 	cbuf[0] = c;
 	set_local_var_from_halves(var, cbuf);
-	set_local_var_from_halves("OPTIND", utoa(optind));
 
 	return exitcode;
 }
diff --git a/shell/hush_test/hush-getopts/getopt_nested.right b/shell/hush_test/hush-getopts/getopt_nested.right
new file mode 100644
index 0000000..0218dba
--- /dev/null
+++ b/shell/hush_test/hush-getopts/getopt_nested.right
@@ -0,0 +1,14 @@
+var:a
+var:b
+var:c
+var:a
+var:b
+var:c
+./getopt_nested.tests: invalid option -- d
+var:?
+./getopt_nested.tests: invalid option -- e
+var:?
+./getopt_nested.tests: invalid option -- f
+var:?
+var:a
+End: var:? OPTIND:6
diff --git a/shell/hush_test/hush-getopts/getopt_nested.tests b/shell/hush_test/hush-getopts/getopt_nested.tests
new file mode 100755
index 0000000..1b48b40
--- /dev/null
+++ b/shell/hush_test/hush-getopts/getopt_nested.tests
@@ -0,0 +1,21 @@
+# Test that there is no interference of getopt()
+# in getopts and unset.
+# It's unclear what "correct" OPTIND values should be
+# for "b" and "c" results from "-bc": 2? 3?
+# What we focus on here is that all options are reported
+# correct number of times and in correct sequence.
+
+(
+
+loop=99
+while getopts "abc" var -a -bc -abc -def -a; do
+    echo "var:$var" #OPTIND:$OPTIND
+    # this may use getopt():
+    unset -ff func
+    test $((--loop)) = 0 && break  # BUG if this triggers
+done
+echo "End: var:$var OPTIND:$OPTIND"
+
+) 2>&1 \
+| sed   -e 's/ unrecognized option: / invalid option -- /' \
+        -e 's/ illegal option -- / invalid option -- /' \