Module Name: src
Committed By: kre
Date: Wed Apr 10 08:13:11 UTC 2019
Modified Files:
src/bin/sh: expand.c
src/tests/bin/sh: t_expand.sh
Log Message:
PR bin/54112
Fix handling of "$@" (that is, double quoted dollar at), when it
appears in a string which will be subject to field splitting.
Eg:
${0+"$@" }
More common usages, like the simple "$@" or ${0+"$@"} end up
being entirely quoted, so no field splitting happens, and the
problem was avoided.
See the PR for more details.
This ends up making a bunch of old hack code (and some that was
relatively new) vanish - for now it is just #if 0'd or commented out.
Cleanups of that stuff will happen later.
That some of the worst $@ hacks are now gone does not mean that processing
of "$@" does not retain a very special place in every hackers heart.
RIP extreme ugliness - long live the merely ordinary ugly.
Added a new bin/sh ATF test case to verify that all this remains fixed.
To generate a diff of this commit:
cvs rdiff -u -r1.131 -r1.132 src/bin/sh/expand.c
cvs rdiff -u -r1.20 -r1.21 src/tests/bin/sh/t_expand.sh
Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.
Modified files:
Index: src/bin/sh/expand.c
diff -u src/bin/sh/expand.c:1.131 src/bin/sh/expand.c:1.132
--- src/bin/sh/expand.c:1.131 Wed Feb 27 04:10:56 2019
+++ src/bin/sh/expand.c Wed Apr 10 08:13:11 2019
@@ -1,4 +1,4 @@
-/* $NetBSD: expand.c,v 1.131 2019/02/27 04:10:56 kre Exp $ */
+/* $NetBSD: expand.c,v 1.132 2019/04/10 08:13:11 kre Exp $ */
/*-
* Copyright (c) 1991, 1993
@@ -37,7 +37,7 @@
#if 0
static char sccsid[] = "@(#)expand.c 8.5 (Berkeley) 5/15/95";
#else
-__RCSID("$NetBSD: expand.c,v 1.131 2019/02/27 04:10:56 kre Exp $");
+__RCSID("$NetBSD: expand.c,v 1.132 2019/04/10 08:13:11 kre Exp $");
#endif
#endif /* not lint */
@@ -97,6 +97,8 @@ struct ifsregion ifsfirst; /* first stru
struct ifsregion *ifslastp; /* last struct in list */
struct arglist exparg; /* holds expanded arg list */
+static int empty_dollar_at; /* have expanded "$@" to nothing */
+
STATIC const char *argstr(const char *, int);
STATIC const char *exptilde(const char *, int);
STATIC void expbackq(union node *, int, int);
@@ -180,6 +182,7 @@ expandarg(union node *arg, struct arglis
if (fflag) /* no filename expandsion */
flag &= ~EXP_GLOB;
+ empty_dollar_at = 0;
argbackq = arg->narg.backquote;
STARTSTACKSTR(expdest);
ifsfirst.next = NULL;
@@ -243,6 +246,8 @@ argstr(const char *p, int flag)
char c;
const int quotes = flag & EXP_QNEEDED; /* do CTLESC */
int firsteq = 1;
+ int had_dol_at = 0;
+ int startoff;
const char *ifs = NULL;
int ifs_split = EXP_IFS_SPLIT;
@@ -251,6 +256,7 @@ argstr(const char *p, int flag)
CTRACE(DBG_EXPAND, ("argstr(\"%s\", %#x) quotes=%#x\n", p,flag,quotes));
+ startoff = expdest - stackblock();
if (*p == '~' && (flag & (EXP_TILDE | EXP_VARTILDE)))
p = exptilde(p, flag);
for (;;) {
@@ -262,6 +268,8 @@ argstr(const char *p, int flag)
return p - 1;
case CTLENDVAR: /* end of expanding yyy in ${xxx-yyy} */
case CTLENDARI: /* end of a $(( )) string */
+ if (had_dol_at && (*p&0xFF) == CTLQUOTEEND)
+ p++;
NULLTERM_4_TRACE(expdest);
VTRACE(DBG_EXPAND, ("argstr returning at \"%.6s\"..."
" after %2.2X; added \"%s\" to expdest\n",
@@ -270,8 +278,12 @@ argstr(const char *p, int flag)
case CTLQUOTEMARK:
/* "$@" syntax adherence hack */
if (p[0] == CTLVAR && p[1] & VSQUOTE &&
- p[2] == '@' && p[3] == '=')
+ p[2] == '@' && p[3] == '=') {
+ had_dol_at = 1;
break;
+ }
+ had_dol_at = 0;
+ empty_dollar_at = 0;
if ((flag & EXP_SPLIT) != 0)
STPUTC(c, expdest);
ifs_split = 0;
@@ -285,9 +297,14 @@ argstr(const char *p, int flag)
STPUTC('\n', expdest); /* no line_number++ */
break;
case CTLQUOTEEND:
- if ((flag & EXP_SPLIT) != 0)
+ if (empty_dollar_at &&
+ expdest - stackblock() > startoff &&
+ expdest[-1] == CTLQUOTEMARK)
+ expdest--;
+ else if (!had_dol_at && (flag & EXP_SPLIT) != 0)
STPUTC(c, expdest);
ifs_split = EXP_IFS_SPLIT;
+ had_dol_at = 0;
break;
case CTLESC:
if (quotes || ISCTL(*p))
@@ -890,6 +907,8 @@ evalvar(const char *p, int flag)
} else if (special) {
set = varisset(var, varflags & VSNUL);
val = NULL;
+ if (!set && *var == '@')
+ empty_dollar_at = 1;
} else {
val = lookupvar(var);
if (val == NULL || ((varflags & VSNUL) && val[0] == '\0')) {
@@ -916,9 +935,11 @@ evalvar(const char *p, int flag)
}
}
+#if 0 /* no longer need this $@ evil ... */
if (!set && subtype != VSPLUS && special && *var == '@')
if (startloc > 0 && expdest[-1] == CTLQUOTEMARK)
expdest--, startloc--;
+#endif
if (set && subtype != VSPLUS) {
/* insert the value of the variable */
@@ -1202,13 +1223,23 @@ varvalue(const char *name, int quoted, i
if (flag & EXP_SPLIT && quoted) {
VTRACE(DBG_EXPAND, (": $@ split (%d)\n",
shellparam.nparam));
+#if 0
/* GROSS HACK */
if (shellparam.nparam == 0 &&
expdest[-1] == CTLQUOTEMARK)
expdest--;
/* KCAH SSORG */
+#endif
+ if (shellparam.nparam == 0)
+ empty_dollar_at = 1;
+
for (ap = shellparam.p ; (p = *ap++) != NULL ; ) {
- STRTODEST(p);
+ if (*p == '\0') {
+ /* retain an explicit null string */
+ STPUTC(CTLQUOTEMARK, expdest);
+ STPUTC(CTLQUOTEEND, expdest);
+ } else
+ STRTODEST(p);
if (*ap)
/* A NUL separates args inside "" */
STPUTC('\0', expdest);
@@ -1396,8 +1427,10 @@ ifsbreakup(char *string, struct arglist
}
}
+/*
while (*start == CTLQUOTEEND)
start++;
+*/
/*
* Save anything left as an argument.
Index: src/tests/bin/sh/t_expand.sh
diff -u src/tests/bin/sh/t_expand.sh:1.20 src/tests/bin/sh/t_expand.sh:1.21
--- src/tests/bin/sh/t_expand.sh:1.20 Tue Nov 27 09:59:30 2018
+++ src/tests/bin/sh/t_expand.sh Wed Apr 10 08:13:11 2019
@@ -1,4 +1,4 @@
-# $NetBSD: t_expand.sh,v 1.20 2018/11/27 09:59:30 kre Exp $
+# $NetBSD: t_expand.sh,v 1.21 2019/04/10 08:13:11 kre Exp $
#
# Copyright (c) 2007, 2009 The NetBSD Foundation, Inc.
# All rights reserved.
@@ -46,7 +46,7 @@ delim_argv() {
atf_test_case dollar_at
dollar_at_head() {
- atf_set "descr" "Somewhere between 2.0.2 and 3.0 the expansion" \
+ atf_set descr "Somewhere between 2.0.2 and 3.0 the expansion" \
"of the \$@ variable had been broken. Check for" \
"this behavior."
}
@@ -65,7 +65,7 @@ dollar_at_body() {
atf_test_case dollar_at_unquoted_or_conditional
dollar_at_unquoted_or_conditional_head() {
- atf_set "descr" 'Sometime during 2013 the expansion of "${1+$@}"' \
+ atf_set descr 'Sometime during 2013 the expansion of "${1+$@}"' \
' (where $1 and $2 (and maybe more) are set)' \
' seems to have broken. Check for this bug.'
}
@@ -87,7 +87,7 @@ dollar_at_unquoted_or_conditional_body()
atf_test_case dollar_at_with_text
dollar_at_with_text_head() {
- atf_set "descr" "Test \$@ expansion when it is surrounded by text" \
+ atf_set descr "Test \$@ expansion when it is surrounded by text" \
"within the quotes. PR bin/33956."
}
dollar_at_with_text_body() {
@@ -160,7 +160,7 @@ EOF
atf_test_case dollar_at_empty_and_conditional
dollar_at_empty_and_conditional_head() {
- atf_set "descr" 'Test $@ expansion when there are no args, and ' \
+ atf_set descr 'Test $@ expansion when there are no args, and ' \
'when conditionally expanded.'
}
dollar_at_empty_and_conditional_body() {
@@ -355,7 +355,7 @@ EOF
atf_test_case strip
strip_head() {
- atf_set "descr" "Checks that the %% operator works and strips" \
+ atf_set descr "Checks that the %% operator works and strips" \
"the contents of a variable from the given point" \
"to the end"
}
@@ -385,7 +385,7 @@ strip_body() {
atf_test_case wrap_strip
wrap_strip_head() {
- atf_set "descr" "Checks that the %% operator works and strips" \
+ atf_set descr "Checks that the %% operator works and strips" \
"the contents of a variable from the given point" \
'to the end, and that \ \n sequences do not break it'
}
@@ -429,7 +429,7 @@ ne%\
atf_test_case tilde
tilde_head() {
- atf_set "descr" "Checks that the ~ expansions work"
+ atf_set descr "Checks that the ~ expansions work"
}
tilde_body() {
for HOME in '' / /home/foo \
@@ -489,7 +489,7 @@ ${U4:=~/:~/}\t'"${HOME}"'/:'"${HOME}"'/\
atf_test_case varpattern_backslashes
varpattern_backslashes_head() {
- atf_set "descr" "Tests that protecting wildcards with backslashes" \
+ atf_set descr "Tests that protecting wildcards with backslashes" \
"works in variable patterns."
}
varpattern_backslashes_body() {
@@ -501,7 +501,7 @@ varpattern_backslashes_body() {
atf_test_case arithmetic
arithmetic_head() {
- atf_set "descr" "POSIX requires shell arithmetic to use signed" \
+ atf_set descr "POSIX requires shell arithmetic to use signed" \
"long or a wider type. We use intmax_t, so at" \
"least 64 bits should be available. Make sure" \
"this is true."
@@ -518,7 +518,7 @@ arithmetic_body() {
atf_test_case iteration_on_null_parameter
iteration_on_null_parameter_head() {
- atf_set "descr" "Check iteration of \$@ in for loop when set to null;" \
+ atf_set descr "Check iteration of \$@ in for loop when set to null;" \
"the error \"sh: @: parameter not set\" is incorrect." \
"PR bin/48202."
}
@@ -529,7 +529,7 @@ iteration_on_null_parameter_body() {
atf_test_case iteration_on_quoted_null_parameter
iteration_on_quoted_null_parameter_head() {
- atf_set "descr" \
+ atf_set descr \
'Check iteration of "$@" in for loop when set to null;'
}
iteration_on_quoted_null_parameter_body() {
@@ -539,7 +539,7 @@ iteration_on_quoted_null_parameter_body(
atf_test_case iteration_on_null_or_null_parameter
iteration_on_null_or_null_parameter_head() {
- atf_set "descr" \
+ atf_set descr \
'Check expansion of null parameter as default for another null'
}
iteration_on_null_or_null_parameter_body() {
@@ -549,7 +549,7 @@ iteration_on_null_or_null_parameter_body
atf_test_case iteration_on_null_or_missing_parameter
iteration_on_null_or_missing_parameter_head() {
- atf_set "descr" \
+ atf_set descr \
'Check expansion of missing parameter as default for another null'
}
iteration_on_null_or_missing_parameter_body() {
@@ -666,7 +666,7 @@ results()
atf_test_case shell_params
shell_params_head() {
- atf_set "descr" "Test correct operation of the numeric parameters"
+ atf_set descr "Test correct operation of the numeric parameters"
}
shell_params_body() {
atf_require_prog mktemp
@@ -725,7 +725,7 @@ shell_params_body() {
atf_test_case var_with_embedded_cmdsub
var_with_embedded_cmdsub_head() {
- atf_set "descr" "Test expansion of vars with embedded cmdsub"
+ atf_set descr "Test expansion of vars with embedded cmdsub"
}
var_with_embedded_cmdsub_body() {
@@ -782,7 +782,7 @@ var_with_embedded_cmdsub_body() {
atf_test_case dollar_hash
dollar_hash_head() {
- atf_set "descr" 'Test expansion of various aspects of $#'
+ atf_set descr 'Test expansion of various aspects of $#'
}
dollar_hash_body() {
@@ -994,7 +994,7 @@ dollar_hash_body() {
atf_test_case dollar_star
dollar_star_head() {
- atf_set "descr" 'Test expansion of various aspects of $*'
+ atf_set descr 'Test expansion of various aspects of $*'
}
dollar_star_body() {
@@ -1038,7 +1038,7 @@ dollar_star_body() {
atf_test_case dollar_star_in_word
dollar_star_in_word_head() {
- atf_set "descr" 'Test expansion $* occurring in word of ${var:-word}'
+ atf_set descr 'Test expansion $* occurring in word of ${var:-word}'
}
dollar_star_in_word_body() {
@@ -1090,7 +1090,7 @@ dollar_star_in_word_body() {
atf_test_case dollar_star_with_empty_ifs
dollar_star_with_empty_ifs_head() {
- atf_set "descr" 'Test expansion of $* with IFS=""'
+ atf_set descr 'Test expansion of $* with IFS=""'
}
dollar_star_with_empty_ifs_body() {
@@ -1124,7 +1124,7 @@ dollar_star_with_empty_ifs_body() {
atf_test_case dollar_star_in_word_empty_ifs
dollar_star_in_word_empty_ifs_head() {
- atf_set "descr" 'Test expansion of ${unset:-$*} with IFS=""'
+ atf_set descr 'Test expansion of ${unset:-$*} with IFS=""'
}
dollar_star_in_word_empty_ifs_body() {
@@ -1166,7 +1166,7 @@ dollar_star_in_word_empty_ifs_body() {
atf_test_case dollar_star_in_quoted_word
dollar_star_in_quoted_word_head() {
- atf_set "descr" 'Test expansion $* occurring in word of ${var:-"word"}'
+ atf_set descr 'Test expansion $* occurring in word of ${var:-"word"}'
}
dollar_star_in_quoted_word_body() {
@@ -1224,9 +1224,65 @@ dollar_star_in_quoted_word_body() {
results # FIXED: 'PR bin/52090 - 2 of 26 subtests expected to fail'
}
+atf_test_case dollar_at_in_field_split_context
+dollar_at_in_field_split_context_head() {
+ atf_set descr 'Test "$@" wth field splitting -- PR bin/54112'
+}
+dollar_at_in_field_split_context_body() {
+ reset dollar_at_in_field_split_context
+
+ # the simple case (no field split) which always worked
+ check 'set -- ""; set -- ${0+"$@"}; echo $#' 1 0 #1
+
+ # The original failure case from the bash-bug list
+ check 'set -- ""; set -- ${0+"$@" "$@"}; echo $#' 2 0 #2
+
+ # slightly simpler cases that triggered the same issue
+ check 'set -- ""; set -- ${0+"$@" }; echo $#' 1 0 #3
+ check 'set -- ""; set -- ${0+ "$@"}; echo $#' 1 0 #4
+ check 'set -- ""; set -- ${0+ "$@" }; echo $#' 1 0 #5
+
+ # and the bizarre
+ check 'set -- ""; set -- ${0+"$@" "$@" "$@"}; echo $#' 3 0 #6
+
+ # repeat tests when there is more than one set empty numeric param
+
+ check 'set -- "" ""; set -- ${0+"$@"}; echo $#' 2 0 #7
+ check 'set -- "" ""; set -- ${0+"$@" "$@"}; echo $#' 4 0 #8
+ check 'set -- "" ""; set -- ${0+"$@" }; echo $#' 2 0 #9
+ check 'set -- "" ""; set -- ${0+ "$@"}; echo $#' 2 0 #10
+ check 'set -- "" ""; set -- ${0+ "$@" }; echo $#' 2 0 #11
+ check 'set -- "" ""; set -- ${0+"$@" "$@" "$@"}; echo $#' \
+ 6 0 #12
+
+ # Next some checks of the way the NetBSD shell
+ # interprets some expressions that are POSIX unspecified.
+ # Other shells might fail these tests, without that
+ # being a problem. We retain these tests so accidental
+ # changes in our behaviour can be detected.
+
+ check 'set --; X=; set -- "$X$@"; echo $#' 0 0 #13
+ check 'set --; X=; set -- "$@$X"; echo $#' 0 0 #14
+ check 'set --; X=; set -- "$X$@$X"; echo $#' 0 0 #15
+ check 'set --; X=; set -- "$@$@"; echo $#' 0 0 #16
+
+ check 'set -- ""; X=; set -- "$X$@"; echo $#' 1 0 #17
+ check 'set -- ""; X=; set -- "$@$X"; echo $#' 1 0 #19
+ check 'set -- ""; X=; set -- "$X$@$X"; echo $#' 1 0 #19
+ check 'set -- ""; X=; set -- "$@$@"; echo $#' 1 0 #20
+
+ check 'set -- "" ""; X=; set -- "$X$@"; echo $#' 2 0 #21
+ check 'set -- "" ""; X=; set -- "$@$X"; echo $#' 2 0 #22
+ check 'set -- "" ""; X=; set -- "$X$@$X"; echo $#' 2 0 #23
+ # Yes, this next one really is (and should be) 3...
+ check 'set -- "" ""; X=; set -- "$@$@"; echo $#' 3 0 #24
+
+ results
+}
+
atf_test_case embedded_nl
embedded_nl_head() {
- atf_set "descr" 'Test literal \n in xxx string in ${var-xxx}'
+ atf_set descr 'Test literal \n in xxx string in ${var-xxx}'
}
embedded_nl_body() {
@@ -1281,9 +1337,10 @@ atf_init_test_cases() {
atf_add_test_case arithmetic
atf_add_test_case dollar_at
+ atf_add_test_case dollar_at_empty_and_conditional
+ atf_add_test_case dollar_at_in_field_split_context
atf_add_test_case dollar_at_unquoted_or_conditional
atf_add_test_case dollar_at_with_text
- atf_add_test_case dollar_at_empty_and_conditional
atf_add_test_case dollar_hash
atf_add_test_case dollar_star
atf_add_test_case dollar_star_in_quoted_word