The expected behaviour for the following expansions is to always result
in b='0 1':
  $ IFS=' ' a=(a b) b=${!a[@]}   # (1)
  $ IFS=' ' a=(a b) b="${!a[@]}" # (1q)
  $ IFS='+' a=(a b) b=${!a[@]}   # (2)
  $ IFS='+' a=(a b) b="${!a[@]}" # (2q)
  $ IFS=''  a=(a b) b=${!a[@]}   # (3)
  $ IFS=''  a=(a b) b="${!a[@]}" # (3q)

And that is the behaviour we get in bash 5.0; however, in bash 5.1,
there seems to have been a regression: while (1q) "quoted; space IFS",
(3q) "quoted; null IFS, no splitting", and (2) "unquoted; nonspace IFS"
still work correctly, (2q) incorrectly results in  b='0+1'  which we
would expect from b="${!a[*]}" or, equivalently, b=${!a[*]}.

  $ declare -p BASH_VERSION
  declare -- BASH_VERSION="5.3.9(1)-release"
  $ IFS=' ' a=(a b) b=${!a[@]}  ; declare -p b # (1)
  declare -- b="0 1"
  $ IFS=' ' a=(a b) b="${!a[@]}"; declare -p b # (1q)
  declare -- b="0 1"
  $ IFS='+' a=(a b) b=${!a[@]}  ; declare -p b # (2)
  declare -- b="0 1"
  $ IFS='+' a=(a b) b="${!a[@]}"; declare -p b # (2q) <<<<WRONG>>>>
  declare -- b="0+1"
  $ IFS=''  a=(a b) b=${!a[@]}  ; declare -p b # (3)
  declare -- b="0 1"
  $ IFS=''  a=(a b) b="${!a[@]}"; declare -p b # (3q)
  declare -- b="0 1"

The reason why ${!a[@]} expands incorrectly when quoted as the RHS of an
assignment is that..

  subst.c:10331:parameter_brace_expand():
            temp = array_keys (temp1, quoted, pflags);

  arrayfunc.c:1719:array_keys()
    retval = string_list_pos_params (t[0], l, quoted, pflags);

..in string_list_pos_params() (subst.c:3200), the check at line 3236
takes precedence over the one at 3247 in presence of a quoted @, which
causes string_list_dollar_at() to be invoked with pflags overrode as 0
(so, without PF_ASSIGNRHS):

  3236:
    else if (pchar == '@' && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
    /* We use string_list_dollar_at, but only if the string is quoted, since
       that quotes the escapes if it's not, which we don't want.  We could
       use string_list (the old code did), but that doesn't do the right
       thing if the first character of $IFS is not a space.  We use
       string_list_dollar_star if the string is unquoted so we make sure that
       the elements of $@ are separated by the first character of $IFS for
       later splitting. */
  2347:
    else if (pchar == '@' && quoted == 0 && (pflags & PF_ASSIGNRHS))
    /* XXX - param_expand uses quoted|Q_DOUBLE_QUOTES for this case, but
       that quotes the escapes. We could use string_list_internal with " "
       as the second argument. */
    ret = string_list_dollar_at (list, quoted, pflags); /* Posix interp 888 */

The current order of these cases does not seem logical to me; it is
probably incorrect: I would expect the PF_ASSIGNRHS case not to care
about whether the expansion is quoted, and thus also to take precedence
over the case for double quoted expansions outside of assignments.

Unless I am missing something, reordering those two cases seems to fix
the regression; so, the fix probably looks similar to something like
this:

====

diff --git a/subst.c b/subst.c
index e4dffd34..5cf87496 100644
--- a/subst.c
+++ b/subst.c
@@ -3233,6 +3233,11 @@ string_list_pos_params (int pchar, WORD_LIST *list, int 
quoted, int pflags)
         separator. */
       ret = string_list_dollar_star (list, quoted, 0);
     }
+  else if (pchar == '@' && (pflags & PF_ASSIGNRHS))
+    /* XXX - param_expand uses quoted|Q_DOUBLE_QUOTES for this case, but
+       that quotes the escapes. We could use string_list_internal with " "
+       as the second argument. */
+    ret = string_list_dollar_at (list, quoted, pflags);        /* Posix interp 
888 */
   else if (pchar == '@' && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
     /* We use string_list_dollar_at, but only if the string is quoted, since
        that quotes the escapes if it's not, which we don't want.  We could
@@ -3244,11 +3249,6 @@ string_list_pos_params (int pchar, WORD_LIST *list, int 
quoted, int pflags)
     ret = string_list_dollar_at (list, quoted, 0);
   else if (pchar == '@' && quoted == 0 && ifs_is_null) /* XXX */
     ret = string_list_dollar_at (list, quoted, 0);     /* Posix interp 888 */
-  else if (pchar == '@' && quoted == 0 && (pflags & PF_ASSIGNRHS))
-    /* XXX - param_expand uses quoted|Q_DOUBLE_QUOTES for this case, but
-       that quotes the escapes. We could use string_list_internal with " "
-       as the second argument. */
-    ret = string_list_dollar_at (list, quoted, pflags);        /* Posix interp 
888 */
   else if (pchar == '@' && quoted == 0 && (pflags & PF_ASSIGNRHS) == 0 &&
           ifs_is_set && ifs_is_null == 0 &&
 #if defined (HANDLE_MULTIBYTE)

====

o/
 emanuele6
diff --git a/subst.c b/subst.c
index e4dffd34..5cf87496 100644
--- a/subst.c
+++ b/subst.c
@@ -3233,6 +3233,11 @@ string_list_pos_params (int pchar, WORD_LIST *list, int 
quoted, int pflags)
         separator. */
       ret = string_list_dollar_star (list, quoted, 0);
     }
+  else if (pchar == '@' && (pflags & PF_ASSIGNRHS))
+    /* XXX - param_expand uses quoted|Q_DOUBLE_QUOTES for this case, but
+       that quotes the escapes. We could use string_list_internal with " "
+       as the second argument. */
+    ret = string_list_dollar_at (list, quoted, pflags);        /* Posix interp 
888 */
   else if (pchar == '@' && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
     /* We use string_list_dollar_at, but only if the string is quoted, since
        that quotes the escapes if it's not, which we don't want.  We could
@@ -3244,11 +3249,6 @@ string_list_pos_params (int pchar, WORD_LIST *list, int 
quoted, int pflags)
     ret = string_list_dollar_at (list, quoted, 0);
   else if (pchar == '@' && quoted == 0 && ifs_is_null) /* XXX */
     ret = string_list_dollar_at (list, quoted, 0);     /* Posix interp 888 */
-  else if (pchar == '@' && quoted == 0 && (pflags & PF_ASSIGNRHS))
-    /* XXX - param_expand uses quoted|Q_DOUBLE_QUOTES for this case, but
-       that quotes the escapes. We could use string_list_internal with " "
-       as the second argument. */
-    ret = string_list_dollar_at (list, quoted, pflags);        /* Posix interp 
888 */
   else if (pchar == '@' && quoted == 0 && (pflags & PF_ASSIGNRHS) == 0 &&
           ifs_is_set && ifs_is_null == 0 &&
 #if defined (HANDLE_MULTIBYTE)

Reply via email to