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;
 

Reply via email to