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>
This commit is contained in:
Denys Vlasenko 2017-08-29 13:38:30 +02:00
parent 0d1eaf407c
commit 238ff98bb8
5 changed files with 133 additions and 15 deletions

View File

@ -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

View File

@ -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 -- /' \

View File

@ -906,6 +906,9 @@ struct globals {
#if ENABLE_HUSH_LOOPS #if ENABLE_HUSH_LOOPS
unsigned depth_break_continue; unsigned depth_break_continue;
unsigned depth_of_loop; unsigned depth_of_loop;
#endif
#if ENABLE_HUSH_GETOPTS
unsigned getopt_count;
#endif #endif
const char *ifs; const char *ifs;
const char *cwd; const char *cwd;
@ -2214,6 +2217,10 @@ static int set_local_var(char *str, unsigned flags)
cur->flg_export = 1; cur->flg_export = 1;
if (name_len == 4 && cur->varstr[0] == 'P' && cur->varstr[1] == 'S') if (name_len == 4 && cur->varstr[0] == 'P' && cur->varstr[1] == 'S')
cmdedit_update_prompt(); cmdedit_update_prompt();
#if ENABLE_HUSH_GETOPTS
if (strncmp(cur->varstr, "OPTIND=", 7) == 0)
G.getopt_count = 0;
#endif
if (cur->flg_export) { if (cur->flg_export) {
if (flags & SETFLAG_UNEXPORT) { if (flags & SETFLAG_UNEXPORT) {
cur->flg_export = 0; cur->flg_export = 0;
@ -2244,6 +2251,10 @@ static int unset_local_var_len(const char *name, int name_len)
if (!name) if (!name)
return EXIT_SUCCESS; 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; var_pp = &G.top_var;
while ((cur = *var_pp) != NULL) { while ((cur = *var_pp) != NULL) {
if (strncmp(cur->varstr, name, name_len) == 0 && cur->varstr[name_len] == '=') { if (strncmp(cur->varstr, name, name_len) == 0 && cur->varstr[name_len] == '=') {
@ -9889,7 +9900,8 @@ Test that VAR is a valid variable name?
*/ */
char cbuf[2]; char cbuf[2];
const char *cp, *optstring, *var; const char *cp, *optstring, *var;
int c, exitcode; int c, n, exitcode, my_opterr;
unsigned count;
optstring = *++argv; optstring = *++argv;
if (!optstring || !(var = *++argv)) { if (!optstring || !(var = *++argv)) {
@ -9897,17 +9909,18 @@ Test that VAR is a valid variable name?
return EXIT_FAILURE; 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] != ':') { if (optstring[0] != ':') {
cp = get_local_var_value("OPTERR"); cp = get_local_var_value("OPTERR");
/* 0 if "OPTERR=0", 1 otherwise */ /* 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 */ /* getopts stops on first non-option. Add "+" to force that */
/*if (optstring[0] != '+')*/ { /*if (optstring[0] != '+')*/ {
@ -9916,11 +9929,47 @@ Test that VAR is a valid variable name?
optstring = s; optstring = s;
} }
if (argv[1]) /* Naively, now we should just
argv[0] = G.global_argv[0]; /* for error messages */ * cp = get_local_var_value("OPTIND");
else * optind = cp ? atoi(cp) : 0;
argv = G.global_argv; * optarg = NULL;
c = getopt(string_array_len(argv), argv, optstring); * 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 */ /* Set OPTARG */
/* Always set or unset, never left as-is, even on exit/error: /* Always set or unset, never left as-is, even on exit/error:
@ -9949,10 +9998,9 @@ Test that VAR is a valid variable name?
c = '?'; c = '?';
} }
/* Set VAR and OPTIND */ /* Set VAR */
cbuf[0] = c; cbuf[0] = c;
set_local_var_from_halves(var, cbuf); set_local_var_from_halves(var, cbuf);
set_local_var_from_halves("OPTIND", utoa(optind));
return exitcode; return exitcode;
} }

View File

@ -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

View File

@ -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 -- /' \