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;