Attached patch demonstrates a solution that solves the current issues
of unset discussed in
https://lists.gnu.org/archive/html/bug-bash/2021-03/msg00056.html
while avoiding breakage of scripts and keeping the expansion of
subscripts consistent with expansion in references.
The patched code is also available in
https://github.com/konsolebox/bash/tree/skip_expansion_of_valid_array_refs_in_unset.
The patch is applied against the latest commit (f3a35a2d60) in master
branch. The changes can be conveniently compared in
https://git.io/JOgrE.
The solution allows the following tests to pass. A few of them were
described to fail in the other thread.
-------------------------
#!/bin/bash
declare -A a
key='$(echo foo)'
# Here the tokens are valid array references and are not expanded.
# The references are completely similar to the assignments.
# This solves the surprise expansion issues.
a[$key]=1
unset a[$key]
declare -p a # Displays no element
a['$key']=2
unset a['$key']
declare -p a # Displays no element
a["foo"]=3
unset a["foo"]
declare -p a # Displays no element
echo -----
# Here the tokens are "strings". They expand and keep the
# original behavior and allows existing scripts to not break.
# It also allows nref or iref references to be transparently
# referenced in it.
a[$key]=1
unset 'a[$key]' # Transforms to a[$key] after expansion
declare -p a # Displays no element
a['$key']=2
unset "a['\$key']" # Transforms to a['$key'] after expansion
declare -p a # Displays no element
a["foo"]=3
unset 'a["foo"]' # Transforms to a["foo"] after expansion
declare -p a # Displays no element
echo -----
# The update also keeps compatibility with already existing behavior of
# unset when assoc_expand_once is enabled, but only for quoted tokens.
# I would prefer it totally removed though.
# Also notice assoc_expand_once's limitation below.
shopt -s assoc_expand_once
a[$key]=1
unset "a[$key]"
declare -p a # Displays no element
a['$key']=2
unset "a[\$key]"
declare -p a # Displays no element
a["foo"]=3
unset "a[foo]"
declare -p a # Displays no element
echo ----------
# For unsetting '@' and all elements:
key=@
declare -A a=(@ v0 . v1)
unset a[$key]
declare -p a # Displays 'declare -A a=([.]="v1" )'
declare -A a=(@ v0 . v1)
unset a[@]
declare -p a # Displays 'declare: a: not found'
echo -----
shopt -u assoc_expand_once
declare -A a=(@ v0 . v1)
unset 'a[$key]'
declare -p a # Displays 'declare -A a=([.]="v1" )'
declare -A a=(@ v0 . v1)
unset 'a[@]'
declare -p a # Displays 'declare: a: not found'
echo -----
shopt -s assoc_expand_once
declare -A a=(@ v0 . v1)
unset "a[$key]"
declare -p a # Displays 'declare: a: not found' (A limitation)
declare -A a=(@ v0 . v1)
unset 'a[@]'
declare -p a # Displays 'declare: a: not found'
--
konsolebox
diff --git a/builtins/set.def b/builtins/set.def
index 8ee0165..6ef1d17 100644
--- a/builtins/set.def
+++ b/builtins/set.def
@@ -872,10 +872,6 @@ unset_builtin (list)
else if (unset_function && nameref)
nameref = 0;
-#if defined (ARRAY_VARS)
- vflags = assoc_expand_once ? (VA_NOEXPAND|VA_ONEWORD) : 0;
-#endif
-
while (list)
{
SHELL_VAR *var;
@@ -890,6 +886,9 @@ unset_builtin (list)
unset_variable = global_unset_var;
#if defined (ARRAY_VARS)
+ vflags = (assoc_expand_once && (list->word->flags & W_NOEXPAND) == 0) ?
+ (VA_NOEXPAND|VA_ONEWORD) : 0;
+
unset_array = 0;
/* XXX valid array reference second arg was 0 */
if (!unset_function && nameref == 0 && valid_array_reference (name, vflags))
diff --git a/command.h b/command.h
index 914198f..4dafcf0 100644
--- a/command.h
+++ b/command.h
@@ -104,6 +104,7 @@ enum command_type { cm_for, cm_case, cm_while, cm_if, cm_simple, cm_select,
#define W_CHKLOCAL (1 << 28) /* check for local vars on assignment */
#define W_NOASSNTILDE (1 << 29) /* don't do tilde expansion like an assignment statement */
#define W_FORCELOCAL (1 << 30) /* force assignments to be to local variables, non-fatal on assignment errors */
+#define W_NOEXPAND (1 << 31) /* inhibits any form of expansion */
/* Flags for the `pflags' argument to param_expand() and various
parameter_brace_expand_xxx functions; also used for string_list_dollar_at */
diff --git a/parser.h b/parser.h
index 59bddac..a70481a 100644
--- a/parser.h
+++ b/parser.h
@@ -48,6 +48,7 @@
#define PST_REDIRLIST 0x080000 /* parsing a list of redirections preceding a simple command name */
#define PST_COMMENT 0x100000 /* parsing a shell comment; used by aliases */
#define PST_ENDALIAS 0x200000 /* just finished expanding and consuming an alias */
+#define PST_UNSET 0x400000
/* Definition of the delimiter stack. Needed by parse.y and bashhist.c. */
struct dstack {
diff --git a/subst.c b/subst.c
index 6132316..da391d2 100644
--- a/subst.c
+++ b/subst.c
@@ -4378,6 +4378,8 @@ dequote_list (list)
for (tlist = list; tlist; tlist = tlist->next)
{
+ if (tlist->word->flags & W_NOEXPAND)
+ continue;
s = dequote_string (tlist->word->word);
if (QUOTED_NULL (tlist->word->word))
tlist->word->flags &= ~W_HASQUOTEDNULL;
@@ -11391,9 +11393,11 @@ glob_expand_word_list (tlist, eflags)
words are freed. */
next = tlist->next;
+ if (tlist->word->flags & W_NOEXPAND)
+ PREPEND_LIST (tlist, output_list);
/* If the word isn't an assignment and contains an unquoted
pattern matching character, then glob it. */
- if ((tlist->word->flags & W_NOGLOB) == 0 &&
+ else if ((tlist->word->flags & W_NOGLOB) == 0 &&
unquoted_glob_pattern_p (tlist->word->word))
{
glob_array = shell_glob_filename (tlist->word->word, QGLOB_CTLESC); /* XXX */
@@ -11487,7 +11491,7 @@ brace_expand_word_list (tlist, eflags)
{
next = tlist->next;
- if (tlist->word->flags & W_NOBRACE)
+ if (tlist->word->flags & (W_NOBRACE|W_NOEXPAND))
{
/*itrace("brace_expand_word_list: %s: W_NOBRACE", tlist->word->word);*/
PREPEND_LIST (tlist, output_list);
@@ -11863,6 +11867,12 @@ shell_expand_word_list (tlist, eflags)
next = tlist->next;
+ if (tlist->word->flags & W_NOEXPAND)
+ {
+ new_list = make_word_list (copy_word (tlist->word), new_list);
+ continue;
+ }
+
#if defined (ARRAY_VARS)
/* If this is a compound array assignment to a builtin that accepts
such assignments (e.g., `declare'), take the assignment and perform
diff --git a/y.tab.c b/y.tab.c
index dcc5b7f..a9ae0e6 100644
--- a/y.tab.c
+++ b/y.tab.c
@@ -7700,7 +7700,15 @@ got_token:
parser_state |= PST_ASSIGNOK;
else if (STREQ (token, "eval") || STREQ (token, "let"))
parser_state |= PST_ASSIGNOK;
+#if defined (ARRAY_VARS)
+ else if (STREQ (token, "unset"))
+ parser_state |= PST_UNSET;
+#endif
}
+#if defined (ARRAY_VARS)
+ else if (parser_state & PST_UNSET && valid_array_reference (token, 0))
+ the_word->flags |= W_NOEXPAND;
+#endif
yylval.word = the_word;