]> git.ipfire.org Git - thirdparty/bash.git/blobdiff - subst.c
Bash-5.2 patch 26: fix typo when specifying readline's custom color prefix
[thirdparty/bash.git] / subst.c
diff --git a/subst.c b/subst.c
index dbf0157e0e89a9a94a3c5a3ea1f295516fe61315..2012e1fd9e3412740a3eae8c6a17e5f3d5c7b959 100644 (file)
--- a/subst.c
+++ b/subst.c
@@ -4,7 +4,7 @@
 /* ``Have a little faith, there's magic in the night.  You ain't a
      beauty, but, hey, you're alright.'' */
 
-/* Copyright (C) 1987-2016 Free Software Foundation, Inc.
+/* Copyright (C) 1987-2022 Free Software Foundation, Inc.
 
    This file is part of GNU Bash, the Bourne Again SHell.
 
@@ -45,6 +45,7 @@
 
 #include "shell.h"
 #include "parser.h"
+#include "redir.h"
 #include "flags.h"
 #include "jobs.h"
 #include "execute_cmd.h"
@@ -131,7 +132,6 @@ extern int errno;
    the embedded `break'. The dangling else accommodates a trailing semicolon;
    we could also put in a do ; while (0) */
 
-   
 #define CHECK_STRING_OVERRUN(oind, ind, len, ch) \
   if (ind >= len) \
     { \
@@ -144,7 +144,7 @@ extern int errno;
 /* An expansion function that takes a string and a quoted flag and returns
    a WORD_LIST *.  Used as the type of the third argument to
    expand_string_if_necessary(). */
-typedef WORD_LIST *EXPFUNC __P((char *, int));
+typedef WORD_LIST *EXPFUNC PARAMS((char *, int));
 
 /* Process ID of the last command executed within command substitution. */
 pid_t last_command_subst_pid = NO_PID;
@@ -180,36 +180,31 @@ WORD_LIST *subst_assign_varlist = (WORD_LIST *)NULL;
    errors.  Enabled when doing completion and prompt string expansion. */
 int no_longjmp_on_fatal_error = 0;
 
+/* Non-zero means to allow unmatched globbed filenames to expand to
+   a null file. */
+int allow_null_glob_expansion;
+
+/* Non-zero means to throw an error when globbing fails to match anything. */
+int fail_glob_expansion;
+
+/* If non-zero, perform `&' substitution on the replacement string in the
+   pattern substitution word expansion. */
+int patsub_replacement = 1;
+
 /* Extern functions and variables from different files. */
-extern int last_command_exit_value, last_command_exit_signal;
-extern int subshell_environment, running_in_background;
-extern int subshell_level, parse_and_execute_level, sourcelevel;
-extern int eof_encountered, line_number;
-extern int return_catch_flag, return_catch_value;
-extern pid_t dollar_dollar_pid;
-extern int posixly_correct;
-extern char *this_command_name;
 extern struct fd_bitmap *current_fds_to_close;
 extern int wordexp_only;
-extern int expanding_redir;
-extern int tempenv_assign_error;
-extern int builtin_ignoring_errexit;
+extern int singlequote_translations;
+extern int extended_quote;
 
 #if defined (JOB_CONTROL) && defined (PROCESS_SUBSTITUTION)
 extern PROCESS *last_procsub_child;
 #endif
 
 #if !defined (HAVE_WCSDUP) && defined (HANDLE_MULTIBYTE)
-extern wchar_t *wcsdup __P((const wchar_t *));
+extern wchar_t *wcsdup PARAMS((const wchar_t *));
 #endif
 
-/* Non-zero means to allow unmatched globbed filenames to expand to
-   a null file. */
-int allow_null_glob_expansion;
-
-/* Non-zero means to throw an error when globbing fails to match anything. */
-int fail_glob_expansion;
-
 #if 0
 /* Variables to keep track of which words in an expanded word list (the
    output of expand_word_list_internal) are the result of globbing
@@ -221,147 +216,162 @@ static int glob_argv_flags_size;
 
 static WORD_LIST *cached_quoted_dollar_at = 0;
 
+/* Distinguished error values to return from expansion functions */
 static WORD_LIST expand_word_error, expand_word_fatal;
 static WORD_DESC expand_wdesc_error, expand_wdesc_fatal;
-static char expand_param_error, expand_param_fatal;
+static char expand_param_error, expand_param_fatal, expand_param_unset;
 static char extract_string_error, extract_string_fatal;
 
-/* Set by expand_word_unsplit; used to inhibit splitting and re-joining
-   $* on $IFS, primarily when doing assignment statements. */
+/* Set by expand_word_unsplit and several of the expand_string_XXX functions;
+   used to inhibit splitting and re-joining $* on $IFS, primarily when doing
+   assignment statements.  The idea is that if we're in a context where this
+   is set, we're not going to be performing word splitting, so we use the same
+   rules to expand $* as we would if it appeared within double quotes. */
 static int expand_no_split_dollar_star = 0;
 
 /* A WORD_LIST of words to be expanded by expand_word_list_internal,
    without any leading variable assignments. */
 static WORD_LIST *garglist = (WORD_LIST *)NULL;
 
-static char *quoted_substring __P((char *, int, int));
-static int quoted_strlen __P((char *));
-static char *quoted_strchr __P((char *, int, int));
+static char *quoted_substring PARAMS((char *, int, int));
+static int quoted_strlen PARAMS((char *));
+static char *quoted_strchr PARAMS((char *, int, int));
 
-static char *expand_string_if_necessary __P((char *, int, EXPFUNC *));
-static inline char *expand_string_to_string_internal __P((char *, int, EXPFUNC *));
-static WORD_LIST *call_expand_word_internal __P((WORD_DESC *, int, int, int *, int *));
-static WORD_LIST *expand_string_internal __P((char *, int));
-static WORD_LIST *expand_string_leave_quoted __P((char *, int));
-static WORD_LIST *expand_string_for_rhs __P((char *, int, int *, int *));
+static char *expand_string_if_necessary PARAMS((char *, int, EXPFUNC *));
+static inline char *expand_string_to_string_internal PARAMS((char *, int, EXPFUNC *));
+static WORD_LIST *call_expand_word_internal PARAMS((WORD_DESC *, int, int, int *, int *));
+static WORD_LIST *expand_string_internal PARAMS((char *, int));
+static WORD_LIST *expand_string_leave_quoted PARAMS((char *, int));
+static WORD_LIST *expand_string_for_rhs PARAMS((char *, int, int, int, int *, int *));
+static WORD_LIST *expand_string_for_pat PARAMS((char *, int, int *, int *));
 
-static WORD_LIST *list_quote_escapes __P((WORD_LIST *));
-static WORD_LIST *list_dequote_escapes __P((WORD_LIST *));
+static char *quote_escapes_internal PARAMS((const char *, int));
 
-static char *make_quoted_char __P((int));
-static WORD_LIST *quote_list __P((WORD_LIST *));
+static WORD_LIST *list_quote_escapes PARAMS((WORD_LIST *));
+static WORD_LIST *list_dequote_escapes PARAMS((WORD_LIST *));
 
-static int unquoted_substring __P((char *, char *));
-static int unquoted_member __P((int, char *));
+static char *make_quoted_char PARAMS((int));
+static WORD_LIST *quote_list PARAMS((WORD_LIST *));
+
+static int unquoted_substring PARAMS((char *, char *));
+static int unquoted_member PARAMS((int, char *));
 
 #if defined (ARRAY_VARS)
-static SHELL_VAR *do_compound_assignment __P((char *, char *, int));
+static SHELL_VAR *do_compound_assignment PARAMS((char *, char *, int));
 #endif
-static int do_assignment_internal __P((const WORD_DESC *, int));
+static int do_assignment_internal PARAMS((const WORD_DESC *, int));
 
-static char *string_extract_verbatim __P((char *, size_t, int *, char *, int));
-static char *string_extract __P((char *, int *, char *, int));
-static char *string_extract_double_quoted __P((char *, int *, int));
-static inline char *string_extract_single_quoted __P((char *, int *));
-static inline int skip_single_quoted __P((const char *, size_t, int, int));
-static int skip_double_quoted __P((char *, size_t, int, int));
-static char *extract_delimited_string __P((char *, int *, char *, char *, char *, int));
-static char *extract_dollar_brace_string __P((char *, int *, int, int));
-static int skip_matched_pair __P((const char *, int, int, int, int));
+static char *string_extract_verbatim PARAMS((char *, size_t, int *, char *, int));
+static char *string_extract PARAMS((char *, int *, char *, int));
+static char *string_extract_double_quoted PARAMS((char *, int *, int));
+static inline char *string_extract_single_quoted PARAMS((char *, int *, int));
+static inline int skip_single_quoted PARAMS((const char *, size_t, int, int));
+static int skip_double_quoted PARAMS((char *, size_t, int, int));
+static char *extract_delimited_string PARAMS((char *, int *, char *, char *, char *, int));
+static char *extract_heredoc_dolbrace_string PARAMS((char *, int *, int, int));
+static char *extract_dollar_brace_string PARAMS((char *, int *, int, int));
+static int skip_matched_pair PARAMS((const char *, int, int, int, int));
 
-static char *pos_params __P((char *, int, int, int));
+static char *pos_params PARAMS((char *, int, int, int, int));
 
-static unsigned char *mb_getcharlens __P((char *, int));
+static unsigned char *mb_getcharlens PARAMS((char *, int));
 
-static char *remove_upattern __P((char *, char *, int));
+static char *remove_upattern PARAMS((char *, char *, int));
 #if defined (HANDLE_MULTIBYTE) 
-static wchar_t *remove_wpattern __P((wchar_t *, size_t, wchar_t *, int));
+static wchar_t *remove_wpattern PARAMS((wchar_t *, size_t, wchar_t *, int));
 #endif
-static char *remove_pattern __P((char *, char *, int));
+static char *remove_pattern PARAMS((char *, char *, int));
 
-static int match_upattern __P((char *, char *, int, char **, char **));
+static int match_upattern PARAMS((char *, char *, int, char **, char **));
 #if defined (HANDLE_MULTIBYTE)
-static int match_wpattern __P((wchar_t *, char **, size_t, wchar_t *, int, char **, char **));
-#endif
-static int match_pattern __P((char *, char *, int, char **, char **));
-static int getpatspec __P((int, char *));
-static char *getpattern __P((char *, int, int));
-static char *variable_remove_pattern __P((char *, char *, int, int));
-static char *list_remove_pattern __P((WORD_LIST *, char *, int, int, int));
-static char *parameter_list_remove_pattern __P((int, char *, int, int));
+static int match_wpattern PARAMS((wchar_t *, char **, size_t, wchar_t *, int, char **, char **));
+#endif
+static int match_pattern PARAMS((char *, char *, int, char **, char **));
+static int getpatspec PARAMS((int, char *));
+static char *getpattern PARAMS((char *, int, int));
+static char *variable_remove_pattern PARAMS((char *, char *, int, int));
+static char *list_remove_pattern PARAMS((WORD_LIST *, char *, int, int, int));
+static char *parameter_list_remove_pattern PARAMS((int, char *, int, int));
 #ifdef ARRAY_VARS
-static char *array_remove_pattern __P((SHELL_VAR *, char *, int, char *, int));
+static char *array_remove_pattern PARAMS((SHELL_VAR *, char *, int, int, int));
 #endif
-static char *parameter_brace_remove_pattern __P((char *, char *, int, char *, int, int, int));
+static char *parameter_brace_remove_pattern PARAMS((char *, char *, array_eltstate_t *, char *, int, int, int));
 
-static char *string_var_assignment __P((SHELL_VAR *, char *));
+static char *string_var_assignment PARAMS((SHELL_VAR *, char *));
 #if defined (ARRAY_VARS)
-static char *array_var_assignment __P((SHELL_VAR *, int, int));
+static char *array_var_assignment PARAMS((SHELL_VAR *, int, int, int));
 #endif
-static char *pos_params_assignment __P((WORD_LIST *, int, int));
-static char *string_transform __P((int, SHELL_VAR *, char *));
-static char *list_transform __P((int, SHELL_VAR *, WORD_LIST *, int, int));
-static char *parameter_list_transform __P((int, int, int));
+static char *pos_params_assignment PARAMS((WORD_LIST *, int, int));
+static char *string_transform PARAMS((int, SHELL_VAR *, char *));
+static char *list_transform PARAMS((int, SHELL_VAR *, WORD_LIST *, int, int));
+static char *parameter_list_transform PARAMS((int, int, int));
 #if defined ARRAY_VARS
-static char *array_transform __P((int, SHELL_VAR *, char *, int));
+static char *array_transform PARAMS((int, SHELL_VAR *, int, int));
 #endif
-static char *parameter_brace_transform __P((char *, char *, int, char *, int, int, int));
+static char *parameter_brace_transform PARAMS((char *, char *, array_eltstate_t *, char *, int, int, int, int));
+static int valid_parameter_transform PARAMS((char *));
 
-static char *process_substitute __P((char *, int));
+static char *process_substitute PARAMS((char *, int));
 
-static char *read_comsub __P((int, int, int *));
+static char *optimize_cat_file PARAMS((REDIRECT *, int, int, int *));
+static char *read_comsub PARAMS((int, int, int, int *));
 
 #ifdef ARRAY_VARS
-static arrayind_t array_length_reference __P((char *));
+static arrayind_t array_length_reference PARAMS((char *));
 #endif
 
-static int valid_brace_expansion_word __P((char *, int));
-static int chk_atstar __P((char *, int, int *, int *));
-static int chk_arithsub __P((const char *, int));
+static int valid_brace_expansion_word PARAMS((char *, int));
+static int chk_atstar PARAMS((char *, int, int, int *, int *));
+static int chk_arithsub PARAMS((const char *, int));
 
-static WORD_DESC *parameter_brace_expand_word __P((char *, int, int, int, arrayind_t *));
-static char *parameter_brace_find_indir __P((char *, int, int, int));
-static WORD_DESC *parameter_brace_expand_indir __P((char *, int, int, int *, int *));
-static WORD_DESC *parameter_brace_expand_rhs __P((char *, char *, int, int, int, int *, int *));
-static void parameter_brace_expand_error __P((char *, char *));
+static WORD_DESC *parameter_brace_expand_word PARAMS((char *, int, int, int, array_eltstate_t *));
+static char *parameter_brace_find_indir PARAMS((char *, int, int, int));
+static WORD_DESC *parameter_brace_expand_indir PARAMS((char *, int, int, int, int *, int *));
+static WORD_DESC *parameter_brace_expand_rhs PARAMS((char *, char *, int, int, int, int *, int *));
+static void parameter_brace_expand_error PARAMS((char *, char *, int));
 
-static int valid_length_expression __P((char *));
-static intmax_t parameter_brace_expand_length __P((char *));
+static int valid_length_expression PARAMS((char *));
+static intmax_t parameter_brace_expand_length PARAMS((char *));
 
-static char *skiparith __P((char *, int));
-static int verify_substring_values __P((SHELL_VAR *, char *, char *, int, intmax_t *, intmax_t *));
-static int get_var_and_type __P((char *, char *, arrayind_t, int, int, SHELL_VAR **, char **));
-static char *mb_substring __P((char *, int, int));
-static char *parameter_brace_substring __P((char *, char *, int, char *, int, int));
+static char *skiparith PARAMS((char *, int));
+static int verify_substring_values PARAMS((SHELL_VAR *, char *, char *, int, intmax_t *, intmax_t *));
+static int get_var_and_type PARAMS((char *, char *, array_eltstate_t *, int, int, SHELL_VAR **, char **));
+static char *mb_substring PARAMS((char *, int, int));
+static char *parameter_brace_substring PARAMS((char *, char *, array_eltstate_t *, char *, int, int, int));
 
-static int shouldexp_replacement __P((char *));
+static int shouldexp_replacement PARAMS((char *));
 
-static char *pos_params_pat_subst __P((char *, char *, char *, int));
+static char *pos_params_pat_subst PARAMS((char *, char *, char *, int));
 
-static char *parameter_brace_patsub __P((char *, char *, int, char *, int, int, int));
+static char *expand_string_for_patsub PARAMS((char *, int));
+static char *parameter_brace_patsub PARAMS((char *, char *, array_eltstate_t *, char *, int, int, int));
 
-static char *pos_params_casemod __P((char *, char *, int, int));
-static char *parameter_brace_casemod __P((char *, char *, int, int, char *, int, int));
+static char *pos_params_casemod PARAMS((char *, char *, int, int));
+static char *parameter_brace_casemod PARAMS((char *, char *, array_eltstate_t *, int, char *, int, int, int));
 
-static WORD_DESC *parameter_brace_expand __P((char *, int *, int, int, int *, int *));
-static WORD_DESC *param_expand __P((char *, int *, int, int *, int *, int *, int *, int));
+static WORD_DESC *parameter_brace_expand PARAMS((char *, int *, int, int, int *, int *));
+static WORD_DESC *param_expand PARAMS((char *, int *, int, int *, int *, int *, int *, int));
 
-static WORD_LIST *expand_word_internal __P((WORD_DESC *, int, int, int *, int *));
+static WORD_LIST *expand_word_internal PARAMS((WORD_DESC *, int, int, int *, int *));
 
-static WORD_LIST *word_list_split __P((WORD_LIST *));
+static WORD_LIST *word_list_split PARAMS((WORD_LIST *));
 
-static void exp_jump_to_top_level __P((int));
+static void exp_jump_to_top_level PARAMS((int));
 
-static WORD_LIST *separate_out_assignments __P((WORD_LIST *));
-static WORD_LIST *glob_expand_word_list __P((WORD_LIST *, int));
+static WORD_LIST *separate_out_assignments PARAMS((WORD_LIST *));
+static WORD_LIST *glob_expand_word_list PARAMS((WORD_LIST *, int));
 #ifdef BRACE_EXPANSION
-static WORD_LIST *brace_expand_word_list __P((WORD_LIST *, int));
+static WORD_LIST *brace_expand_word_list PARAMS((WORD_LIST *, int));
 #endif
 #if defined (ARRAY_VARS)
-static int make_internal_declare __P((char *, char *, char *));
+static int make_internal_declare PARAMS((char *, char *, char *));
+static void expand_compound_assignment_word PARAMS((WORD_LIST *, int));
+static WORD_LIST *expand_declaration_argument PARAMS((WORD_LIST *, WORD_LIST *));
 #endif
-static WORD_LIST *shell_expand_word_list __P((WORD_LIST *, int));
-static WORD_LIST *expand_word_list_internal __P((WORD_LIST *, int));
+static WORD_LIST *shell_expand_word_list PARAMS((WORD_LIST *, int));
+static WORD_LIST *expand_word_list_internal PARAMS((WORD_LIST *, int));
+
+static int do_assignment_statements PARAMS((WORD_LIST *, char *, int));
 
 /* **************************************************************** */
 /*                                                                 */
@@ -393,10 +403,10 @@ dump_word_flags (flags)
       f &= ~W_ASSIGNARRAY;
       fprintf (stderr, "W_ASSIGNARRAY%s", f ? "|" : "");
     }
-  if (f & W_HASCTLESC)
+  if (f & W_SAWQUOTEDNULL)
     {
-      f &= ~W_HASCTLESC;
-      fprintf (stderr, "W_HASCTLESC%s", f ? "|" : "");
+      f &= ~W_SAWQUOTEDNULL;
+      fprintf (stderr, "W_SAWQUOTEDNULL%s", f ? "|" : "");
     }
   if (f & W_NOPROCSUB)
     {
@@ -433,15 +443,10 @@ dump_word_flags (flags)
       f &= ~W_COMPASSIGN;
       fprintf (stderr, "W_COMPASSIGN%s", f ? "|" : "");
     }
-  if (f & W_NOEXPAND)
-    {
-      f &= ~W_NOEXPAND;
-      fprintf (stderr, "W_NOEXPAND%s", f ? "|" : "");
-    }
-  if (f & W_ITILDE)
+  if (f & W_EXPANDRHS)
     {
-      f &= ~W_ITILDE;
-      fprintf (stderr, "W_ITILDE%s", f ? "|" : "");
+      f &= ~W_EXPANDRHS;
+      fprintf (stderr, "W_EXPANDRHS%s", f ? "|" : "");
     }
   if (f & W_NOTILDE)
     {
@@ -453,15 +458,20 @@ dump_word_flags (flags)
       f &= ~W_ASSIGNRHS;
       fprintf (stderr, "W_ASSIGNRHS%s", f ? "|" : "");
     }
+  if (f & W_NOASSNTILDE)
+    {
+      f &= ~W_NOASSNTILDE;
+      fprintf (stderr, "W_NOASSNTILDE%s", f ? "|" : "");
+    }
   if (f & W_NOCOMSUB)
     {
       f &= ~W_NOCOMSUB;
       fprintf (stderr, "W_NOCOMSUB%s", f ? "|" : "");
     }
-  if (f & W_DOLLARSTAR)
+  if (f & W_ARRAYREF)
     {
-      f &= ~W_DOLLARSTAR;
-      fprintf (stderr, "W_DOLLARSTAR%s", f ? "|" : "");
+      f &= ~W_ARRAYREF;
+      fprintf (stderr, "W_ARRAYREF%s", f ? "|" : "");
     }
   if (f & W_DOLLARAT)
     {
@@ -518,7 +528,17 @@ dump_word_flags (flags)
       f &= ~W_COMPLETE;
       fprintf (stderr, "W_COMPLETE%s", f ? "|" : "");
     }
-  
+  if (f & W_CHKLOCAL)
+    {
+      f &= ~W_CHKLOCAL;
+      fprintf (stderr, "W_CHKLOCAL%s", f ? "|" : "");
+    }
+  if (f & W_FORCELOCAL)
+    {
+      f &= ~W_FORCELOCAL;
+      fprintf (stderr, "W_FORCELOCAL%s", f ? "|" : "");
+    }
+
   fprintf (stderr, "\n");
   fflush (stderr);
 }
@@ -591,6 +611,7 @@ quoted_strlen (s)
 }
 #endif
 
+#ifdef INCLUDE_UNUSED
 /* Find the first occurrence of character C in string S, obeying shell
    quoting rules.  If (FLAGS & ST_BACKSL) is non-zero, backslash-escaped
    characters are skipped.  If (FLAGS & ST_CTLESC) is non-zero, characters
@@ -618,7 +639,6 @@ quoted_strchr (s, c, flags)
   return ((char *)NULL);
 }
 
-#if defined (INCLUDE_UNUSED)
 /* Return 1 if CHARACTER appears in an unquoted portion of
    STRING.  Return 0 otherwise.  CHARACTER must be a single-byte character. */
 static int
@@ -720,16 +740,15 @@ unquoted_substring (substr, string)
 INLINE char *
 sub_append_string (source, target, indx, size)
      char *source, *target;
-     int *indx;
+     size_t *indx;
      size_t *size;
 {
   if (source)
     {
-      int n;
-      size_t srclen;
+      size_t n, srclen;
 
       srclen = STRLEN (source);
-      if (srclen >= (int)(*size - *indx))
+      if (srclen >= (*size - *indx))
        {
          n = srclen + *indx;
          n = (n + DEFAULT_ARRAY_SIZE) - (n % DEFAULT_ARRAY_SIZE);
@@ -753,7 +772,7 @@ char *
 sub_append_number (number, target, indx, size)
      intmax_t number;
      char *target;
-     int *indx;
+     size_t *indx;
      size_t *size;
 {
   char *temp;
@@ -798,12 +817,12 @@ string_extract (string, sindex, charlist, flags)
            break;
        }
 #if defined (ARRAY_VARS)
-      else if ((flags & SX_VARNAME) && c == '[')
+      else if ((flags & SX_VARNAME) && c == LBRACK)
        {
          int ni;
          /* If this is an array subscript, skip over it and continue. */
          ni = skipsubscript (string, i, 0);
-         if (string[ni] == ']')
+         if (string[ni] == RBRACK)
            i = ni;
        }
 #endif
@@ -880,16 +899,16 @@ string_extract_double_quoted (string, sindex, flags)
             and want to strip these backslashes only if DQUOTE is non-zero,
             indicating that we are inside an embedded double-quoted string. */
 
-            /* If we are in an embedded quoted string, then don't strip
-               backslashes before characters for which the backslash
-               retains its special meaning, but remove backslashes in
-               front of other characters.  If we are not in an
-               embedded quoted string, don't strip backslashes at all.
-               This mess is necessary because the string was already
-               surrounded by double quotes (and sh has some really weird
-               quoting rules).
-               The returned string will be run through expansion as if
-               it were double-quoted. */
+         /* If we are in an embedded quoted string, then don't strip
+            backslashes before characters for which the backslash
+            retains its special meaning, but remove backslashes in
+            front of other characters.  If we are not in an
+            embedded quoted string, don't strip backslashes at all.
+            This mess is necessary because the string was already
+            surrounded by double quotes (and sh has some really weird
+            quoting rules).
+            The returned string will be run through expansion as if
+            it were double-quoted. */
          if ((stripdq == 0 && c != '"') ||
              (stripdq && ((dquote && (sh_syntaxtab[c] & CBSDQUOTE)) || dquote == 0)))
            temp[j++] = '\\';
@@ -918,7 +937,7 @@ add_one_character:
        {
          if (c == '`')
            backquote = 0;
-         temp[j++] = c;
+         temp[j++] = c;        /* COPY_CHAR_I? */
          i++;
          continue;
        }
@@ -954,11 +973,14 @@ add_one_character:
              ret = string + i + 2;
            }
 
+         /* XXX - CHECK_STRING_OVERRUN here? */
          for (t = 0; ret[t]; t++, j++)
            temp[j] = ret[t];
          temp[j] = string[si];
 
-         if (string[si])
+         if (si < i + 2)       /* we went back? */
+           i += 2;
+         else if (string[si])
            {
              j++;
              i = si + 1;
@@ -1070,22 +1092,38 @@ skip_double_quoted (string, slen, sind, flags)
 /* Extract the contents of STRING as if it is enclosed in single quotes.
    SINDEX, when passed in, is the offset of the character immediately
    following the opening single quote; on exit, SINDEX is left pointing after
-   the closing single quote. */
+   the closing single quote. ALLOWESC allows the single quote to be quoted by
+   a backslash; it's not used yet. */
 static inline char *
-string_extract_single_quoted (string, sindex)
+string_extract_single_quoted (string, sindex, allowesc)
      char *string;
      int *sindex;
+     int allowesc;
 {
   register int i;
   size_t slen;
   char *t;
+  int pass_next;
   DECLARE_MBSTATE;
 
   /* Don't need slen for ADVANCE_CHAR unless multibyte chars possible. */
   slen = (MB_CUR_MAX > 1) ? strlen (string + *sindex) + *sindex : 0;
   i = *sindex;
-  while (string[i] && string[i] != '\'')
-    ADVANCE_CHAR (string, slen, i);
+  pass_next = 0;
+  while (string[i])
+    {
+      if (pass_next)
+       {
+         pass_next = 0;
+         ADVANCE_CHAR (string, slen, i);
+         continue;
+       }
+      if (allowesc && string[i] == '\\')
+       pass_next++;
+      else if (string[i] == '\'')
+        break;
+      ADVANCE_CHAR (string, slen, i);
+    }
 
   t = substring (string, *sindex, i);
 
@@ -1134,7 +1172,6 @@ string_extract_verbatim (string, slen, sindex, charlist, flags)
 {
   register int i;
 #if defined (HANDLE_MULTIBYTE)
-  size_t clen;
   wchar_t *wcharlist;
 #endif
   int c;
@@ -1143,19 +1180,25 @@ string_extract_verbatim (string, slen, sindex, charlist, flags)
 
   if ((flags & SX_NOCTLESC) && charlist[0] == '\'' && charlist[1] == '\0')
     {
-      temp = string_extract_single_quoted (string, sindex);
+      temp = string_extract_single_quoted (string, sindex, 0);
       --*sindex;       /* leave *sindex at separator character */
       return temp;
     }
 
+  /* This can never be called with charlist == NULL. If *charlist == NULL,
+     we can skip the loop and just return a copy of the string, updating
+     *sindex */
+  if (*charlist == 0)
+    {
+      temp = string + *sindex;
+      c = (*sindex == 0) ? slen : STRLEN (temp);
+      temp = savestring (temp);
+      *sindex += c;
+      return temp;
+    }
+
   i = *sindex;
-#if 0
-  /* See how the MBLEN and ADVANCE_CHAR macros work to understand why we need
-     this only if MB_CUR_MAX > 1. */
-  slen = (MB_CUR_MAX > 1) ? strlen (string + *sindex) + *sindex : 1;
-#endif
 #if defined (HANDLE_MULTIBYTE)
-  clen = strlen (charlist);
   wcharlist = 0;
 #endif
   while (c = string[i])
@@ -1166,6 +1209,7 @@ string_extract_verbatim (string, slen, sindex, charlist, flags)
       if ((flags & SX_NOCTLESC) == 0 && c == CTLESC)
        {
          i += 2;
+         CHECK_STRING_OVERRUN (i, i, slen, c);
          continue;
        }
       /* Even if flags contains SX_NOCTLESC, we let CTLESC quoting CTLNUL
@@ -1174,11 +1218,15 @@ string_extract_verbatim (string, slen, sindex, charlist, flags)
       else if ((flags & SX_NOESCCTLNUL) == 0 && c == CTLESC && string[i+1] == CTLNUL)
        {
          i += 2;
+         CHECK_STRING_OVERRUN (i, i, slen, c);
          continue;
        }
 
 #if defined (HANDLE_MULTIBYTE)
-      mblength = MBLEN (string + i, slen - i);
+      if (locale_utf8locale && slen > i && UTF8_SINGLEBYTE (string[i]))
+       mblength = (string[i] != 0) ? 1 : 0;
+      else
+       mblength = MBLEN (string + i, slen - i);
       if (mblength > 1)
        {
          wchar_t wc;
@@ -1267,6 +1315,7 @@ extract_process_subst (string, starter, sindex, xflags)
      int xflags;
 {
 #if 0
+  /* XXX - check xflags&SX_COMPLETE here? */
   return (extract_delimited_string (string, sindex, starter, "(", ")", SX_COMMAND));
 #else
   xflags |= (no_longjmp_on_fatal_error ? SX_NOLONGJMP : 0);
@@ -1287,8 +1336,8 @@ extract_array_assignment_list (string, sindex)
   int slen;
   char *ret;
 
-  slen = strlen (string);      /* ( */
-  if (string[slen - 1] == ')')
+  slen = strlen (string);
+  if (string[slen - 1] == RPAREN)
    {
       ret = substring (string, *sindex, slen - 1);
       *sindex = slen - 1;
@@ -1383,6 +1432,7 @@ extract_delimited_string (string, sindex, opener, alt_opener, closer, flags)
         {
           si = i + 2;
           t = extract_command_subst (string, &si, flags|SX_NOALLOC);
+          CHECK_STRING_OVERRUN (i, si, slen, c);
           i = si + 1;
           continue;
         }
@@ -1392,6 +1442,7 @@ extract_delimited_string (string, sindex, opener, alt_opener, closer, flags)
        {
          si = i + len_opener;
          t = extract_delimited_string (string, &si, opener, alt_opener, closer, flags|SX_NOALLOC);
+         CHECK_STRING_OVERRUN (i, si, slen, c);
          i = si + 1;
          continue;
        }
@@ -1401,6 +1452,7 @@ extract_delimited_string (string, sindex, opener, alt_opener, closer, flags)
        {
          si = i + len_alt_opener;
          t = extract_delimited_string (string, &si, alt_opener, alt_opener, closer, flags|SX_NOALLOC);
+         CHECK_STRING_OVERRUN (i, si, slen, c);
          i = si + 1;
          continue;
        }
@@ -1420,6 +1472,7 @@ extract_delimited_string (string, sindex, opener, alt_opener, closer, flags)
        {
          si = i + 1;
          t = string_extract (string, &si, "`", flags|SX_NOALLOC);
+         CHECK_STRING_OVERRUN (i, si, slen, c);
          i = si + 1;
          continue;
        }
@@ -1466,6 +1519,288 @@ extract_delimited_string (string, sindex, opener, alt_opener, closer, flags)
   return (result);
 }
 
+/* A simplified version of extract_dollar_brace_string that exists to handle
+   $'...' and $"..." quoting in here-documents, since the here-document read
+   path doesn't. It's separate because we don't want to mess with the fast
+   common path. We already know we're going to allocate and return a new
+   string and quoted == Q_HERE_DOCUMENT. We might be able to cut it down
+   some more, but extracting strings and adding them as we go adds complexity.
+   This needs to match the logic in parse.y:parse_matched_pair so we get
+   consistent behavior between here-documents and double-quoted strings. */
+static char *
+extract_heredoc_dolbrace_string (string, sindex, quoted, flags)
+     char *string;
+     int *sindex, quoted, flags;
+{
+  register int i, c;
+  size_t slen, tlen, result_index, result_size;
+  int pass_character, nesting_level, si, dolbrace_state;
+  char *result, *t, *send;
+  DECLARE_MBSTATE;
+
+  pass_character = 0;
+  nesting_level = 1;
+  slen = strlen (string + *sindex) + *sindex;
+  send = string + slen;
+
+  result_size = slen;
+  result_index = 0;
+  result = xmalloc (result_size + 1);
+
+  /* This function isn't called if this condition is not true initially. */
+  dolbrace_state = DOLBRACE_QUOTE;
+
+  i = *sindex;
+  while (c = string[i])
+    {
+      if (pass_character)
+       {
+         pass_character = 0;
+         RESIZE_MALLOCED_BUFFER (result, result_index, locale_mb_cur_max + 1, result_size, 64);
+         COPY_CHAR_I (result, result_index, string, send, i);
+         continue;
+       }
+
+      /* CTLESCs and backslashes quote the next character. */
+      if (c == CTLESC || c == '\\')
+       {
+         pass_character++;
+         RESIZE_MALLOCED_BUFFER (result, result_index, 2, result_size, 64);
+         result[result_index++] = c;
+         i++;
+         continue;
+       }
+
+      /* The entire reason we have this separate function right here. */
+      if (c == '$' && string[i+1] == '\'')
+       {
+         char *ttrans;
+         int ttranslen;
+
+         if ((posixly_correct || extended_quote == 0) && dolbrace_state != DOLBRACE_QUOTE && dolbrace_state != DOLBRACE_QUOTE2)
+           {
+             RESIZE_MALLOCED_BUFFER (result, result_index, 3, result_size, 64);
+             result[result_index++] = '$';
+             result[result_index++] = '\'';
+             i += 2;
+             continue;
+           }
+
+         si = i + 2;
+         t = string_extract_single_quoted (string, &si, 1);    /* XXX */
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+
+         tlen = si - i - 2;    /* -2 since si is one after the close quote */
+         ttrans = ansiexpand (t, 0, tlen, &ttranslen);
+         free (t);
+
+         /* needed to correctly quote any embedded single quotes. */
+         if (dolbrace_state == DOLBRACE_QUOTE || dolbrace_state == DOLBRACE_QUOTE2)
+           {
+             t = sh_single_quote (ttrans);
+             tlen = strlen (t);
+             free (ttrans);
+           }
+         else if (extended_quote) /* dolbrace_state == DOLBRACE_PARAM */
+           {
+             /* This matches what parse.y:parse_matched_pair() does */
+             t = ttrans;
+             tlen = strlen (t);
+           }
+
+         RESIZE_MALLOCED_BUFFER (result, result_index, tlen + 1, result_size, 64);
+         strncpy (result + result_index, t, tlen);
+         result_index += tlen;
+         free (t);
+         i = si;
+         continue;
+       }
+
+#if defined (TRANSLATABLE_STRINGS)
+      if (c == '$' && string[i+1] == '"')
+       {
+         char *ttrans;
+         int ttranslen;
+
+         si = i + 2;
+         t = string_extract_double_quoted (string, &si, flags);        /* XXX */
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+
+         tlen = si - i - 2;    /* -2 since si is one after the close quote */
+         ttrans = locale_expand (t, 0, tlen, line_number, &ttranslen);
+         free (t);
+
+         t = singlequote_translations ? sh_single_quote (ttrans) : sh_mkdoublequoted (ttrans, ttranslen, 0);
+         tlen = strlen (t);
+         free (ttrans);
+
+         RESIZE_MALLOCED_BUFFER (result, result_index, tlen + 1, result_size, 64);
+         strncpy (result + result_index, t, tlen);
+         result_index += tlen;
+         free (t);
+         i = si;
+         continue;
+       }
+#endif /* TRANSLATABLE_STRINGS */
+
+      if (c == '$' && string[i+1] == LBRACE)
+       {
+         nesting_level++;
+         RESIZE_MALLOCED_BUFFER (result, result_index, 3, result_size, 64);
+         result[result_index++] = c;
+         result[result_index++] = string[i+1];
+         i += 2;
+         if (dolbrace_state == DOLBRACE_QUOTE || dolbrace_state == DOLBRACE_QUOTE2 || dolbrace_state == DOLBRACE_WORD)
+           dolbrace_state = DOLBRACE_PARAM;
+         continue;
+       }
+
+      if (c == RBRACE)
+       {
+         nesting_level--;
+         if (nesting_level == 0)
+           break;
+         RESIZE_MALLOCED_BUFFER (result, result_index, 2, result_size, 64);
+         result[result_index++] = c;
+         i++;
+         continue;
+       }
+
+      /* Pass the contents of old-style command substitutions through
+        verbatim. */
+      if (c == '`')
+       {
+         si = i + 1;
+         t = string_extract (string, &si, "`", flags); /* already know (flags & SX_NOALLOC) == 0) */
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+
+         tlen = si - i - 1;
+         RESIZE_MALLOCED_BUFFER (result, result_index, tlen + 3, result_size, 64);
+         result[result_index++] = c;
+         strncpy (result + result_index, t, tlen);
+         result_index += tlen;
+         result[result_index++] = string[si];
+         free (t);
+         i = si + 1;
+         continue;
+       }
+
+      /* Pass the contents of new-style command substitutions and
+        arithmetic substitutions through verbatim. */
+      if (string[i] == '$' && string[i+1] == LPAREN)
+       {
+         si = i + 2;
+         t = extract_command_subst (string, &si, flags);
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+
+         tlen = si - i - 2;
+         RESIZE_MALLOCED_BUFFER (result, result_index, tlen + 4, result_size, 64);
+         result[result_index++] = c;
+         result[result_index++] = LPAREN;
+         strncpy (result + result_index, t, tlen);
+         result_index += tlen;
+         result[result_index++] = string[si];
+         free (t);
+         i = si + 1;
+         continue;
+       }
+
+#if defined (PROCESS_SUBSTITUTION)
+      /* Technically this should only work at the start of a word */
+      if ((string[i] == '<' || string[i] == '>') && string[i+1] == LPAREN)
+       {
+         si = i + 2;
+         t = extract_process_subst (string, (string[i] == '<' ? "<(" : ">)"), &si, flags);
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+
+         tlen = si - i - 2;
+         RESIZE_MALLOCED_BUFFER (result, result_index, tlen + 4, result_size, 64);
+         result[result_index++] = c;
+         result[result_index++] = LPAREN;
+         strncpy (result + result_index, t, tlen);
+         result_index += tlen;
+         result[result_index++] = string[si];
+         free (t);
+         i = si + 1;
+         continue;
+       }
+#endif
+
+      if (c == '\'' && posixly_correct && shell_compatibility_level > 42 && dolbrace_state != DOLBRACE_QUOTE)
+       {
+         COPY_CHAR_I (result, result_index, string, send, i);
+         continue;
+       }
+
+      /* Pass the contents of single and double-quoted strings through verbatim. */
+      if (c == '"' || c == '\'')
+       {
+         si = i + 1;
+         if (c == '"')
+           t = string_extract_double_quoted (string, &si, flags);
+         else
+           t = string_extract_single_quoted (string, &si, 0);
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+
+         tlen = si - i - 2;    /* -2 since si is one after the close quote */
+         RESIZE_MALLOCED_BUFFER (result, result_index, tlen + 3, result_size, 64);
+         result[result_index++] = c;
+         strncpy (result + result_index, t, tlen);
+         result_index += tlen;
+         result[result_index++] = string[si - 1];
+         free (t);
+         i = si;
+         continue;
+       }
+
+      /* copy this character, which was not special. */
+      COPY_CHAR_I (result, result_index, string, send, i);
+
+      /* This logic must agree with parse.y:parse_matched_pair, since they
+        share the same defines. */
+      if (dolbrace_state == DOLBRACE_PARAM && c == '%' && (i - *sindex) > 1)
+       dolbrace_state = DOLBRACE_QUOTE;
+      else if (dolbrace_state == DOLBRACE_PARAM && c == '#' && (i - *sindex) > 1)
+        dolbrace_state = DOLBRACE_QUOTE;
+      else if (dolbrace_state == DOLBRACE_PARAM && c == '/' && (i - *sindex) > 1)
+        dolbrace_state = DOLBRACE_QUOTE2;      /* XXX */
+      else if (dolbrace_state == DOLBRACE_PARAM && c == '^' && (i - *sindex) > 1)
+        dolbrace_state = DOLBRACE_QUOTE;
+      else if (dolbrace_state == DOLBRACE_PARAM && c == ',' && (i - *sindex) > 1)
+        dolbrace_state = DOLBRACE_QUOTE;
+      /* This is intended to handle all of the [:]op expansions and the substring/
+        length/pattern removal/pattern substitution expansions. */
+      else if (dolbrace_state == DOLBRACE_PARAM && strchr ("#%^,~:-=?+/", c) != 0)
+       dolbrace_state = DOLBRACE_OP;
+      else if (dolbrace_state == DOLBRACE_OP && strchr ("#%^,~:-=?+/", c) == 0)
+       dolbrace_state = DOLBRACE_WORD;
+    }
+
+  if (c == 0 && nesting_level)
+    {
+      free (result);
+      if (no_longjmp_on_fatal_error == 0)
+       {                       /* { */
+         last_command_exit_value = EXECUTION_FAILURE;
+         report_error (_("bad substitution: no closing `%s' in %s"), "}", string);
+         exp_jump_to_top_level (DISCARD);
+       }
+      else
+       {
+         *sindex = i;
+         return ((char *)NULL);
+       }
+    }
+
+  *sindex = i;
+  result[result_index] = '\0';
+
+  return (result);
+}
+
+#define PARAMEXPNEST_MAX       32      // for now
+static int dbstate[PARAMEXPNEST_MAX];
+
 /* Extract a parameter expansion expression within ${ and } from STRING.
    Obey the Posix.2 rules for finding the ending `}': count braces while
    skipping over enclosed quoted strings and command substitutions.
@@ -1485,10 +1820,6 @@ extract_dollar_brace_string (string, sindex, quoted, flags)
   char *result, *t;
   DECLARE_MBSTATE;
 
-  pass_character = 0;
-  nesting_level = 1;
-  slen = strlen (string + *sindex) + *sindex;
-
   /* The handling of dolbrace_state needs to agree with the code in parse.y:
      parse_matched_pair().  The different initial value is to handle the
      case where this function is called to parse the word in
@@ -1497,6 +1828,15 @@ extract_dollar_brace_string (string, sindex, quoted, flags)
   if ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) && (flags & SX_POSIXEXP))
     dolbrace_state = DOLBRACE_QUOTE;
 
+  if (quoted == Q_HERE_DOCUMENT && dolbrace_state == DOLBRACE_QUOTE && (flags & SX_NOALLOC) == 0)
+    return (extract_heredoc_dolbrace_string (string, sindex, quoted, flags));
+
+  dbstate[0] = dolbrace_state;
+
+  pass_character = 0;
+  nesting_level = 1;
+  slen = strlen (string + *sindex) + *sindex;
+
   i = *sindex;
   while (c = string[i])
     {
@@ -1517,8 +1857,12 @@ extract_dollar_brace_string (string, sindex, quoted, flags)
 
       if (string[i] == '$' && string[i+1] == LBRACE)
        {
+         if (nesting_level < PARAMEXPNEST_MAX)
+           dbstate[nesting_level] = dolbrace_state;
          nesting_level++;
          i += 2;
+         if (dolbrace_state == DOLBRACE_QUOTE || dolbrace_state == DOLBRACE_WORD)
+           dolbrace_state = DOLBRACE_PARAM;
          continue;
        }
 
@@ -1527,6 +1871,7 @@ extract_dollar_brace_string (string, sindex, quoted, flags)
          nesting_level--;
          if (nesting_level == 0)
            break;
+         dolbrace_state = (nesting_level < PARAMEXPNEST_MAX) ? dbstate[nesting_level] : dbstate[0];    /* Guess using initial state */
          i++;
          continue;
        }
@@ -1539,7 +1884,7 @@ extract_dollar_brace_string (string, sindex, quoted, flags)
          t = string_extract (string, &si, "`", flags|SX_NOALLOC);
 
          CHECK_STRING_OVERRUN (i, si, slen, c);
-    
+
          i = si + 1;
          continue;
        }
@@ -1550,9 +1895,26 @@ extract_dollar_brace_string (string, sindex, quoted, flags)
        {
          si = i + 2;
          t = extract_command_subst (string, &si, flags|SX_NOALLOC);
+
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+
+         i = si + 1;
+         continue;
+       }
+
+#if defined (PROCESS_SUBSTITUTION)
+      /* Technically this should only work at the start of a word */
+      if ((string[i] == '<' || string[i] == '>') && string[i+1] == LPAREN)
+       {
+         si = i + 2;
+         t = extract_process_subst (string, (string[i] == '<' ? "<(" : ">)"), &si, flags|SX_NOALLOC);
+
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+
          i = si + 1;
          continue;
        }
+#endif
 
       /* Pass the contents of double-quoted strings through verbatim. */
       if (c == '"')
@@ -1577,6 +1939,16 @@ extract_dollar_brace_string (string, sindex, quoted, flags)
           continue;
        }
 
+#if defined (ARRAY_VARS)
+      if (c == LBRACK && dolbrace_state == DOLBRACE_PARAM)
+       {
+         si = skipsubscript (string, i, 0);
+         CHECK_STRING_OVERRUN (i, si, slen, c);
+         if (string[si] == RBRACK)
+           c = string[i = si];
+       }
+#endif
+
       /* move past this character, which was not special. */
       ADVANCE_CHAR (string, slen, i);
 
@@ -1679,11 +2051,14 @@ unquote_bang (string)
 
 #define CQ_RETURN(x) do { no_longjmp_on_fatal_error = oldjmp; return (x); } while (0)
 
-/* This function assumes s[i] == open; returns with s[ret] == close; used to
-   parse array subscripts.  FLAGS & 1 means to not attempt to skip over
-   matched pairs of quotes or backquotes, or skip word expansions; it is
-   intended to be used after expansion has been performed and during final
-   assignment parsing (see arrayfunc.c:assign_compound_array_list()). */
+/* When FLAGS & 2 == 0, this function assumes STRING[I] == OPEN; when
+   FLAGS & 2 != 0, it assumes STRING[I] points to one character past OPEN;
+   returns with STRING[RET] == close; used to parse array subscripts.
+   FLAGS & 1 means not to attempt to skip over matched pairs of quotes or
+   backquotes, or skip word expansions; it is intended to be used after
+   expansion has been performed and during final assignment parsing (see
+   arrayfunc.c:assign_compound_array_list()) or during execution by a builtin
+   which has already undergone word expansion. */
 static int
 skip_matched_pair (string, start, open, close, flags)
      const char *string;
@@ -1698,7 +2073,10 @@ skip_matched_pair (string, start, open, close, flags)
   oldjmp = no_longjmp_on_fatal_error;
   no_longjmp_on_fatal_error = 1;
 
-  i = start + 1;               /* skip over leading bracket */
+  /* Move to the first character after a leading OPEN. If FLAGS&2, we assume
+    that START already points to that character. If not, we need to skip over
+    it here. */
+  i = (flags & 2) ? start : start + 1;
   count = 1;
   pass_next = backq = 0;
   ss = (char *)string;
@@ -1712,7 +2090,7 @@ skip_matched_pair (string, start, open, close, flags)
          ADVANCE_CHAR (string, slen, i);
          continue;
        }
-      else if (c == '\\')
+      else if ((flags & 1) == 0 && c == '\\')
        {
          pass_next = 1;
          i++;
@@ -1751,12 +2129,13 @@ skip_matched_pair (string, start, open, close, flags)
                          : skip_double_quoted (ss, slen, ++i, 0);
          /* no increment, the skip functions increment past the closing quote. */
        }
-      else if ((flags&1) == 0 && c == '$' && (string[i+1] == LPAREN || string[i+1] == LBRACE))
+      else if ((flags & 1) == 0 && c == '$' && (string[i+1] == LPAREN || string[i+1] == LBRACE))
        {
          si = i + 2;
          if (string[si] == '\0')
            CQ_RETURN(si);
 
+         /* XXX - extract_command_subst here? */
          if (string[i+1] == LPAREN)
            temp = extract_delimited_string (ss, &si, "$(", "(", ")", SX_NOALLOC|SX_COMMAND); /* ) */
          else
@@ -1778,6 +2157,11 @@ skip_matched_pair (string, start, open, close, flags)
 }
 
 #if defined (ARRAY_VARS)
+/* FLAGS has 1 as a reserved value, since skip_matched_pair uses it for
+   skipping over quoted strings and taking the first instance of the
+   closing character. FLAGS & 2 means that STRING[START] points one
+   character past the open bracket; FLAGS & 2 == 0 means that STRING[START]
+   points to the open bracket. skip_matched_pair knows how to deal with this. */
 int
 skipsubscript (string, start, flags)
      const char *string;
@@ -1901,9 +2285,10 @@ skip_to_delim (string, start, delims, flags)
            CQ_RETURN(si);
 
          if (string[i+1] == LPAREN)
-           temp = extract_delimited_string (string, &si, "$(", "(", ")", SX_NOALLOC|SX_COMMAND); /* ) */
+           temp = extract_delimited_string (string, &si, "$(", "(", ")", SX_NOALLOC|SX_COMMAND|completeflag); /* ) */
          else
            temp = extract_dollar_brace_string (string, &si, 0, SX_NOALLOC);
+         CHECK_STRING_OVERRUN (i, si, slen, c);
          i = si;
          if (string[i] == '\0')        /* don't increment i past EOS in loop */
            break;
@@ -1916,12 +2301,9 @@ skip_to_delim (string, start, delims, flags)
          si = i + 2;
          if (string[si] == '\0')
            CQ_RETURN(si);
-#if 1
+
          temp = extract_delimited_string (string, &si, (c == '<') ? "<(" : ">(", "(", ")", SX_COMMAND|SX_NOALLOC); /* )) */
-#else
-         temp = extract_process_subst (string, (c == '<') ? "<(" : ">(", &si, 0);
-         free (temp);          /* XXX - not using SX_NOALLOC here yet */
-#endif
+         CHECK_STRING_OVERRUN (i, si, slen, c);
          i = si;
          if (string[i] == '\0')
            break;
@@ -1941,6 +2323,7 @@ skip_to_delim (string, start, delims, flags)
          open[2] = '\0';
          temp = extract_delimited_string (string, &si, open, "(", ")", SX_NOALLOC); /* ) */
 
+         CHECK_STRING_OVERRUN (i, si, slen, c);
          i = si;
          if (string[i] == '\0')        /* don't increment i past EOS in loop */
            break;
@@ -1983,10 +2366,9 @@ skip_to_histexp (string, start, delims, flags)
      char *delims;
      int flags;
 {
-  int i, pass_next, backq, dquote, si, c, oldjmp;
+  int i, pass_next, backq, dquote, c, oldjmp;
   int histexp_comsub, histexp_backq, old_dquote;
   size_t slen;
-  char *temp, open[3];
   DECLARE_MBSTATE;
 
   slen = strlen (string + start) + start;
@@ -2115,6 +2497,21 @@ char_is_quoted (string, eindex)
   oldjmp = no_longjmp_on_fatal_error;
   no_longjmp_on_fatal_error = 1;
   i = pass_next = 0;
+
+  /* If we have an open quoted string from a previous line, see if it's
+     closed before string[eindex], so we don't interpret that close quote
+     as starting a new quoted string. */
+  if (current_command_line_count > 0 && dstack.delimiter_depth > 0)
+    {
+      c = dstack.delimiters[dstack.delimiter_depth - 1];
+      if (c == '\'')
+       i = skip_single_quoted (string, slen, 0, 0);
+      else if (c == '"')
+       i = skip_double_quoted (string, slen, 0, SX_COMPLETE);
+      if (i > eindex)
+       CQ_RETURN (1);
+    }
+
   while (i <= eindex)
     {
       c = string[i];
@@ -2209,12 +2606,12 @@ unclosed_pair (string, eindex, openstr)
    rules.  SENTINEL is an index to look for.  NWP, if non-NULL,
    gets the number of words in the returned list.  CWP, if non-NULL, gets
    the index of the word containing SENTINEL.  Non-whitespace chars in
-   DELIMS delimit separate fields. */
+   DELIMS delimit separate fields.  This is used by programmable completion. */
 WORD_LIST *
 split_at_delims (string, slen, delims, sentinel, flags, nwp, cwp)
      char *string;
      int slen;
-     char *delims;
+     const char *delims;
      int sentinel, flags;
      int *nwp, *cwp;
 {
@@ -2231,7 +2628,7 @@ split_at_delims (string, slen, delims, sentinel, flags, nwp, cwp)
       return ((WORD_LIST *)NULL);
     }
 
-  d = (delims == 0) ? ifs_value : delims;
+  d = (delims == 0) ? ifs_value : (char *)delims;
   ifs_split = delims == 0;
 
   /* Make d2 the non-whitespace characters in delims */
@@ -2302,16 +2699,16 @@ split_at_delims (string, slen, delims, sentinel, flags, nwp, cwp)
          /* If we're using IFS splitting, the non-whitespace delimiter char
             and any additional IFS whitespace delimits a field. */
          if (ifs_split)
-           while (member (string[te], d) && spctabnl (string[te]))
+           while (member (string[te], d) && spctabnl (string[te]) && ((flags&SD_NOQUOTEDELIM) == 0 || (string[te] != '\'' && string[te] != '"')))
              te++;
          else
-           while (member (string[te], d2))
+           while (member (string[te], d2) && ((flags&SD_NOQUOTEDELIM) == 0 || (string[te] != '\'' && string[te] != '"')))
              te++;
        }
 
       token = substring (string, ts, te);
 
-      ret = add_string_to_list (token, ret);
+      ret = add_string_to_list (token, ret);   /* XXX */
       free (token);
       nw++;
 
@@ -2338,7 +2735,8 @@ split_at_delims (string, slen, delims, sentinel, flags, nwp, cwp)
        break;
 
       i = te;
-      while (member (string[i], d) && (ifs_split || spctabnl(string[i])))
+      /* XXX - honor SD_NOQUOTEDELIM here */
+      while (member (string[i], d) && (ifs_split || spctabnl(string[i])) && ((flags&SD_NOQUOTEDELIM) == 0 || (string[te] != '\'' && string[te] != '"')))
        i++;
 
       if (string[i])
@@ -2501,9 +2899,12 @@ ifs_firstchar (lenp)
    expansion [of $*] appears within a double quoted string, it expands
    to a single field with the value of each parameter separated by the
    first character of the IFS variable, or by a <space> if IFS is unset." */
+/* Posix interpretation 888 changes this when IFS is null by specifying
+   that when unquoted, this expands to separate arguments */
 char *
-string_list_dollar_star (list)
+string_list_dollar_star (list, quoted, flags)
      WORD_LIST *list;
+     int quoted, flags;
 {
   char *ret;
 #if defined (HANDLE_MULTIBYTE)
@@ -2582,7 +2983,8 @@ string_list_dollar_at (list, quoted, flags)
 #  if !defined (__GNUC__)
   sep = (char *)xmalloc (MB_CUR_MAX + 1);
 #  endif /* !__GNUC__ */
-  /* XXX - bash-4.4/bash-5.0 testing PF_ASSIGNRHS */
+  /* XXX - testing PF_ASSIGNRHS to make sure positional parameters are
+     separated with a space even when word splitting will not occur. */
   if (flags & PF_ASSIGNRHS)
     {
       sep[0] = ' ';
@@ -2606,11 +3008,12 @@ string_list_dollar_at (list, quoted, flags)
       sep[0] = ' ';
       sep[1] = '\0';
     }
-#else
-  /* XXX - bash-4.4/bash-5.0 test PF_ASSIGNRHS */
+#else  /* !HANDLE_MULTIBYTE */
+  /* XXX - PF_ASSIGNRHS means no word splitting, so we want positional
+     parameters separated by a space. */
   sep[0] = ((flags & PF_ASSIGNRHS) || ifs == 0 || *ifs == 0) ? ' ' : *ifs;
   sep[1] = '\0';
-#endif
+#endif /* !HANDLE_MULTIBYTE */
 
   /* XXX -- why call quote_list if ifs == 0?  we can get away without doing
      it now that quote_escapes quotes spaces */
@@ -2629,11 +3032,13 @@ string_list_dollar_at (list, quoted, flags)
    the various subtleties of using the first character of $IFS as the
    separator.  Calls string_list_dollar_at, string_list_dollar_star, and
    string_list as appropriate. */
+/* This needs to fully understand the additional contexts where word
+   splitting does not occur (W_ASSIGNRHS, etc.) */
 char *
-string_list_pos_params (pchar, list, quoted)
+string_list_pos_params (pchar, list, quoted, pflags)
      int pchar;
      WORD_LIST *list;
-     int quoted;
+     int quoted, pflags;
 {
   char *ret;
   WORD_LIST *tlist;
@@ -2642,7 +3047,7 @@ string_list_pos_params (pchar, list, quoted)
     {
       tlist = quote_list (list);
       word_list_remove_quoted_nulls (tlist);
-      ret = string_list_dollar_star (tlist);
+      ret = string_list_dollar_star (tlist, 0, 0);
     }
   else if (pchar == '*' && (quoted & Q_HERE_DOCUMENT))
     {
@@ -2650,12 +3055,16 @@ string_list_pos_params (pchar, list, quoted)
       word_list_remove_quoted_nulls (tlist);
       ret = string_list (tlist);
     }
+  else if (pchar == '*' && quoted == 0 && ifs_is_null) /* XXX */
+    ret = expand_no_split_dollar_star ? string_list_dollar_star (list, quoted, 0) : string_list_dollar_at (list, quoted, 0);   /* Posix interp 888 */
+  else if (pchar == '*' && quoted == 0 && (pflags & PF_ASSIGNRHS))     /* XXX */
+    ret = expand_no_split_dollar_star ? string_list_dollar_star (list, quoted, 0) : string_list_dollar_at (list, quoted, 0);   /* Posix interp 888 */
   else if (pchar == '*')
     {
       /* Even when unquoted, string_list_dollar_star does the right thing
         making sure that the first character of $IFS is used as the
         separator. */
-      ret = string_list_dollar_star (list);
+      ret = string_list_dollar_star (list, quoted, 0);
     }
   else if (pchar == '@' && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
     /* We use string_list_dollar_at, but only if the string is quoted, since
@@ -2666,8 +3075,12 @@ string_list_pos_params (pchar, list, quoted)
        the elements of $@ are separated by the first character of $IFS for
        later splitting. */
     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))
+    ret = string_list_dollar_at (list, quoted, pflags);        /* Posix interp 888 */
   else if (pchar == '@')
-    ret = string_list_dollar_star (list);
+    ret = string_list_dollar_star (list, quoted, 0);
   else
     ret = string_list ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) ? quote_list (list) : list);
 
@@ -2706,6 +3119,13 @@ string_list_pos_params (pchar, list, quoted)
                                              : (c) == (separators)[0]) \
                           : 0)
 
+/* member of the space character class in the current locale */
+#define ifs_whitespace(c)      ISSPACE(c)
+
+/* "adjacent IFS white space" */
+#define ifs_whitesep(c)        ((sh_style_split || separators == 0) ? spctabnl (c) \
+                                                            : ifs_whitespace (c))
+
 WORD_LIST *
 list_string (string, separators, quoted)
      register char *string, *separators;
@@ -2714,7 +3134,7 @@ list_string (string, separators, quoted)
   WORD_LIST *result;
   WORD_DESC *t;
   char *current_word, *s;
-  int sindex, sh_style_split, whitesep, xflags;
+  int sindex, sh_style_split, whitesep, xflags, free_word;
   size_t slen;
 
   if (!string || !*string)
@@ -2733,10 +3153,19 @@ list_string (string, separators, quoted)
   slen = 0;
   /* Remove sequences of whitespace at the beginning of STRING, as
      long as those characters appear in IFS.  Do not do this if
-     STRING is quoted or if there are no separator characters. */
+     STRING is quoted or if there are no separator characters. We use the
+     Posix definition of whitespace as a member of the space character
+     class in the current locale. */
+#if 0
   if (!quoted || !separators || !*separators)
+#else
+  /* issep() requires that separators be non-null, and always returns 0 if
+     separator is the empty string, so don't bother if we get an empty string
+     for separators. We already returned NULL above if STRING is empty. */
+  if (!quoted && separators && *separators)
+#endif
     {
-      for (s = string; *s && spctabnl (*s) && issep (*s); s++);
+      for (s = string; *s && issep (*s) && ifs_whitespace (*s); s++);
 
       if (!*s)
        return ((WORD_LIST *)NULL);
@@ -2747,17 +3176,19 @@ list_string (string, separators, quoted)
   /* OK, now STRING points to a word that does not begin with white space.
      The splitting algorithm is:
        extract a word, stopping at a separator
-       skip sequences of spc, tab, or nl as long as they are separators
+       skip sequences of whitespace characters as long as they are separators
      This obeys the field splitting rules in Posix.2. */
-  slen = (MB_CUR_MAX > 1) ? STRLEN (string) : 1;
+  slen = STRLEN (string);
   for (result = (WORD_LIST *)NULL, sindex = 0; string[sindex]; )
     {
-      /* Don't need string length in ADVANCE_CHAR or string_extract_verbatim
-        unless multibyte chars are possible. */
+      /* Don't need string length in ADVANCE_CHAR unless multibyte chars are
+        possible, but need it in string_extract_verbatim for bounds checking */
       current_word = string_extract_verbatim (string, slen, &sindex, separators, xflags);
       if (current_word == 0)
        break;
 
+      free_word = 1;   /* If non-zero, we free current_word */
+
       /* If we have a quoted empty string, add a quoted null argument.  We
         want to preserve the quoted null character iff this is a quoted
         empty string; otherwise the quoted null characters are removed
@@ -2774,15 +3205,27 @@ list_string (string, separators, quoted)
          /* If we have something, then add it regardless.  However,
             perform quoted null character removal on the current word. */
          remove_quoted_nulls (current_word);
-         result = add_string_to_list (current_word, result);
+
+         /* We don't want to set the word flags based on the string contents
+            here -- that's mostly for the parser -- so we just allocate a
+            WORD_DESC *, assign current_word (noting that we don't want to
+            free it), and skip all of make_word. */
+         t = alloc_word_desc ();
+         t->word = current_word;
+         result = make_word_list (t, result);
+         free_word = 0;
          result->word->flags &= ~W_HASQUOTEDNULL;      /* just to be sure */
          if (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT))
            result->word->flags |= W_QUOTED;
+         /* If removing quoted null characters leaves an empty word, note
+            that we saw this for the caller to act on. */
+         if (current_word == 0 || current_word[0] == '\0')
+           result->word->flags |= W_SAWQUOTEDNULL;
        }
 
       /* If we're not doing sequences of separators in the traditional
         Bourne shell style, then add a quoted null argument. */
-      else if (!sh_style_split && !spctabnl (string[sindex]))
+      else if (!sh_style_split && !ifs_whitespace (string[sindex]))
        {
          t = alloc_word_desc ();
          t->word = make_quoted_char ('\0');
@@ -2790,10 +3233,11 @@ list_string (string, separators, quoted)
          result = make_word_list (t, result);
        }
 
-      free (current_word);
+      if (free_word)
+       free (current_word);
 
       /* Note whether or not the separator is IFS whitespace, used later. */
-      whitesep = string[sindex] && spctabnl (string[sindex]);
+      whitesep = string[sindex] && ifs_whitesep (string[sindex]);
 
       /* Move past the current separator character. */
       if (string[sindex])
@@ -2802,21 +3246,21 @@ list_string (string, separators, quoted)
          ADVANCE_CHAR (string, slen, sindex);
        }
 
-      /* Now skip sequences of space, tab, or newline characters if they are
+      /* Now skip sequences of whitespace characters if they are
         in the list of separators. */
-      while (string[sindex] && spctabnl (string[sindex]) && issep (string[sindex]))
+      while (string[sindex] && ifs_whitesep (string[sindex]) && issep (string[sindex]))
        sindex++;
 
       /* If the first separator was IFS whitespace and the current character
         is a non-whitespace IFS character, it should be part of the current
         field delimiter, not a separate delimiter that would result in an
         empty field.  Look at POSIX.2, 3.6.5, (3)(b). */
-      if (string[sindex] && whitesep && issep (string[sindex]) && !spctabnl (string[sindex]))
+      if (string[sindex] && whitesep && issep (string[sindex]) && !ifs_whitesep (string[sindex]))
        {
          sindex++;
          /* An IFS character that is not IFS white space, along with any
             adjacent IFS white space, shall delimit a field. (SUSv3) */
-         while (string[sindex] && spctabnl (string[sindex]) && isifs (string[sindex]))
+         while (string[sindex] && ifs_whitesep (string[sindex]) && isifs (string[sindex]))
            sindex++;
        }
     }
@@ -2832,6 +3276,7 @@ list_string (string, separators, quoted)
    XXX - this function is very similar to list_string; they should be
         combined - XXX */
 
+/* character is in $IFS */
 #define islocalsep(c)  (local_cmap[(unsigned char)(c)] != 0)
 
 char *
@@ -2866,17 +3311,17 @@ get_word_from_string (stringp, separators, endptr)
      long as those characters appear in SEPARATORS.  This happens if
      SEPARATORS == $' \t\n' or if IFS is unset. */
   if (sh_style_split || separators == 0)
-    {
-      for (; *s && spctabnl (*s) && islocalsep (*s); s++);
+    for (; *s && spctabnl (*s) && islocalsep (*s); s++);
+  else
+    for (; *s && ifs_whitespace (*s) && islocalsep (*s); s++);
 
-      /* If the string is nothing but whitespace, update it and return. */
-      if (!*s)
-       {
-         *stringp = s;
-         if (endptr)
-           *endptr = s;
-         return ((char *)NULL);
-       }
+  /* If the string is nothing but whitespace, update it and return. */
+  if (!*s)
+    {
+      *stringp = s;
+      if (endptr)
+       *endptr = s;
+      return ((char *)NULL);
     }
 
   /* OK, S points to a word that does not begin with white space.
@@ -2896,7 +3341,7 @@ get_word_from_string (stringp, separators, endptr)
     *endptr = s + sindex;
 
   /* Note whether or not the separator is IFS whitespace, used later. */
-  whitesep = s[sindex] && spctabnl (s[sindex]);
+  whitesep = s[sindex] && ifs_whitesep (s[sindex]);
 
   /* Move past the current separator character. */
   if (s[sindex])
@@ -2914,12 +3359,12 @@ get_word_from_string (stringp, separators, endptr)
      a non-whitespace IFS character, it should be part of the current field
      delimiter, not a separate delimiter that would result in an empty field.
      Look at POSIX.2, 3.6.5, (3)(b). */
-  if (s[sindex] && whitesep && islocalsep (s[sindex]) && !spctabnl (s[sindex]))
+  if (s[sindex] && whitesep && islocalsep (s[sindex]) && !ifs_whitesep (s[sindex]))
     {
       sindex++;
       /* An IFS character that is not IFS white space, along with any adjacent
         IFS white space, shall delimit a field. */
-      while (s[sindex] && spctabnl (s[sindex]) && islocalsep(s[sindex]))
+      while (s[sindex] && ifs_whitesep (s[sindex]) && islocalsep(s[sindex]))
        sindex++;
     }
 
@@ -3016,49 +3461,59 @@ do_compound_assignment (name, value, flags)
      int flags;
 {
   SHELL_VAR *v;
-  int mklocal, mkassoc, mkglobal;
+  int mklocal, mkassoc, mkglobal, chklocal;
   WORD_LIST *list;
+  char *newname;       /* used for local nameref references */
 
   mklocal = flags & ASS_MKLOCAL;
   mkassoc = flags & ASS_MKASSOC;
   mkglobal = flags & ASS_MKGLOBAL;
+  chklocal = flags & ASS_CHKLOCAL;
 
   if (mklocal && variable_context)
     {
-      v = find_variable (name);
+      v = find_variable (name);                /* follows namerefs */
+      newname = (v == 0) ? nameref_transform_name (name, flags) : v->name;
       if (v && ((readonly_p (v) && (flags & ASS_FORCE) == 0) || noassign_p (v)))
        {
-         if (v && readonly_p (v))
+         if (readonly_p (v))
            err_readonly (name);
          return (v);   /* XXX */
        }
       list = expand_compound_array_assignment (v, value, flags);
       if (mkassoc)
-       v = make_local_assoc_variable (name);
+       v = make_local_assoc_variable (newname, 0);
       else if (v == 0 || (array_p (v) == 0 && assoc_p (v) == 0) || v->context != variable_context)
-        v = make_local_array_variable (name, 0);
+        v = make_local_array_variable (newname, 0);
       if (v)
        assign_compound_array_list (v, list, flags);
       if (list)
        dispose_words (list);
     }
-  /* In a function but forcing assignment in global context */
+  /* In a function but forcing assignment in global context. CHKLOCAL means to
+     check for an existing local variable first. */
   else if (mkglobal && variable_context)
     {
-      v = find_global_variable (name);
+      v = chklocal ? find_variable (name) : 0;
+      if (v && (local_p (v) == 0 || v->context != variable_context))
+       v = 0;
+      if (v == 0)
+        v = find_global_variable (name);
       if (v && ((readonly_p (v) && (flags & ASS_FORCE) == 0) || noassign_p (v)))
        {
-         if (v && readonly_p (v))
+         if (readonly_p (v))
            err_readonly (name);
          return (v);   /* XXX */
        }
+      /* sanity check */
+      newname = (v == 0) ? nameref_transform_name (name, flags) : name;
       list = expand_compound_array_assignment (v, value, flags);
       if (v == 0 && mkassoc)
-       v = make_new_assoc_variable (name);
+       v = make_new_assoc_variable (newname);
       else if (v && mkassoc && assoc_p (v) == 0)
        v = convert_var_to_assoc (v);
       else if (v == 0)
-       v = make_new_array_variable (name);
+       v = make_new_array_variable (newname);
       else if (v && mkassoc == 0 && array_p (v) == 0)
        v = convert_var_to_array (v);
       if (v)
@@ -3071,7 +3526,7 @@ do_compound_assignment (name, value, flags)
       v = assign_array_from_string (name, value, flags);
       if (v && ((readonly_p (v) && (flags & ASS_FORCE) == 0) || noassign_p (v)))
        {
-         if (v && readonly_p (v))
+         if (readonly_p (v))
            err_readonly (name);
          return (v);   /* XXX */
        }
@@ -3155,19 +3610,22 @@ do_assignment_internal (word, expand)
     aflags |= ASS_APPEND;
 
 #if defined (ARRAY_VARS)
-  if (t = mbschr (name, '['))  /*]*/
+  if (t = mbschr (name, LBRACK))
     {
       if (assign_list)
        {
          report_error (_("%s: cannot assign list to array member"), name);
          ASSIGN_RETURN (0);
        }
-      entry = assign_array_element (name, value, aflags);
+      aflags |= ASS_ALLOWALLSUB;       /* allow a[@]=value for existing associative arrays */
+      entry = assign_array_element (name, value, aflags, (array_eltstate_t *)0);
       if (entry == 0)
        ASSIGN_RETURN (0);
     }
   else if (assign_list)
     {
+      if ((word->flags & W_ASSIGNARG) && (word->flags & W_CHKLOCAL))
+       aflags |= ASS_CHKLOCAL;
       if ((word->flags & W_ASSIGNARG) && (word->flags & W_ASSNGLOBAL) == 0)
        aflags |= ASS_MKLOCAL;
       if ((word->flags & W_ASSIGNARG) && (word->flags & W_ASSNGLOBAL))
@@ -3180,14 +3638,17 @@ do_assignment_internal (word, expand)
 #endif /* ARRAY_VARS */
   entry = bind_variable (name, value, aflags);
 
-  stupidly_hack_special_variables (name);
+  if (entry)
+    stupidly_hack_special_variables (entry->name);     /* might be a nameref */
+  else
+    stupidly_hack_special_variables (name);
 
   /* Return 1 if the assignment seems to have been performed correctly. */
   if (entry == 0 || readonly_p (entry))
     retval = 0;                /* assignment failure */
   else if (noassign_p (entry))
     {
-      last_command_exit_value = EXECUTION_FAILURE;
+      set_exit_status (EXECUTION_FAILURE);
       retval = 1;      /* error status, but not assignment failure */
     }
   else
@@ -3259,19 +3720,6 @@ list_rest_of_args ()
   return (REVERSE_LIST (list, WORD_LIST *));
 }
 
-int
-number_of_args ()
-{
-  register WORD_LIST *list;
-  int n;
-
-  for (n = 0; n < 9 && dollar_vars[n+1]; n++)
-    ;
-  for (list = rest_of_args; list; list = list->next)
-    n++;
-  return n;
-}
-
 /* Return the value of a positional parameter.  This handles values > 10. */
 char *
 get_dollar_var_value (ind)
@@ -3303,7 +3751,7 @@ string_rest_of_args (dollar_star)
   char *string;
 
   list = list_rest_of_args ();
-  string = dollar_star ? string_list_dollar_star (list) : string_list (list);
+  string = dollar_star ? string_list_dollar_star (list, 0, 0) : string_list (list);
   dispose_words (list);
   return (string);
 }
@@ -3314,9 +3762,9 @@ string_rest_of_args (dollar_star)
    Q_HERE_DOCUMENT or Q_DOUBLE_QUOTES, this returns a quoted list, otherwise
    no quoting chars are added. */
 static char *
-pos_params (string, start, end, quoted)
+pos_params (string, start, end, quoted, pflags)
      char *string;
-     int start, end, quoted;
+     int start, end, quoted, pflags;
 {
   WORD_LIST *save, *params, *h, *t;
   char *ret;
@@ -3348,10 +3796,9 @@ pos_params (string, start, end, quoted)
       t = params;
       params = params->next;
     }
-
   t->next = (WORD_LIST *)NULL;
 
-  ret = string_list_pos_params (string[0], h, quoted);
+  ret = string_list_pos_params (string[0], h, quoted, pflags);
 
   if (t != params)
     t->next = params;
@@ -3372,6 +3819,10 @@ pos_params (string, start, end, quoted)
 #define EXP_CHAR(s) (s == '$' || s == '`' || s == CTLESC || s == '~')
 #endif
 
+/* We don't perform process substitution in arithmetic expressions, so don't
+   bother checking for it. */
+#define ARITH_EXP_CHAR(s) (s == '$' || s == '`' || s == CTLESC || s == '~')
+
 /* If there are any characters in STRING that require full expansion,
    then call FUNC to expand STRING; otherwise just perform quote
    removal if necessary.  This returns a new string. */
@@ -3466,6 +3917,104 @@ expand_assignment_string_to_string (string, quoted)
   return (expand_string_to_string_internal (string, quoted, expand_string_assignment));
 }
 
+/* Kind of like a combination of dequote_string and quote_string_for_globbing;
+   try to remove CTLESC quoting characters and convert CTLESC escaping a `&'
+   or a backslash into a backslash. The output of this function must eventually
+   be processed by strcreplace(). */
+static char *
+quote_string_for_repl (string, flags)
+     char *string;
+     int flags;
+{
+  size_t slen;
+  char *result, *t;
+  const char *s, *send;
+  DECLARE_MBSTATE;
+
+  slen = strlen (string);
+  send = string + slen;
+
+  result = (char *)xmalloc (slen * 2 + 1);
+
+  if (string[0] == CTLESC && string[1] == 0)
+    {
+      result[0] = CTLESC;
+      result[1] = '\0';
+      return (result);
+    }
+
+  /* This is awkward. We want to translate CTLESC-\ to \\ if we will
+     eventually send this string through strcreplace(), which we will do
+     only if shouldexp_replacement() determines that there is something
+     to replace. We can either make sure to escape backslashes here and
+     have shouldexp_replacement() signal that we should send the string to
+     strcreplace() if it sees an escaped backslash, or we can scan the
+     string before copying it and turn CTLESC-\ into \\ only if we encounter
+     a CTLESC-& or a &. This does the former and changes shouldexp_replacement().
+     If we double the backslashes  here, we'll get doubled backslashes in any
+     result that doesn't get passed to strcreplace(). */
+
+  for (s = string, t = result; *s; )
+    {
+      /* This function's result has to be processed by strcreplace() */
+      if (*s == CTLESC && (s[1] == '&' || s[1] == '\\'))
+        {
+          *t++ = '\\';
+          s++;
+          *t++ = *s++;
+          continue;
+        }
+      /* Dequote it */
+      if (*s == CTLESC)
+        {
+         s++;
+         if (*s == '\0')
+           break;
+        }
+      COPY_CHAR_P (t, s, send);
+    }
+
+  *t = '\0';
+  return (result);
+}
+       
+/* This does not perform word splitting on the WORD_LIST it returns and
+   it treats $* as if it were quoted. It dequotes the WORD_LIST, adds
+   backslash escapes before CTLESC-quoted backslash and `& if 
+   patsub_replacement is enabled. */
+static char *
+expand_string_for_patsub (string, quoted)
+     char *string;
+     int quoted;
+{
+  WORD_LIST *value;
+  char *ret, *t;
+
+  if (string == 0 || *string == '\0')
+    return (char *)NULL;
+
+  value = expand_string_for_pat (string, quoted, (int *)0, (int *)0);
+
+  if (value && value->word)
+    {
+      remove_quoted_nulls (value->word->word); /* XXX */
+      value->word->flags &= ~W_HASQUOTEDNULL;
+    }
+
+  if (value)
+    {
+      t = (value->next) ? string_list (value) : value->word->word;
+      ret = quote_string_for_repl (t, quoted);
+      if (t != value->word->word)
+       free (t);
+      dispose_words (value);
+    }
+  else
+    ret = (char *)NULL;
+
+  return (ret);
+}
+
 char *
 expand_arith_string (string, quoted)
      char *string;
@@ -3483,10 +4032,10 @@ expand_arith_string (string, quoted)
   i = saw_quote = 0;
   while (string[i])
     {
-      if (EXP_CHAR (string[i]))
+      if (ARITH_EXP_CHAR (string[i]))
        break;
       else if (string[i] == '\'' || string[i] == '\\' || string[i] == '"')
-       saw_quote = 1;
+       saw_quote = string[i];
       ADVANCE_CHAR (string, slen, i);
     }
 
@@ -3494,7 +4043,11 @@ expand_arith_string (string, quoted)
     {
       /* This is expanded version of expand_string_internal as it's called by
         expand_string_leave_quoted  */
-      td.flags = W_NOPROCSUB;  /* don't want process substitution */
+      td.flags = W_NOPROCSUB|W_NOTILDE;        /* don't want process substitution or tilde expansion */
+#if 0  /* TAG: bash-5.2 */
+      if (quoted & Q_ARRAYSUB)
+       td.flags |= W_NOCOMSUB;
+#endif
       td.word = savestring (string);
       list = call_expand_word_internal (&td, quoted, 0, (int *)NULL, (int *)NULL);
       /* This takes care of the calls from expand_string_leave_quoted and
@@ -3554,8 +4107,12 @@ remove_backslashes (string)
    to the != or == operator, and should be treated as a pattern.  In
    this case, we quote the string specially for the globbing code.  If
    SPECIAL is 2, this is an rhs argument for the =~ operator, and should
-   be quoted appropriately for regcomp/regexec.  The caller is responsible
-   for removing the backslashes if the unquoted word is needed later. */   
+   be quoted appropriately for regcomp/regexec.  If SPECIAL is 3, this is
+   an array subscript and should be quoted after expansion so it's only
+   expanded once (Q_ARITH). The caller is responsible
+   for removing the backslashes if the unquoted word is needed later. In
+   any case, since we don't perform word splitting, we need to do quoted
+   null character removal. */
 char *
 cond_expand_word (w, special)
      WORD_DESC *w;
@@ -3570,12 +4127,22 @@ cond_expand_word (w, special)
 
   expand_no_split_dollar_star = 1;
   w->flags |= W_NOSPLIT2;
-  l = call_expand_word_internal (w, 0, 0, (int *)0, (int *)0);
+  qflags = (special == 3) ? Q_ARITH : 0;
+  l = call_expand_word_internal (w, qflags, 0, (int *)0, (int *)0);
   expand_no_split_dollar_star = 0;
   if (l)
     {
       if (special == 0)                        /* LHS */
        {
+         if (l->word)
+           word_list_remove_quoted_nulls (l);
+         dequote_list (l);
+         r = string_list (l);
+       }
+      else if (special == 3)           /* arithmetic expression, Q_ARITH */
+       {
+         if (l->word)
+           word_list_remove_quoted_nulls (l);  /* for now */
          dequote_list (l);
          r = string_list (l);
        }
@@ -3584,7 +4151,7 @@ cond_expand_word (w, special)
          /* Need to figure out whether or not we should call dequote_escapes
             or a new dequote_ctlnul function here, and under what
             circumstances. */
-         qflags = QGLOB_CVTNULL;
+         qflags = QGLOB_CVTNULL|QGLOB_CTLESC;
          if (special == 2)
            qflags |= QGLOB_REGEXP;
          word_list_remove_quoted_nulls (l);
@@ -3601,6 +4168,150 @@ cond_expand_word (w, special)
 }
 #endif
 
+/* Expand $'...' and $"..." in a string for code paths that don't do it. The
+   FLAGS argument is 1 if this function should treat CTLESC as a quote
+   character (e.g., for here-documents) or not (e.g., for shell_expand_line). */
+char *
+expand_string_dollar_quote (string, flags)
+     char *string;
+     int flags;
+{
+  size_t slen, retind, retsize;
+  int sindex, c, translen, peekc, news;
+  char *ret, *trans, *send, *t;
+  DECLARE_MBSTATE;
+
+  slen = strlen (string);
+  send = string + slen;
+  sindex = 0;
+
+  retsize = slen + 1;
+  ret = xmalloc (retsize);
+  retind = 0;
+
+  while (c = string[sindex])
+    {
+      switch (c)
+       {
+       default:
+         RESIZE_MALLOCED_BUFFER (ret, retind, locale_mb_cur_max + 1, retsize, 64);
+         COPY_CHAR_I (ret, retind, string, send, sindex);
+         break;
+
+       case '\\':
+         RESIZE_MALLOCED_BUFFER (ret, retind, locale_mb_cur_max + 2, retsize, 64);
+         ret[retind++] = string[sindex++];
+
+         if (string[sindex])
+           COPY_CHAR_I (ret, retind, string, send, sindex);
+         break;
+
+       case '\'':
+       case '"':
+         if (c == '\'')
+           news = skip_single_quoted (string, slen, ++sindex, SX_COMPLETE);
+         else
+           news = skip_double_quoted (string, slen, ++sindex, SX_COMPLETE);
+         translen = news - sindex - 1;
+         RESIZE_MALLOCED_BUFFER (ret, retind, translen + 3, retsize, 64);
+         ret[retind++] = c;
+         if (translen > 0)
+           {
+             strncpy (ret + retind, string + sindex, translen);
+             retind += translen;
+           }
+         if (news > sindex && string[news - 1] == c)
+           ret[retind++] = c;
+         sindex = news;
+         break;
+
+       case CTLESC:
+         RESIZE_MALLOCED_BUFFER (ret, retind, locale_mb_cur_max + 2, retsize, 64);
+         if (flags)
+           ret[retind++] = string[sindex++];
+         if (string[sindex])
+           COPY_CHAR_I (ret, retind, string, send, sindex);
+         break;
+
+       case '$':
+         peekc = string[++sindex];
+#if defined (TRANSLATABLE_STRINGS)
+         if (peekc != '\'' && peekc != '"')
+#else
+         if (peekc != '\'')
+#endif
+           {
+             RESIZE_MALLOCED_BUFFER (ret, retind, 2, retsize, 16);
+             ret[retind++] = c;
+             break;
+           }
+         if (string[sindex + 1] == '\0')       /* don't bother */      
+           {
+             RESIZE_MALLOCED_BUFFER (ret, retind, 3, retsize, 16);
+             ret[retind++] = c;
+             ret[retind++] = peekc;
+             sindex++;
+             break;
+           }
+         if (peekc == '\'')
+           {
+             /* SX_COMPLETE is the  equivalent of ALLOWESC here */
+             /* We overload SX_COMPLETE below */
+             news = skip_single_quoted (string, slen, ++sindex, SX_COMPLETE);
+             /* Check for unclosed string and don't bother if so */
+             if (news > sindex && string[news] == '\0' && string[news-1] != peekc)
+               {
+                 RESIZE_MALLOCED_BUFFER (ret, retind, 3, retsize, 16);
+                 ret[retind++] = c;
+                 ret[retind++] = peekc;
+                 continue;
+               }
+             t = substring (string, sindex, news - 1);
+             trans = ansiexpand (t, 0, news-sindex-1, &translen);
+             free (t);
+             t = sh_single_quote (trans);
+             sindex = news;
+           }
+#if defined (TRANSLATABLE_STRINGS)
+         else
+           {
+             news = ++sindex;
+             t = string_extract_double_quoted (string, &news, SX_COMPLETE);
+             /* Check for unclosed string and don't bother if so */
+             if (news > sindex && string[news] == '\0' && string[news-1] != peekc)
+               {
+                 RESIZE_MALLOCED_BUFFER (ret, retind, 3, retsize, 16);
+                 ret[retind++] = c;
+                 ret[retind++] = peekc;
+                 free (t);
+                 continue;
+               }
+             trans = locale_expand (t, 0, news-sindex, 0, &translen);
+             free (t);
+             if (singlequote_translations &&
+                   ((news-sindex-1) != translen || STREQN (t, trans, translen) == 0))
+               t = sh_single_quote (trans);
+             else
+               t = sh_mkdoublequoted (trans, translen, 0);
+             sindex = news;
+           }
+#endif /* TRANSLATABLE_STRINGS */
+         free (trans);
+         trans = t;
+         translen = strlen (trans);
+
+         RESIZE_MALLOCED_BUFFER (ret, retind, translen + 1, retsize, 128);
+         strcpy (ret + retind, trans);
+         retind += translen;
+         FREE (trans);
+         break;
+       }
+    }
+
+  ret[retind] = 0;
+  return ret;
+}
+
 /* Call expand_word_internal to expand W and handle error returns.
    A convenience function for functions that don't want to handle
    any errors or free any memory before aborting. */
@@ -3675,7 +4386,7 @@ expand_string_unsplit (string, quoted)
     {
       if (value->word)
        {
-         remove_quoted_nulls (value->word->word);
+         remove_quoted_nulls (value->word->word);      /* XXX */
          value->word->flags &= ~W_HASQUOTEDNULL;
        }
       dequote_list (value);
@@ -3697,7 +4408,17 @@ expand_string_assignment (string, quoted)
 
   expand_no_split_dollar_star = 1;
 
+#if 0
+  /* Other shells (ksh93) do it this way, which affects how $@ is expanded
+     in constructs like bar=${@#0} (preserves the spaces resulting from the
+     expansion of $@ in a context where you don't do word splitting); Posix
+     interp 888 makes the expansion of $@ in contexts where word splitting
+     is not performed unspecified. */
+  td.flags = W_ASSIGNRHS|W_NOSPLIT2;           /* Posix interp 888 */
+#else
   td.flags = W_ASSIGNRHS;
+#endif
+  td.flags |= (W_NOGLOB|W_TILDEEXP);
   td.word = savestring (string);
   value = call_expand_word_internal (&td, quoted, 0, (int *)NULL, (int *)NULL);
   FREE (td.word);
@@ -3708,7 +4429,7 @@ expand_string_assignment (string, quoted)
     {
       if (value->word)
        {
-         remove_quoted_nulls (value->word->word);
+         remove_quoted_nulls (value->word->word);      /* XXX */
          value->word->flags &= ~W_HASQUOTEDNULL;
        }
       dequote_list (value);
@@ -3716,7 +4437,6 @@ expand_string_assignment (string, quoted)
   return (value);
 }
 
-
 /* Expand one of the PS? prompt strings. This is a sort of combination of
    expand_string_unsplit and expand_string_internal, but returns the
    passed string when an error occurs.  Might want to trap other calls
@@ -3750,7 +4470,7 @@ expand_prompt_string (string, quoted, wflags)
     {
       if (value->word)
        {
-         remove_quoted_nulls (value->word->word);
+         remove_quoted_nulls (value->word->word);      /* XXX */
          value->word->flags &= ~W_HASQUOTEDNULL;
        }
       dequote_list (value);
@@ -3788,21 +4508,72 @@ expand_string_leave_quoted (string, quoted)
 /* This does not perform word splitting or dequote the WORD_LIST
    it returns. */
 static WORD_LIST *
-expand_string_for_rhs (string, quoted, dollar_at_p, expanded_p)
+expand_string_for_rhs (string, quoted, op, pflags, dollar_at_p, expanded_p)
+     char *string;
+     int quoted, op, pflags;
+     int *dollar_at_p, *expanded_p;
+{
+  WORD_DESC td;
+  WORD_LIST *tresult;
+  int old_nosplit;
+
+  if (string == 0 || *string == '\0')
+    return (WORD_LIST *)NULL;
+
+  /* We want field splitting to be determined by what is going to be done with
+     the entire ${parameterOPword} expansion, so we don't want to split the RHS
+     we expand here.  However, the expansion of $* is determined by whether we
+     are going to eventually perform word splitting, so we want to set this
+     depending on whether or not are are going to be splitting: if the expansion
+     is quoted, if the OP is `=', or if IFS is set to the empty string, we
+     are not going to be splitting, so we set expand_no_split_dollar_star to
+     note this to callees.
+     We pass through PF_ASSIGNRHS as W_ASSIGNRHS if this is on the RHS of an
+     assignment statement. */
+  /* The updated treatment of $* is the result of Posix interp 888 */
+  /* This was further clarified on the austin-group list in March, 2017 and
+     in Posix bug 1129 */
+  old_nosplit = expand_no_split_dollar_star;
+  expand_no_split_dollar_star = (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) || op == '=' || ifs_is_null == 0; /* XXX - was 1 */
+  td.flags = W_EXPANDRHS;              /* expanding RHS of ${paramOPword} */
+  td.flags |= W_NOSPLIT2;              /* no splitting, remove "" and '' */
+  if (pflags & PF_ASSIGNRHS)           /* pass through */
+    td.flags |= W_ASSIGNRHS;
+  if (op == '=')
+#if 0
+    td.flags |= W_ASSIGNRHS;           /* expand b in ${a=b} like assignment */
+#else
+    td.flags |= W_ASSIGNRHS|W_NOASSNTILDE;             /* expand b in ${a=b} like assignment */
+#endif
+  td.word = savestring (string);
+  tresult = call_expand_word_internal (&td, quoted, 1, dollar_at_p, expanded_p);
+  expand_no_split_dollar_star = old_nosplit;
+  free (td.word);
+
+  return (tresult);
+}
+
+/* This does not perform word splitting or dequote the WORD_LIST
+   it returns and it treats $* as if it were quoted. */
+static WORD_LIST *
+expand_string_for_pat (string, quoted, dollar_at_p, expanded_p)
      char *string;
      int quoted, *dollar_at_p, *expanded_p;
 {
   WORD_DESC td;
   WORD_LIST *tresult;
+  int oexp;
 
   if (string == 0 || *string == '\0')
     return (WORD_LIST *)NULL;
 
+  oexp = expand_no_split_dollar_star;
   expand_no_split_dollar_star = 1;
   td.flags = W_NOSPLIT2;               /* no splitting, remove "" and '' */
-  td.word = string;
+  td.word = savestring (string);
   tresult = call_expand_word_internal (&td, quoted, 1, dollar_at_p, expanded_p);
-  expand_no_split_dollar_star = 0;
+  expand_no_split_dollar_star = oexp;
+  free (td.word);
 
   return (tresult);
 }
@@ -3826,6 +4597,63 @@ expand_string (string, quoted)
   return (result ? dequote_list (result) : result);
 }
 
+/*******************************************
+ *                                        *
+ *     Functions to expand WORD_DESCs     *
+ *                                        *
+ *******************************************/
+
+/* Expand WORD, performing word splitting on the result.  This does
+   parameter expansion, command substitution, arithmetic expansion,
+   word splitting, and quote removal. */
+
+WORD_LIST *
+expand_word (word, quoted)
+     WORD_DESC *word;
+     int quoted;
+{
+  WORD_LIST *result, *tresult;
+
+  tresult = call_expand_word_internal (word, quoted, 0, (int *)NULL, (int *)NULL);
+  result = word_list_split (tresult);
+  dispose_words (tresult);
+  return (result ? dequote_list (result) : result);
+}
+
+/* Expand WORD, but do not perform word splitting on the result.  This
+   does parameter expansion, command substitution, arithmetic expansion,
+   and quote removal. */
+WORD_LIST *
+expand_word_unsplit (word, quoted)
+     WORD_DESC *word;
+     int quoted;
+{
+  WORD_LIST *result;
+
+  result = expand_word_leave_quoted (word, quoted);
+  return (result ? dequote_list (result) : result);
+}
+
+/* Perform shell expansions on WORD, but do not perform word splitting or
+   quote removal on the result.  Virtually identical to expand_word_unsplit;
+   could be combined if implementations don't diverge. */
+WORD_LIST *
+expand_word_leave_quoted (word, quoted)
+     WORD_DESC *word;
+     int quoted;
+{
+  WORD_LIST *result;
+
+  expand_no_split_dollar_star = 1;
+  if (ifs_is_null)
+    word->flags |= W_NOSPLIT;
+  word->flags |= W_NOSPLIT2;
+  result = call_expand_word_internal (word, quoted, 0, (int *)NULL, (int *)NULL);
+  expand_no_split_dollar_star = 0;
+
+  return result;
+}
+
 /***************************************************
  *                                                *
  *     Functions to handle quoting chars          *
@@ -3846,24 +4674,32 @@ expand_string (string, quoted)
    code exists in dequote_escapes.  Even if we don't end up splitting on
    spaces, quoting spaces is not a problem.  This should never be called on
    a string that is quoted with single or double quotes or part of a here
-   document (effectively double-quoted). */
-char *
-quote_escapes (string)
-     char *string;
-{
-  register char *s, *t;
+   document (effectively double-quoted).
+   FLAGS says whether or not we are going to split the result. If we are not,
+   and there is a CTLESC or CTLNUL in IFS, we need to quote CTLESC and CTLNUL,
+   respectively, to prevent them from being removed as part of dequoting. */
+static char *
+quote_escapes_internal (string, flags)
+     const char *string;
+     int flags;
+{
+  const char *s, *send;
+  char *t, *result;
   size_t slen;
-  char *result, *send;
-  int quote_spaces, skip_ctlesc, skip_ctlnul;
+  int quote_spaces, skip_ctlesc, skip_ctlnul, nosplit;
   DECLARE_MBSTATE; 
 
   slen = strlen (string);
   send = string + slen;
 
   quote_spaces = (ifs_value && *ifs_value == 0);
+  nosplit = (flags & PF_NOSPLIT2);
 
   for (skip_ctlesc = skip_ctlnul = 0, s = ifs_value; s && *s; s++)
-    skip_ctlesc |= *s == CTLESC, skip_ctlnul |= *s == CTLNUL;
+    {
+      skip_ctlesc |= (nosplit == 0 && *s == CTLESC);
+      skip_ctlnul |= (nosplit == 0 && *s == CTLNUL);
+    }
 
   t = result = (char *)xmalloc ((slen * 2) + 1);
   s = string;
@@ -3879,6 +4715,20 @@ quote_escapes (string)
   return (result);
 }
 
+char *
+quote_escapes (string)
+     const char *string;
+{
+  return (quote_escapes_internal (string, 0));
+}
+
+char *
+quote_rhs (string)
+     const char *string;
+{
+  return (quote_escapes_internal (string, PF_NOSPLIT2));
+}
+
 static WORD_LIST *
 list_quote_escapes (list)
      WORD_LIST *list;
@@ -3908,16 +4758,16 @@ list_quote_escapes (list)
    Also used by parts of the pattern substitution code. */
 char *
 dequote_escapes (string)
-     char *string;
+     const char *string;
 {
-  register char *s, *t, *s1;
+  const char *s, *send;
+  char *t, *result;
   size_t slen;
-  char *result, *send;
   int quote_spaces;
   DECLARE_MBSTATE;
 
   if (string == 0)
-    return string;
+    return (char *)0;
 
   slen = strlen (string);
   send = string + slen;
@@ -3945,6 +4795,7 @@ dequote_escapes (string)
   return result;
 }
 
+#if defined (INCLUDE_UNUSED)
 static WORD_LIST *
 list_dequote_escapes (list)
      WORD_LIST *list;
@@ -3960,6 +4811,7 @@ list_dequote_escapes (list)
     }
   return list;
 }
+#endif
 
 /* Return a new string with the quoted representation of character C.
    This turns "" into QUOTED_NULL, so the W_HASQUOTEDNULL flag needs to be
@@ -4031,12 +4883,10 @@ dequote_string (string)
   char *result, *send;
   DECLARE_MBSTATE;
 
-#if defined (DEBUG)
   if (string[0] == CTLESC && string[1] == 0)
-    internal_inform ("dequote_string: string with bare CTLESC");
-#endif
+    internal_debug ("dequote_string: string with bare CTLESC");
 
-  slen = strlen (string);
+  slen = STRLEN (string);
 
   t = result = (char *)xmalloc (slen + 1);
 
@@ -4096,6 +4946,21 @@ quote_list (list)
   return list;
 }
 
+WORD_DESC *
+dequote_word (word)
+     WORD_DESC *word;
+{
+  register char *s;
+
+  s = dequote_string (word->word);
+  if (QUOTED_NULL (word->word))
+    word->flags &= ~W_HASQUOTEDNULL;
+  free (word->word);
+  word->word = s;
+
+  return word;
+}
+
 /* De-quote quoted characters in each word in LIST. */
 WORD_LIST *
 dequote_list (list)
@@ -4133,11 +4998,43 @@ remove_quoted_escapes (string)
   return (string);
 }
 
-/* Perform quoted null character removal on STRING.  We don't allow any
-   quoted null characters in the middle or at the ends of strings because
-   of how expand_word_internal works.  remove_quoted_nulls () turns
-   STRING into an empty string iff it only consists of a quoted null,
-   and removes all unquoted CTLNUL characters. */
+/* Remove quoted $IFS characters from STRING.  Quoted IFS characters are
+   added to protect them from word splitting, but we need to remove them
+   if no word splitting takes place.  This returns newly-allocated memory,
+   so callers can use it to replace savestring(). */
+char *
+remove_quoted_ifs (string)
+     char *string;
+{
+  register size_t slen;
+  register int i, j;
+  char *ret, *send;
+  DECLARE_MBSTATE;
+
+  slen = strlen (string);
+  send = string + slen;
+
+  i = j = 0;
+  ret = (char *)xmalloc (slen + 1);
+
+  while (i < slen)
+    {
+      if (string[i] == CTLESC)
+       {
+         i++;
+         if (string[i] == 0 || isifs (string[i]) == 0)
+           ret[j++] = CTLESC;
+         if (i == slen)
+           break;
+       }
+
+      COPY_CHAR_I (ret, j, string, send, i);
+    }
+  ret[j] = '\0';
+
+  return (ret);
+}
+
 char *
 remove_quoted_nulls (string)
      char *string;
@@ -4160,7 +5057,8 @@ remove_quoted_nulls (string)
             point -- what if a CTLNUL has already been removed from the
             string?  We don't want to drop the CTLESC or recopy characters
             that we've already copied down. */
-         i++; string[j++] = CTLESC;
+         i++;
+         string[j++] = CTLESC;
          if (i == slen)
            break;
        }
@@ -4171,7 +5069,7 @@ remove_quoted_nulls (string)
        }
 
       prev_i = i;
-      ADVANCE_CHAR (string, slen, i);
+      ADVANCE_CHAR (string, slen, i);          /* COPY_CHAR_I? */
       if (j < prev_i)
        {
          do string[j++] = string[prev_i++]; while (prev_i < i);
@@ -4206,7 +5104,7 @@ word_list_remove_quoted_nulls (list)
 /* **************************************************************** */
 
 #if defined (HANDLE_MULTIBYTE)
-#if 0 /* Currently unused */
+# ifdef INCLUDE_UNUSED
 static unsigned char *
 mb_getcharlens (string, len)
      char *string;
@@ -4229,7 +5127,7 @@ mb_getcharlens (string, len)
     }
   return ret;
 }
-#endif
+#  endif
 #endif
 
 /* Remove the portion of PARAM matched by PATTERN according to OP, where OP
@@ -4408,6 +5306,9 @@ remove_pattern (param, pattern, op)
       wchar_t *wparam, *wpattern;
       mbstate_t ps;
 
+      /* XXX - could optimize here by checking param and pattern for multibyte
+        chars with mbsmbchar and calling remove_upattern. */
+
       n = xdupmbstowcs (&wpattern, NULL, pattern);
       if (n == (size_t)-1)
        {
@@ -4467,7 +5368,6 @@ match_upattern (string, pat, mtype, sp, ep)
   size_t len;
   register char *p, *p1, *npat;
   char *end;
-  int n1;
 
   /* If the pattern doesn't match anywhere in the string, go ahead and
      short-circuit right away.  A minor optimization, saves a bunch of
@@ -4485,7 +5385,7 @@ match_upattern (string, pat, mtype, sp, ep)
 
       p = npat = (char *)xmalloc (len + 3);
       p1 = pat;
-      if (*p1 != '*' || (*p1 == '*' && p1[1] == LPAREN && extended_glob))
+      if ((mtype != MATCH_BEG) && (*p1 != '*' || (*p1 == '*' && p1[1] == LPAREN && extended_glob)))
        *p++ = '*';
       while (*p1)
        *p++ = *p1++;
@@ -4496,7 +5396,7 @@ match_upattern (string, pat, mtype, sp, ep)
       /* If the pattern ends with a `*' we leave it alone if it's preceded by
         an even number of backslashes, but if it's escaped by a backslash
         we need to add another `*'. */
-      if (p1[-1] == '*' && (unescaped_backslash = p1[-2] == '\\'))
+      if ((mtype != MATCH_END) && (p1[-1] == '*' && (unescaped_backslash = p1[-2] == '\\')))
        {
          pp = p1 - 3;
          while (pp >= pat && *pp-- == '\\')
@@ -4504,7 +5404,7 @@ match_upattern (string, pat, mtype, sp, ep)
          if (unescaped_backslash)
            *p++ = '*';
        }
-      else if (p1[-1] != '*')
+      else if (mtype != MATCH_END && p1[-1] != '*')
        *p++ = '*';
 #else 
       if (p1[-1] != '*' || p1[-2] == '\\')
@@ -4524,6 +5424,8 @@ match_upattern (string, pat, mtype, sp, ep)
   end = string + len;
 
   mlen = umatchlen (pat, len);
+  if (mlen > (int)len)
+    return (0);
 
   switch (mtype)
     {
@@ -4679,6 +5581,8 @@ match_wpattern (wstring, indices, wstrlen, wpat, mtype, sp, ep)
     return (0);
 
   mlen = wmatchlen (wpat, wstrlen);
+  if (mlen > (int)wstrlen)
+    return (0);
 
 /* itrace("wmatchlen (%ls) -> %d", wpat, mlen); */
   switch (mtype)
@@ -4768,7 +5672,6 @@ match_pattern (string, pat, mtype, sp, ep)
   size_t n;
   wchar_t *wstring, *wpat;
   char **indices;
-  size_t slen, plen, mslen, mplen;
 #endif
 
   if (string == 0 || pat == 0 || *pat == 0)
@@ -4846,12 +5749,14 @@ getpattern (value, quoted, expandpat)
     }
 #endif
 
-  /* expand_string_for_rhs () leaves WORD quoted and does not perform
+  /* expand_string_for_pat () leaves WORD quoted and does not perform
      word splitting. */
-  l = *value ? expand_string_for_rhs (value,
+  l = *value ? expand_string_for_pat (value,
                                      (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) ? Q_PATQUOTE : quoted,
                                      (int *)NULL, (int *)NULL)
             : (WORD_LIST *)0;
+  if (l)
+    word_list_remove_quoted_nulls (l);
   pat = string_list (l);
   dispose_words (l);
   if (pat)
@@ -4898,7 +5803,7 @@ list_remove_pattern (list, pattern, patspec, itype, quoted)
     }
 
   l = REVERSE_LIST (new, WORD_LIST *);
-  tword = string_list_pos_params (itype, l, quoted);
+  tword = string_list_pos_params (itype, l, quoted, 0);
   dispose_words (l);
 
   return (tword);
@@ -4923,11 +5828,11 @@ parameter_list_remove_pattern (itype, pattern, patspec, quoted)
 
 #if defined (ARRAY_VARS)
 static char *
-array_remove_pattern (var, pattern, patspec, varname, quoted)
+array_remove_pattern (var, pattern, patspec, starsub, quoted)
      SHELL_VAR *var;
      char *pattern;
      int patspec;
-     char *varname;    /* so we can figure out how it's indexed */
+     int starsub;      /* so we can figure out how it's indexed */
      int quoted;
 {
   ARRAY *a;
@@ -4937,14 +5842,9 @@ array_remove_pattern (var, pattern, patspec, varname, quoted)
   WORD_LIST *list;
   SHELL_VAR *v;
 
-  /* compute itype from varname here */
-  v = array_variable_part (varname, &ret, 0);
-
-  /* XXX */
-  if (v && invisible_p (v))
-    return ((char *)NULL);
+  v = var;             /* XXX - for now */
 
-  itype = ret[0];
+  itype = starsub ? '*' : '@';
 
   a = (v && array_p (v)) ? array_cell (v) : 0;
   h = (v && assoc_p (v)) ? assoc_cell (v) : 0;
@@ -4960,24 +5860,28 @@ array_remove_pattern (var, pattern, patspec, varname, quoted)
 #endif /* ARRAY_VARS */
 
 static char *
-parameter_brace_remove_pattern (varname, value, ind, patstr, rtype, quoted, flags)
+parameter_brace_remove_pattern (varname, value, estatep, patstr, rtype, quoted, flags)
      char *varname, *value;
-     int ind;
+     array_eltstate_t *estatep;
      char *patstr;
      int rtype, quoted, flags;
 {
   int vtype, patspec, starsub;
-  char *temp1, *val, *pattern;
+  char *temp1, *val, *pattern, *oname;
   SHELL_VAR *v;
 
   if (value == 0)
     return ((char *)NULL);
 
+  oname = this_command_name;
   this_command_name = varname;
 
-  vtype = get_var_and_type (varname, value, ind, quoted, flags, &v, &val);
+  vtype = get_var_and_type (varname, value, estatep, quoted, flags, &v, &val);
   if (vtype == -1)
-    return ((char *)NULL);
+    {
+      this_command_name = oname;
+      return ((char *)NULL);
+    }
 
   starsub = vtype & VT_STARSUB;
   vtype &= ~VT_STARSUB;
@@ -5011,7 +5915,7 @@ parameter_brace_remove_pattern (varname, value, ind, patstr, rtype, quoted, flag
       break;
 #if defined (ARRAY_VARS)
     case VT_ARRAYVAR:
-      temp1 = array_remove_pattern (v, pattern, patspec, varname, quoted);
+      temp1 = array_remove_pattern (v, pattern, patspec, starsub, quoted);
       if (temp1 && ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) == 0))
        {
          val = quote_escapes (temp1);
@@ -5022,7 +5926,11 @@ parameter_brace_remove_pattern (varname, value, ind, patstr, rtype, quoted, flag
 #endif
     case VT_POSPARMS:
       temp1 = parameter_list_remove_pattern (varname[0], pattern, patspec, quoted);
-      if (temp1 && ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) == 0))
+      if (temp1 && quoted == 0 && ifs_is_null)
+       {
+         /* Posix interp 888 */
+       }
+      else if (temp1 && ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) == 0))
        {
          val = quote_escapes (temp1);
          free (temp1);
@@ -5031,433 +5939,119 @@ parameter_brace_remove_pattern (varname, value, ind, patstr, rtype, quoted, flag
       break;
     }
 
+  this_command_name = oname;
+
   FREE (pattern);
   return temp1;
 }    
 
-static char *
-string_var_assignment (v, s)
-     SHELL_VAR *v;
-     char *s;
-{
-  char flags[MAX_ATTRIBUTES], *ret, *val;
-  int i;
+#if defined (PROCESS_SUBSTITUTION)
 
-  val = sh_quote_reusable (s, 0);
-  i = var_attribute_string (v, 0, flags);
-  ret = (char *)xmalloc (i + strlen (val) + strlen (v->name) + 16 + MAX_ATTRIBUTES);
-  if (i > 0)
-    sprintf (ret, "declare -%s %s=%s", flags, v->name, val);
-  else
-    sprintf (ret, "%s=%s", v->name, val);
-  free (val);
-  return ret;
-}
+static void reap_some_procsubs PARAMS((int));
 
-#if defined (ARRAY_VARS)
-static char *
-array_var_assignment (v, itype, quoted)
-     SHELL_VAR *v;
-     int itype, quoted;
+/*****************************************************************/
+/*                                                              */
+/*                 Hacking Process Substitution                 */
+/*                                                              */
+/*****************************************************************/
+
+#if !defined (HAVE_DEV_FD)
+/* Named pipes must be removed explicitly with `unlink'.  This keeps a list
+   of FIFOs the shell has open.  unlink_fifo_list will walk the list and
+   unlink the ones that don't have a living process on the other end.
+   unlink_all_fifos will walk the list and unconditionally unlink them, trying
+   to open and close the FIFO first to release any child processes sleeping on
+   the FIFO. add_fifo_list adds the name of an open FIFO to the list. 
+   NFIFO is a count of the number of FIFOs in the list. */
+#define FIFO_INCR 20
+
+/* PROC value of -1 means the process has been reaped and the FIFO needs to
+   be removed. PROC value of 0 means the slot is unused. */
+struct temp_fifo {
+  char *file;
+  pid_t proc;
+};
+
+static struct temp_fifo *fifo_list = (struct temp_fifo *)NULL;
+static int nfifo;
+static int fifo_list_size;
+
+void
+clear_fifo_list ()
 {
-  char *ret, *val, flags[MAX_ATTRIBUTES];
   int i;
 
-  if (v == 0)
-    return (char *)NULL;
-  val = array_p (v) ? array_to_assign (array_cell (v), 0)
-                   : assoc_to_assign (assoc_cell (v), 0);
-  if (val == 0)
-    {
-      val = (char *)xmalloc (3);
-      val[0] = '(';
-      val[1] = ')';
-      val[2] = 0;
-    }
-  else
+  for (i = 0; i < fifo_list_size; i++)
     {
-      ret = (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) ? quote_string (val) : quote_escapes (val);
-      free (val);
-      val = ret;
+      if (fifo_list[i].file)
+       free (fifo_list[i].file);
+      fifo_list[i].file = NULL;
+      fifo_list[i].proc = 0;
     }
-  i = var_attribute_string (v, 0, flags);
-  ret = (char *)xmalloc (i + strlen (val) + strlen (v->name) + 16);
-  sprintf (ret, "declare -%s %s=%s", flags, v->name, val);
-  free (val);
-  return ret;
+  nfifo = 0;
 }
-#endif
 
-static char *
-pos_params_assignment (list, itype, quoted)
-     WORD_LIST *list;
-     int itype;
-     int quoted;
+void *
+copy_fifo_list (sizep)
+     int *sizep;
 {
-  char *temp, *ret;
-
-  /* first, we transform the list to quote each word. */
-  temp = list_transform ('Q', (SHELL_VAR *)0, list, itype, quoted);
-  ret = (char *)xmalloc (strlen (temp) + 8);
-  strcpy (ret, "set -- ");
-  strcpy (ret + 7, temp);
-  free (temp);
-  return ret;
+  if (sizep)
+    *sizep = 0;
+  return (void *)NULL;
 }
 
-static char *
-string_transform (xc, v, s)
-     int xc;
-     SHELL_VAR *v;
-     char *s;
+static void
+add_fifo_list (pathname)
+     char *pathname;
 {
-  char *ret, flags[MAX_ATTRIBUTES];
-  int i;
+  int osize, i;
 
-  if (((xc == 'A' || xc == 'a') && v == 0) || (xc != 'a' && s == 0))
-    return (char *)NULL;
-
-  switch (xc)
+  if (nfifo >= fifo_list_size - 1)
     {
-      /* Transformations that interrogate the variable */
-      case 'a':
-       i = var_attribute_string (v, 0, flags);
-       ret = (i > 0) ? savestring (flags) : (char *)NULL;
-       break;
-      case 'A':
-       ret = string_var_assignment (v, s);
-       break;
-      /* Transformations that modify the variable's value */
-      case 'E':
-       ret = ansiexpand (s, 0, strlen (s), (int *)0);
-       break;
-      case 'P':
-       ret = decode_prompt_string (s);
-       break;
-      case 'Q':
-       ret = sh_quote_reusable (s, 0);
-       break;
-      default:
-       ret = (char *)NULL;
-       break;
+      osize = fifo_list_size;
+      fifo_list_size += FIFO_INCR;
+      fifo_list = (struct temp_fifo *)xrealloc (fifo_list,
+                               fifo_list_size * sizeof (struct temp_fifo));
+      for (i = osize; i < fifo_list_size; i++)
+       {
+         fifo_list[i].file = (char *)NULL;
+         fifo_list[i].proc = 0;        /* unused */
+       }
     }
-  return ret;
+
+  fifo_list[nfifo].file = savestring (pathname);
+  nfifo++;
 }
 
-static char *
-list_transform (xc, v, list, itype, quoted)
-     int xc;
-     SHELL_VAR *v;
-     WORD_LIST *list;
-     int itype, quoted;
+void
+unlink_fifo (i)
+     int i;
 {
-  WORD_LIST *new, *l;
-  WORD_DESC *w;
-  char *tword;
-
-  for (new = (WORD_LIST *)NULL, l = list; l; l = l->next)
+  if ((fifo_list[i].proc == (pid_t)-1) || (fifo_list[i].proc > 0 && (kill(fifo_list[i].proc, 0) == -1)))
     {
-      tword = string_transform (xc, v, l->word->word);
-      w = alloc_word_desc ();
-      w->word = tword ? tword : savestring ("");       /* XXX */
-      new = make_word_list (w, new);
+      unlink (fifo_list[i].file);
+      free (fifo_list[i].file);
+      fifo_list[i].file = (char *)NULL;
+      fifo_list[i].proc = 0;
     }
-
-  l = REVERSE_LIST (new, WORD_LIST *);
-  tword = string_list_pos_params (itype, l, quoted);
-  dispose_words (l);
-
-  return (tword);
 }
 
-static char *
-parameter_list_transform (xc, itype, quoted)
-     int xc;
-     int itype;
-     int quoted;
+void
+unlink_fifo_list ()
 {
-  char *ret;
-  WORD_LIST *list;
+  int saved, i, j;
 
-  list = list_rest_of_args ();
-  if (list == 0)
-    return ((char *)NULL);
-  if (xc == 'A')
-    return (pos_params_assignment (list, itype, quoted));
-  ret = list_transform (xc, (SHELL_VAR *)0, list, itype, quoted);
-  dispose_words (list);
-  return (ret);
-}
-
-#if defined (ARRAY_VARS)
-static char *
-array_transform (xc, var, varname, quoted)
-     int xc;
-     SHELL_VAR *var;
-     char *varname;    /* so we can figure out how it's indexed */
-     int quoted;
-{
-  ARRAY *a;
-  HASH_TABLE *h;
-  int itype;
-  char *ret;
-  WORD_LIST *list;
-  SHELL_VAR *v;
-
-  /* compute itype from varname here */
-  v = array_variable_part (varname, &ret, 0);
-
-  /* XXX */
-  if (v && invisible_p (v))
-    return ((char *)NULL);
-
-  itype = ret[0];
-
-  if (xc == 'A')
-    return (array_var_assignment (v, itype, quoted));
-
-  a = (v && array_p (v)) ? array_cell (v) : 0;
-  h = (v && assoc_p (v)) ? assoc_cell (v) : 0;
-  
-  list = a ? array_to_word_list (a) : (h ? assoc_to_word_list (h) : 0);
-  if (list == 0)
-   return ((char *)NULL);
-  ret = list_transform (xc, v, list, itype, quoted);
-  dispose_words (list);
-
-  return ret;
-}
-#endif /* ARRAY_VARS */
-
-static char *
-parameter_brace_transform (varname, value, ind, xform, rtype, quoted, flags)
-     char *varname, *value;
-     int ind;
-     char *xform;
-     int rtype, quoted, flags;
-{
-  int vtype, xc;
-  char *temp1, *val;
-  SHELL_VAR *v;
-
-  xc = xform[0];
-  if (value == 0 && xc != 'A' && xc != 'a')
-    return ((char *)NULL);
-
-  this_command_name = varname;
-
-  vtype = get_var_and_type (varname, value, ind, quoted, flags, &v, &val);
-  if (vtype == -1)
-    return ((char *)NULL);
-
-  /* check for valid values of xc */
-  switch (xc)
-    {
-    case 'a':          /* expand to a string with just attributes */
-    case 'A':          /* expand as an assignment statement with attributes */
-    case 'E':          /* expand like $'...' */
-    case 'P':          /* expand like prompt string */
-    case 'Q':          /* quote reusably */
-      break;
-    default:
-      return &expand_param_error;
-    }
-
-  temp1 = (char *)NULL;                /* shut up gcc */
-  switch (vtype & ~VT_STARSUB)
-    {
-    case VT_VARIABLE:
-    case VT_ARRAYMEMBER:
-      temp1 = string_transform (xc, v, val);
-      if (vtype == VT_VARIABLE)
-       FREE (val);
-      if (temp1)
-       {
-         val = (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES))
-                       ? quote_string (temp1)
-                       : quote_escapes (temp1);
-         free (temp1);
-         temp1 = val;
-       }
-      break;
-#if defined (ARRAY_VARS)
-    case VT_ARRAYVAR:
-      temp1 = array_transform (xc, v, varname, quoted);
-      if (temp1 && ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) == 0))
-       {
-         val = quote_escapes (temp1);
-         free (temp1);
-         temp1 = val;
-       }
-      break;
-#endif
-    case VT_POSPARMS:
-      temp1 = parameter_list_transform (xc, varname[0], quoted);
-      if (temp1 && ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) == 0))
-       {
-         val = quote_escapes (temp1);
-         free (temp1);
-         temp1 = val;
-       }
-      break;
-    }
-
-  return temp1;
-}
-
-/*******************************************
- *                                        *
- *     Functions to expand WORD_DESCs     *
- *                                        *
- *******************************************/
-
-/* Expand WORD, performing word splitting on the result.  This does
-   parameter expansion, command substitution, arithmetic expansion,
-   word splitting, and quote removal. */
-
-WORD_LIST *
-expand_word (word, quoted)
-     WORD_DESC *word;
-     int quoted;
-{
-  WORD_LIST *result, *tresult;
-
-  tresult = call_expand_word_internal (word, quoted, 0, (int *)NULL, (int *)NULL);
-  result = word_list_split (tresult);
-  dispose_words (tresult);
-  return (result ? dequote_list (result) : result);
-}
-
-/* Expand WORD, but do not perform word splitting on the result.  This
-   does parameter expansion, command substitution, arithmetic expansion,
-   and quote removal. */
-WORD_LIST *
-expand_word_unsplit (word, quoted)
-     WORD_DESC *word;
-     int quoted;
-{
-  WORD_LIST *result;
-
-  expand_no_split_dollar_star = 1;
-#if defined (HANDLE_MULTIBYTE)
-  if (ifs_firstc[0] == 0)
-#else
-  if (ifs_firstc == 0)
-#endif
-    word->flags |= W_NOSPLIT;
-  word->flags |= W_NOSPLIT2;
-  result = call_expand_word_internal (word, quoted, 0, (int *)NULL, (int *)NULL);
-  expand_no_split_dollar_star = 0;
-
-  return (result ? dequote_list (result) : result);
-}
-
-/* Perform shell expansions on WORD, but do not perform word splitting or
-   quote removal on the result.  Virtually identical to expand_word_unsplit;
-   could be combined if implementations don't diverge. */
-WORD_LIST *
-expand_word_leave_quoted (word, quoted)
-     WORD_DESC *word;
-     int quoted;
-{
-  WORD_LIST *result;
-
-  expand_no_split_dollar_star = 1;
-#if defined (HANDLE_MULTIBYTE)
-  if (ifs_firstc[0] == 0)
-#else
-  if (ifs_firstc == 0)
-#endif
-    word->flags |= W_NOSPLIT;
-  word->flags |= W_NOSPLIT2;
-  result = call_expand_word_internal (word, quoted, 0, (int *)NULL, (int *)NULL);
-  expand_no_split_dollar_star = 0;
-
-  return result;
-}
-
-#if defined (PROCESS_SUBSTITUTION)
-
-/*****************************************************************/
-/*                                                              */
-/*                 Hacking Process Substitution                 */
-/*                                                              */
-/*****************************************************************/
-
-#if !defined (HAVE_DEV_FD)
-/* Named pipes must be removed explicitly with `unlink'.  This keeps a list
-   of FIFOs the shell has open.  unlink_fifo_list will walk the list and
-   unlink all of them. add_fifo_list adds the name of an open FIFO to the
-   list.  NFIFO is a count of the number of FIFOs in the list. */
-#define FIFO_INCR 20
-
-struct temp_fifo {
-  char *file;
-  pid_t proc;
-};
-
-static struct temp_fifo *fifo_list = (struct temp_fifo *)NULL;
-static int nfifo;
-static int fifo_list_size;
-
-void
-clear_fifo_list ()
-{
-}
-
-char *
-copy_fifo_list (sizep)
-     int *sizep;
-{
-  if (sizep)
-    *sizep = 0;
-  return (char *)NULL;
-}
-
-static void
-add_fifo_list (pathname)
-     char *pathname;
-{
-  if (nfifo >= fifo_list_size - 1)
-    {
-      fifo_list_size += FIFO_INCR;
-      fifo_list = (struct temp_fifo *)xrealloc (fifo_list,
-                               fifo_list_size * sizeof (struct temp_fifo));
-    }
-
-  fifo_list[nfifo].file = savestring (pathname);
-  nfifo++;
-}
-
-void
-unlink_fifo (i)
-     int i;
-{
-  if ((fifo_list[i].proc == -1) || (kill(fifo_list[i].proc, 0) == -1))
-    {
-      unlink (fifo_list[i].file);
-      free (fifo_list[i].file);
-      fifo_list[i].file = (char *)NULL;
-      fifo_list[i].proc = -1;
-    }
-}
-
-void
-unlink_fifo_list ()
-{
-  int saved, i, j;
-
-  if (nfifo == 0)
-    return;
+  if (nfifo == 0)
+    return;
 
   for (i = saved = 0; i < nfifo; i++)
     {
-      if ((fifo_list[i].proc == -1) || (kill(fifo_list[i].proc, 0) == -1))
+      if ((fifo_list[i].proc == (pid_t)-1) || (fifo_list[i].proc > 0 && (kill(fifo_list[i].proc, 0) == -1)))
        {
          unlink (fifo_list[i].file);
          free (fifo_list[i].file);
          fifo_list[i].file = (char *)NULL;
-         fifo_list[i].proc = -1;
+         fifo_list[i].proc = 0;
        }
       else
        saved++;
@@ -5469,8 +6063,13 @@ unlink_fifo_list ()
       for (i = j = 0; i < nfifo; i++)
        if (fifo_list[i].file)
          {
-           fifo_list[j].file = fifo_list[i].file;
-           fifo_list[j].proc = fifo_list[i].proc;
+           if (i != j)
+             {
+               fifo_list[j].file = fifo_list[i].file;
+               fifo_list[j].proc = fifo_list[i].proc;
+               fifo_list[i].file = (char *)NULL;
+               fifo_list[i].proc = 0;
+             }
            j++;
          }
       nfifo = j;
@@ -5479,6 +6078,30 @@ unlink_fifo_list ()
     nfifo = 0;
 }
 
+void
+unlink_all_fifos ()
+{
+  int i, fd;
+
+  if (nfifo == 0)
+    return;
+
+  for (i = 0; i < nfifo; i++)
+    {
+      fifo_list[i].proc = (pid_t)-1;
+#if defined (O_NONBLOCK)
+      fd = open (fifo_list[i].file, O_RDWR|O_NONBLOCK);
+#else
+      fd = -1;
+#endif
+      unlink_fifo (i);
+      if (fd >= 0)
+       close (fd);
+    }
+
+  nfifo = 0;
+}
+
 /* Take LIST, which is a bitmap denoting active FIFOs in fifo_list
    from some point in the past, and close all open FIFOs in fifo_list
    that are not marked as active in LIST.  If LIST is NULL, close
@@ -5486,10 +6109,11 @@ unlink_fifo_list ()
    case it's larger than fifo_list_size (size of fifo_list). */
 void
 close_new_fifos (list, lsize)
-     char *list;
+     void *list;
      int lsize;
 {
   int i;
+  char *plist;
 
   if (list == 0)
     {
@@ -5497,8 +6121,8 @@ close_new_fifos (list, lsize)
       return;
     }
 
-  for (i = 0; i < lsize; i++)
-    if (list[i] == 0 && i < fifo_list_size && fifo_list[i].proc != -1)
+  for (plist = (char *)list, i = 0; i < lsize; i++)
+    if (plist[i] == 0 && i < fifo_list_size && fifo_list[i].proc != -1)
       unlink_fifo (i);
 
   for (i = lsize; i < fifo_list_size; i++)
@@ -5506,21 +6130,81 @@ close_new_fifos (list, lsize)
 }
 
 int
-fifos_pending ()
+find_procsub_child (pid)
+     pid_t pid;
 {
-  return nfifo;
+  int i;
+
+  for (i = 0; i < nfifo; i++)
+    if (fifo_list[i].proc == pid)
+      return i;
+  return -1;
 }
 
-int
-num_fifos ()
+void
+set_procsub_status (ind, pid, status)
+     int ind;
+     pid_t pid;
+     int status;
 {
-  return nfifo;
+  if (ind >= 0 && ind < nfifo)
+    fifo_list[ind].proc = (pid_t)-1;           /* sentinel */
 }
 
-static char *
-make_named_pipe ()
+/* If we've marked the process for this procsub as dead, close the
+   associated file descriptor and delete the FIFO. */
+static void
+reap_some_procsubs (max)
+     int max;
 {
-  char *tname;
+  int i;
+
+  for (i = 0; i < max; i++)
+    if (fifo_list[i].proc == (pid_t)-1)        /* reaped */
+      unlink_fifo (i);
+}
+
+void
+reap_procsubs ()
+{
+  reap_some_procsubs (nfifo);
+}
+
+#if 0
+/* UNUSED */
+void
+wait_procsubs ()
+{
+  int i, r;
+
+  for (i = 0; i < nfifo; i++)
+    {
+      if (fifo_list[i].proc != (pid_t)-1 && fifo_list[i].proc > 0)
+       {
+         r = wait_for (fifo_list[i].proc, 0);
+         save_proc_status (fifo_list[i].proc, r);
+         fifo_list[i].proc = (pid_t)-1;
+       }
+    }
+}
+#endif
+
+int
+fifos_pending ()
+{
+  return nfifo;
+}
+
+int
+num_fifos ()
+{
+  return nfifo;
+}
+
+static char *
+make_named_pipe ()
+{
+  char *tname;
 
   tname = sh_mktmpname ("sh-np", MT_USERANDOM|MT_USETMPDIR);
   if (mkfifo (tname, 0600) < 0)
@@ -5539,7 +6223,10 @@ make_named_pipe ()
    has open to children.  NFDS is a count of the number of bits currently
    set in DEV_FD_LIST.  TOTFDS is a count of the highest possible number
    of open files. */
-static char *dev_fd_list = (char *)NULL;
+/* dev_fd_list[I] value of -1 means the process has been reaped and file
+   descriptor I needs to be closed. Value of 0 means the slot is unused. */
+
+static pid_t *dev_fd_list = (pid_t *)NULL;
 static int nfds;
 static int totfds;     /* The highest possible number of open files. */
 
@@ -5568,23 +6255,23 @@ clear_fifo_list ()
   nfds = 0;
 }
 
-char *
+void *
 copy_fifo_list (sizep)
      int *sizep;
 {
-  char *ret;
+  void *ret;
 
   if (nfds == 0 || totfds == 0)
     {
       if (sizep)
        *sizep = 0;
-      return (char *)NULL;
+      return (void *)NULL;
     }
 
   if (sizep)
     *sizep = totfds;
-  ret = (char *)xmalloc (totfds);
-  return (memcpy (ret, dev_fd_list, totfds));
+  ret = xmalloc (totfds * sizeof (pid_t));
+  return (memcpy (ret, dev_fd_list, totfds * sizeof (pid_t)));
 }
 
 static void
@@ -5602,11 +6289,12 @@ add_fifo_list (fd)
       if (fd >= totfds)
        totfds = fd + 2;
 
-      dev_fd_list = (char *)xrealloc (dev_fd_list, totfds);
-      memset (dev_fd_list + ofds, '\0', totfds - ofds);
+      dev_fd_list = (pid_t *)xrealloc (dev_fd_list, totfds * sizeof (dev_fd_list[0]));
+      /* XXX - might need a loop for this */
+      memset (dev_fd_list + ofds, '\0', (totfds - ofds) * sizeof (pid_t));
     }
 
-  dev_fd_list[fd] = 1;
+  dev_fd_list[fd] = 1;         /* marker; updated later */
   nfds++;
 }
 
@@ -5642,12 +6330,18 @@ unlink_fifo_list ()
   if (nfds == 0)
     return;
 
-  for (i = 0; nfds && i < totfds; i++)
+  for (i = totfds-1; nfds && i >= 0; i--)
     unlink_fifo (i);
 
   nfds = 0;
 }
 
+void
+unlink_all_fifos ()
+{
+  unlink_fifo_list ();
+}
+
 /* Take LIST, which is a snapshot copy of dev_fd_list from some point in
    the past, and close all open fds in dev_fd_list that are not marked
    as open in LIST.  If LIST is NULL, close everything in dev_fd_list.
@@ -5655,10 +6349,11 @@ unlink_fifo_list ()
    totfds (size of dev_fd_list). */
 void
 close_new_fifos (list, lsize)
-     char *list;
+     void *list;
      int lsize;
 {
   int i;
+  pid_t *plist;
 
   if (list == 0)
     {
@@ -5666,14 +6361,78 @@ close_new_fifos (list, lsize)
       return;
     }
 
-  for (i = 0; i < lsize; i++)
-    if (list[i] == 0 && i < totfds && dev_fd_list[i])
+  for (plist = (pid_t *)list, i = 0; i < lsize; i++)
+    if (plist[i] == 0 && i < totfds && dev_fd_list[i])
       unlink_fifo (i);
 
   for (i = lsize; i < totfds; i++)
     unlink_fifo (i);  
 }
 
+int
+find_procsub_child (pid)
+     pid_t pid;
+{
+  int i;
+
+  if (nfds == 0)
+    return -1;
+
+  for (i = 0; i < totfds; i++)
+    if (dev_fd_list[i] == pid)
+      return i;
+
+  return -1;
+}
+
+void
+set_procsub_status (ind, pid, status)
+     int ind;
+     pid_t pid;
+     int status;
+{
+  if (ind >= 0 && ind < totfds)
+    dev_fd_list[ind] = (pid_t)-1;              /* sentinel */
+}
+
+/* If we've marked the process for this procsub as dead, close the
+   associated file descriptor. */
+static void
+reap_some_procsubs (max)
+     int max;
+{
+  int i;
+
+  for (i = 0; nfds > 0 && i < max; i++)
+    if (dev_fd_list[i] == (pid_t)-1)
+      unlink_fifo (i);
+}
+
+void
+reap_procsubs ()
+{
+  reap_some_procsubs (totfds);
+}
+
+#if 0
+/* UNUSED */
+void
+wait_procsubs ()
+{
+  int i, r;
+
+  for (i = 0; nfds > 0 && i < totfds; i++)
+    {
+      if (dev_fd_list[i] != (pid_t)-1 && dev_fd_list[i] > 0)
+       {
+         r = wait_for (dev_fd_list[i], 0);
+         save_proc_status (dev_fd_list[i], r);
+         dev_fd_list[i] = (pid_t)-1;
+       }
+    }
+}
+#endif
+
 #if defined (NOTDEF)
 print_dev_fd_list ()
 {
@@ -5727,7 +6486,7 @@ process_substitute (string, open_for_read_in_child)
      int open_for_read_in_child;
 {
   char *pathname;
-  int fd, result;
+  int fd, result, rc, function_value;
   pid_t old_pid, pid;
 #if defined (HAVE_DEV_FD)
   int parent_pipe_fd, child_pipe_fd;
@@ -5774,16 +6533,34 @@ process_substitute (string, open_for_read_in_child)
   save_pipeline (1);
 #endif /* JOB_CONTROL */
 
-  pid = make_child ((char *)NULL, 1);
+  pid = make_child ((char *)NULL, FORK_ASYNC);
   if (pid == 0)
     {
+#if 0
+      int old_interactive;
+
+      old_interactive = interactive;
+#endif
+      /* The currently-executing shell is not interactive */
+      interactive = 0;
+
       reset_terminating_signals ();    /* XXX */
       free_pushed_string_input ();
       /* Cancel traps, in trap.c. */
       restore_original_signals ();     /* XXX - what about special builtins? bash-4.2 */
+      subshell_environment &= ~SUBSHELL_IGNTRAP;
       QUIT;    /* catch any interrupts we got post-fork */
       setup_async_signals ();
-      subshell_environment |= SUBSHELL_COMSUB|SUBSHELL_PROCSUB;
+#if 0
+      if (open_for_read_in_child == 0 && old_interactive && (bash_input.type == st_stdin || bash_input.type == st_stream))
+       async_redirect_stdin ();
+#endif
+
+      subshell_environment |= SUBSHELL_COMSUB|SUBSHELL_PROCSUB|SUBSHELL_ASYNC;
+
+      /* We don't inherit the verbose option for command substitutions now, so
+        let's try it for process substitutions. */
+      change_flag ('v', FLAG_OFF);
 
       /* if we're expanding a redirection, we shouldn't have access to the
         temporary environment, but commands in the subshell should have
@@ -5809,18 +6586,25 @@ process_substitute (string, open_for_read_in_child)
       close (parent_pipe_fd);
       close (child_pipe_fd);
 #endif /* HAVE_DEV_FD */
+#if defined (JOB_CONTROL)
+      restore_pipeline (1);
+#endif
       return ((char *)NULL);
     }
 
   if (pid > 0)
     {
 #if defined (JOB_CONTROL)
-      if (last_procsub_child)
-       discard_last_procsub_child ();
       last_procsub_child = restore_pipeline (0);
+      /* We assume that last_procsub_child->next == last_procsub_child because
+        of how jobs.c:add_process() works. */
+      last_procsub_child->next = 0;
+      procsub_add (last_procsub_child);
 #endif
 
-#if !defined (HAVE_DEV_FD)
+#if defined (HAVE_DEV_FD)
+      dev_fd_list[parent_pipe_fd] = pid;
+#else
       fifo_list[nfifo-1].proc = pid;
 #endif
 
@@ -5840,7 +6624,27 @@ process_substitute (string, open_for_read_in_child)
   set_sigint_handler ();
 
 #if defined (JOB_CONTROL)
+  /* make sure we don't have any job control */
   set_job_control (0);
+
+  /* Clear out any existing list of process substitutions */
+  procsub_clear ();
+
+  /* The idea is that we want all the jobs we start from an async process
+     substitution to be in the same process group, but not the same pgrp
+     as our parent shell, since we don't want to affect our parent shell's
+     jobs if we get a SIGHUP and end up calling hangup_all_jobs, for example.
+     If pipeline_pgrp != shell_pgrp, we assume that there is a job control
+     shell somewhere in our parent process chain (since make_child initializes
+     pipeline_pgrp to shell_pgrp if job_control == 0). What we do in this
+     case is to set pipeline_pgrp to our PID, so all jobs started by this
+     process have that same pgrp and we are basically the process group leader.
+     This should not have negative effects on child processes surviving
+     after we exit, since we wait for the children we create, but that is
+     something to watch for. */
+
+  if (pipeline_pgrp != shell_pgrp)
+    pipeline_pgrp = getpid ();
 #endif /* JOB_CONTROL */
 
 #if !defined (HAVE_DEV_FD)
@@ -5906,18 +6710,46 @@ process_substitute (string, open_for_read_in_child)
      parent. */
   expanding_redir = 0;
 
-  subshell_level++;
-  result = parse_and_execute (string, "process substitution", (SEVAL_NONINT|SEVAL_NOHIST));
-  subshell_level--;
+  remove_quoted_escapes (string);
+
+  startup_state = 2;   /* see if we can avoid a fork */
+  parse_and_execute_level = 0;
+
+  /* Give process substitution a place to jump back to on failure,
+     so we don't go back up to main (). */
+  result = setjmp_nosigs (top_level);
+
+  /* If we're running a process substitution inside a shell function,
+     trap `return' so we don't return from the function in the subshell
+     and go off to never-never land. */
+  if (result == 0 && return_catch_flag)
+    function_value = setjmp_nosigs (return_catch);
+  else
+    function_value = 0;
+
+  if (result == ERREXIT)
+    rc = last_command_exit_value;
+  else if (result == EXITPROG || result == EXITBLTIN)
+    rc = last_command_exit_value;
+  else if (result)
+    rc = EXECUTION_FAILURE;
+  else if (function_value)
+    rc = return_catch_value;
+  else
+    {
+      subshell_level++;
+      rc = parse_and_execute (string, "process substitution", (SEVAL_NONINT|SEVAL_NOHIST));
+      /* leave subshell level intact for any exit trap */
+    }
 
 #if !defined (HAVE_DEV_FD)
   /* Make sure we close the named pipe in the child before we exit. */
   close (open_for_read_in_child ? 0 : 1);
 #endif /* !HAVE_DEV_FD */
 
-  last_command_exit_value = result;
-  result = run_exit_trap ();
-  exit (result);
+  last_command_exit_value = rc;
+  rc = run_exit_trap ();
+  exit (rc);
   /*NOTREACHED*/
 }
 #endif /* PROCESS_SUBSTITUTION */
@@ -5928,26 +6760,55 @@ process_substitute (string, open_for_read_in_child)
 /*                                */
 /***********************************/
 
+#define COMSUB_PIPEBUF 4096
+
+static char *
+optimize_cat_file (r, quoted, flags, flagp)
+     REDIRECT *r;
+     int quoted, flags, *flagp;
+{
+  char *ret;
+  int fd;
+
+  fd = open_redir_file (r, (char **)0);
+  if (fd < 0)
+    return &expand_param_error;
+
+  ret = read_comsub (fd, quoted, flags, flagp);
+  close (fd);
+
+  return ret;
+}
+
 static char *
-read_comsub (fd, quoted, rflag)
-     int fd, quoted;
+read_comsub (fd, quoted, flags, rflag)
+     int fd, quoted, flags;
      int *rflag;
 {
-  char *istring, buf[128], *bufp, *s;
-  int istring_index, istring_size, c, tflag, skip_ctlesc, skip_ctlnul;
+  char *istring, buf[COMSUB_PIPEBUF], *bufp;
+  int c, tflag, skip_ctlesc, skip_ctlnul;
+  int mb_cur_max;
+  size_t istring_index;
+  size_t istring_size;
   ssize_t bufn;
   int nullbyte;
+#if defined (HANDLE_MULTIBYTE)
+  mbstate_t ps;
+  wchar_t wc;
+  size_t mblen;
+  int i;
+#endif
 
   istring = (char *)NULL;
   istring_index = istring_size = bufn = tflag = 0;
 
-  for (skip_ctlesc = skip_ctlnul = 0, s = ifs_value; s && *s; s++)
-    skip_ctlesc |= *s == CTLESC, skip_ctlnul |= *s == CTLNUL;
+  skip_ctlesc = ifs_cmap[CTLESC];
+  skip_ctlnul = ifs_cmap[CTLNUL];
 
+  mb_cur_max = MB_CUR_MAX;
   nullbyte = 0;
 
-  /* Read the output of the command through the pipe.  This may need to be
-     changed to understand multibyte characters in the future. */
+  /* Read the output of the command through the pipe. */
   while (1)
     {
       if (fd < 0)
@@ -5974,33 +6835,43 @@ read_comsub (fd, quoted, rflag)
        }
 
       /* Add the character to ISTRING, possibly after resizing it. */
-      RESIZE_MALLOCED_BUFFER (istring, istring_index, 2, istring_size, DEFAULT_ARRAY_SIZE);
+      RESIZE_MALLOCED_BUFFER (istring, istring_index, mb_cur_max+1, istring_size, 512);
 
       /* This is essentially quote_string inline */
       if ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) /* || c == CTLESC || c == CTLNUL */)
        istring[istring_index++] = CTLESC;
+      else if ((flags & PF_ASSIGNRHS) && skip_ctlesc && c == CTLESC)
+       istring[istring_index++] = CTLESC;
       /* Escape CTLESC and CTLNUL in the output to protect those characters
         from the rest of the word expansions (word splitting and globbing.)
         This is essentially quote_escapes inline. */
       else if (skip_ctlesc == 0 && c == CTLESC)
-       {
-         tflag |= W_HASCTLESC;
-         istring[istring_index++] = CTLESC;
-       }
+       istring[istring_index++] = CTLESC;
       else if ((skip_ctlnul == 0 && c == CTLNUL) || (c == ' ' && (ifs_value && *ifs_value == 0)))
        istring[istring_index++] = CTLESC;
 
-      istring[istring_index++] = c;
-
-#if 0
-#if defined (__CYGWIN__)
-      if (c == '\n' && istring_index > 1 && istring[istring_index - 2] == '\r')
-       {
-         istring_index--;
-         istring[istring_index - 1] = '\n';
+#if defined (HANDLE_MULTIBYTE)
+      if ((locale_utf8locale && (c & 0x80)) ||
+         (locale_utf8locale == 0 && mb_cur_max > 1 && (unsigned char)c > 127))
+       {
+         /* read a multibyte character from buf */
+         /* punt on the hard case for now */
+         memset (&ps, '\0', sizeof (mbstate_t));
+         mblen = mbrtowc (&wc, bufp-1, bufn, &ps);
+         if (MB_INVALIDCH (mblen) || mblen == 0 || mblen == 1)
+           istring[istring_index++] = c;
+         else
+           {
+             istring[istring_index++] = c;
+             for (i = 0; i < mblen-1; i++)
+               istring[istring_index++] = *bufp++;
+             bufn -= mblen - 1;
+           }
+         continue;
        }
 #endif
-#endif
+
+      istring[istring_index++] = c;
     }
 
   if (istring)
@@ -6045,28 +6916,53 @@ read_comsub (fd, quoted, rflag)
 /* Perform command substitution on STRING.  This returns a WORD_DESC * with the
    contained string possibly quoted. */
 WORD_DESC *
-command_substitute (string, quoted)
+command_substitute (string, quoted, flags)
      char *string;
      int quoted;
+     int flags;
 {
   pid_t pid, old_pid, old_pipeline_pgrp, old_async_pid;
   char *istring, *s;
-  int result, fildes[2], function_value, pflags, rc, tflag;
+  int result, fildes[2], function_value, pflags, rc, tflag, fork_flags;
   WORD_DESC *ret;
+  sigset_t set, oset;
 
   istring = (char *)NULL;
 
   /* Don't fork () if there is no need to.  In the case of no command to
      run, just return NULL. */
-#if 1
   for (s = string; s && *s && (shellblank (*s) || *s == '\n'); s++)
     ;
   if (s == 0 || *s == 0)
     return ((WORD_DESC *)NULL);
-#else
-  if (!string || !*string || (string[0] == '\n' && !string[1]))
-    return ((WORD_DESC *)NULL);
-#endif
+
+  if (*s == '<' && (s[1] != '<' && s[1] != '>' && s[1] != '&'))
+    {
+      COMMAND *cmd;
+
+      cmd = parse_string_to_command (string, 0);       /* XXX - flags */
+      if (cmd && can_optimize_cat_file (cmd))
+       {
+         tflag = 0;
+         istring = optimize_cat_file (cmd->value.Simple->redirects, quoted, flags, &tflag);
+         if (istring == &expand_param_error)
+           {
+             last_command_exit_value = EXECUTION_FAILURE;
+             istring = 0;
+           }
+         else
+           last_command_exit_value = EXECUTION_SUCCESS;        /* compat */
+         last_command_subst_pid = dollar_dollar_pid;
+
+         dispose_command (cmd);          
+         ret = alloc_word_desc ();
+         ret->word = istring;
+         ret->flags = tflag;
+
+         return ret;
+       }
+      dispose_command (cmd);
+    }
 
   if (wordexp_only && read_but_dont_execute)
     {
@@ -6089,6 +6985,8 @@ command_substitute (string, quoted)
   /* Flags to pass to parse_and_execute() */
   pflags = (interactive && sourcelevel == 0) ? SEVAL_RESETLINE : 0;
 
+  old_pid = last_made_pid;
+
   /* Pipe the output of executing STRING into the current shell. */
   if (pipe (fildes) < 0)
     {
@@ -6096,17 +6994,19 @@ command_substitute (string, quoted)
       goto error_exit;
     }
 
-  old_pid = last_made_pid;
 #if defined (JOB_CONTROL)
   old_pipeline_pgrp = pipeline_pgrp;
-  /* Don't reset the pipeline pgrp if we're already a subshell in a pipeline. */
-  if ((subshell_environment & SUBSHELL_PIPE) == 0)
+  /* Don't reset the pipeline pgrp if we're already a subshell in a pipeline or
+     we've already forked to run a disk command (and are expanding redirections,
+     for example). */
+  if ((subshell_environment & (SUBSHELL_FORK|SUBSHELL_PIPE)) == 0)
     pipeline_pgrp = shell_pgrp;
   cleanup_the_pipeline ();
 #endif /* JOB_CONTROL */
 
   old_async_pid = last_asynchronous_pid;
-  pid = make_child ((char *)NULL, subshell_environment&SUBSHELL_ASYNC);
+  fork_flags = (subshell_environment&SUBSHELL_ASYNC) ? FORK_ASYNC : 0;
+  pid = make_child ((char *)NULL, fork_flags|FORK_NOTERM);
   last_asynchronous_pid = old_async_pid;
 
   if (pid == 0)
@@ -6122,6 +7022,7 @@ command_substitute (string, quoted)
        }       
       QUIT;    /* catch any interrupts we got post-fork */
       subshell_environment |= SUBSHELL_RESETTRAP;
+      subshell_environment &= ~SUBSHELL_IGNTRAP;
     }
 
 #if defined (JOB_CONTROL)
@@ -6152,6 +7053,13 @@ command_substitute (string, quoted)
       /* The currently executing shell is not interactive. */
       interactive = 0;
 
+#if defined (JOB_CONTROL)
+      /* Invariant: in child processes started to run command substitutions,
+        pipeline_pgrp == shell_pgrp. Other parts of the shell assume this. */
+      if (pipeline_pgrp > 0 && pipeline_pgrp != shell_pgrp)
+       shell_pgrp = pipeline_pgrp;
+#endif
+
       set_sigint_handler ();   /* XXX */
 
       free_pushed_string_input ();
@@ -6218,7 +7126,18 @@ command_substitute (string, quoted)
 
       remove_quoted_escapes (string);
 
+      /* We want to expand aliases on this pass if we are not in posix mode
+        for backwards compatibility. parse_and_execute() takes care of
+        setting expand_aliases back to the global value when executing the
+        parsed string. We only do this for $(...) command substitution,
+        since that is what parse_comsub handles; `` comsubs are processed
+        using parse.y:parse_matched_pair(). */
+      if (expand_aliases && (flags & PF_BACKQUOTE) == 0)
+        expand_aliases = posixly_correct == 0;
+
       startup_state = 2;       /* see if we can avoid a fork */
+      parse_and_execute_level = 0;
+
       /* Give command substitution a place to jump back to on failure,
         so we don't go back up to main (). */
       result = setjmp_nosigs (top_level);
@@ -6233,7 +7152,7 @@ command_substitute (string, quoted)
 
       if (result == ERREXIT)
        rc = last_command_exit_value;
-      else if (result == EXITPROG)
+      else if (result == EXITPROG || result == EXITBLTIN)
        rc = last_command_exit_value;
       else if (result)
        rc = EXECUTION_FAILURE;
@@ -6243,7 +7162,7 @@ command_substitute (string, quoted)
        {
          subshell_level++;
          rc = parse_and_execute (string, "command substitution", pflags|SEVAL_NOHIST);
-         subshell_level--;
+         /* leave subshell level intact for any exit trap */
        }
 
       last_command_exit_value = rc;
@@ -6255,19 +7174,31 @@ command_substitute (string, quoted)
     }
   else
     {
+      int dummyfd;
+
 #if defined (JOB_CONTROL) && defined (PGRP_PIPE)
       close_pgrp_pipe ();
 #endif /* JOB_CONTROL && PGRP_PIPE */
 
       close (fildes[1]);
 
+      begin_unwind_frame ("read-comsub");
+      dummyfd = fildes[0];
+      add_unwind_protect (close, dummyfd);
+
+      /* Block SIGINT while we're reading from the pipe. If the child
+        process gets a SIGINT, it will either handle it or die, and the
+        read will return. */
+      BLOCK_SIGNAL (SIGINT, set, oset);
       tflag = 0;
-      istring = read_comsub (fildes[0], quoted, &tflag);
+      istring = read_comsub (fildes[0], quoted, flags, &tflag);
 
       close (fildes[0]);
+      discard_unwind_frame ("read-comsub");
+      UNBLOCK_SIGNAL (oset);
 
       current_command_subst_pid = pid;
-      last_command_exit_value = wait_for (pid);
+      last_command_exit_value = wait_for (pid, JWAIT_NOTERM);
       last_command_subst_pid = pid;
       last_made_pid = old_pid;
 
@@ -6277,20 +7208,6 @@ command_substitute (string, quoted)
         SIGINT to ourselves.  This will break out of loops, for instance. */
       if (last_command_exit_value == (128 + SIGINT) && last_command_exit_signal == SIGINT)
        kill (getpid (), SIGINT);
-
-      /* wait_for gives the terminal back to shell_pgrp.  If some other
-        process group should have it, give it away to that group here.
-        pipeline_pgrp is non-zero only while we are constructing a
-        pipeline, so what we are concerned about is whether or not that
-        pipeline was started in the background.  A pipeline started in
-        the background should never get the tty back here.  We duplicate
-        the conditions that wait_for tests to make sure we only give
-        the terminal back to pipeline_pgrp under the conditions that wait_for
-        gave it to shell_pgrp.  If wait_for doesn't mess with the terminal
-        pgrp, we should not either. */
-      if (interactive && pipeline_pgrp != (pid_t)0 && running_in_background == 0 &&
-          (subshell_environment & (SUBSHELL_ASYNC|SUBSHELL_PIPE)) == 0)
-       give_terminal_to (pipeline_pgrp, 0);
 #endif /* JOB_CONTROL */
 
       ret = alloc_word_desc ();
@@ -6321,7 +7238,7 @@ array_length_reference (s)
   HASH_TABLE *h;
   SHELL_VAR *var;
 
-  var = array_variable_part (s, &t, &len);
+  var = array_variable_part (s, 0, &t, &len);
 
   /* If unbound variables should generate an error, report one and return
      failure. */
@@ -6329,7 +7246,7 @@ array_length_reference (s)
     {
       c = *--t;
       *t = '\0';
-      last_command_exit_value = EXECUTION_FAILURE;
+      set_exit_status (EXECUTION_FAILURE);
       err_unboundvar (s);
       *t = c;
       return (-1);
@@ -6344,7 +7261,7 @@ array_length_reference (s)
   array = array_p (var) ? array_cell (var) : (ARRAY *)NULL;
   h = assoc_p (var) ? assoc_cell (var) : (HASH_TABLE *)NULL;
 
-  if (ALL_ELEMENT_SUB (t[0]) && t[1] == ']')
+  if (ALL_ELEMENT_SUB (t[0]) && t[1] == RBRACK)
     {
       if (assoc_p (var))
        return (h ? assoc_num_elements (h) : 0);
@@ -6357,8 +7274,8 @@ array_length_reference (s)
   if (assoc_p (var))
     {
       t[len - 1] = '\0';
-      akey = expand_assignment_string_to_string (t, 0);        /* [ */
-      t[len - 1] = ']';
+      akey = expand_subscript_string (t, 0);   /* [ */
+      t[len - 1] = RBRACK;
       if (akey == 0 || *akey == 0)
        {
          err_badarraysub (t);
@@ -6370,7 +7287,7 @@ array_length_reference (s)
     }
   else
     {
-      ind = array_expand_index (var, t, len);
+      ind = array_expand_index (var, t, len, 0);
       /* negative subscripts to indexed arrays count back from end */
       if (var && array_p (var) && ind < 0)
        ind = array_max_index (array_cell (var)) + 1 + ind;
@@ -6410,9 +7327,9 @@ valid_brace_expansion_word (name, var_is_special)
 }
 
 static int
-chk_atstar (name, quoted, quoted_dollar_atp, contains_dollar_at)
+chk_atstar (name, quoted, pflags, quoted_dollar_atp, contains_dollar_at)
      char *name;
-     int quoted;
+     int quoted, pflags;
      int *quoted_dollar_atp, *contains_dollar_at;
 {
   char *temp1;
@@ -6437,7 +7354,9 @@ chk_atstar (name, quoted, quoted_dollar_atp, contains_dollar_at)
     }
   else if (name[0] == '*' && name[1] == '\0' && quoted == 0)
     {
-      if (contains_dollar_at)
+      /* Need more checks here that parallel what string_list_pos_params and
+        param_expand do. Check expand_no_split_dollar_star and ??? */
+      if (contains_dollar_at && expand_no_split_dollar_star == 0)
        *contains_dollar_at = 1;
       return 1;
     }
@@ -6446,18 +7365,18 @@ chk_atstar (name, quoted, quoted_dollar_atp, contains_dollar_at)
 #if defined (ARRAY_VARS)
   else if (valid_array_reference (name, 0))
     {
-      temp1 = mbschr (name, '[');
-      if (temp1 && temp1[1] == '@' && temp1[2] == ']')
+      temp1 = mbschr (name, LBRACK);
+      if (temp1 && temp1[1] == '@' && temp1[2] == RBRACK)
        {
          if ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) && quoted_dollar_atp)
            *quoted_dollar_atp = 1;
          if (contains_dollar_at)
            *contains_dollar_at = 1;
          return 1;
-       }       /* [ */
+       }
       /* ${array[*]}, when unquoted, should be treated like ${array[@]},
         which should result in separate words even when IFS is unset. */
-      if (temp1 && temp1[1] == '*' && temp1[2] == ']' && quoted == 0)
+      if (temp1 && temp1[1] == '*' && temp1[2] == RBRACK && quoted == 0)
        {
          if (contains_dollar_at)
            *contains_dollar_at = 1;
@@ -6469,29 +7388,36 @@ chk_atstar (name, quoted, quoted_dollar_atp, contains_dollar_at)
 }
 
 /* Parameter expand NAME, and return a new string which is the expansion,
-   or NULL if there was no expansion.
+   or NULL if there was no expansion.  NAME is as given in ${NAMEcWORD}.
    VAR_IS_SPECIAL is non-zero if NAME is one of the special variables in
    the shell, e.g., "@", "$", "*", etc.  QUOTED, if non-zero, means that
    NAME was found inside of a double-quoted expression. */
 static WORD_DESC *
-parameter_brace_expand_word (name, var_is_special, quoted, pflags, indp)
+parameter_brace_expand_word (name, var_is_special, quoted, pflags, estatep)
      char *name;
      int var_is_special, quoted, pflags;
-     arrayind_t *indp;
+     array_eltstate_t *estatep;
 {
   WORD_DESC *ret;
   char *temp, *tt;
   intmax_t arg_index;
   SHELL_VAR *var;
-  int atype, rflags;
-  arrayind_t ind;
+  int rflags;
+  array_eltstate_t es;
 
   ret = 0;
   temp = 0;
   rflags = 0;
 
-  if (indp)
-    *indp = INTMAX_MIN;
+#if defined (ARRAY_VARS)
+  if (estatep)
+    es = *estatep;     /* structure copy */
+  else
+    {
+      init_eltstate (&es);
+      es.ind = INTMAX_MIN;
+    }
+#endif
 
   /* Handle multiple digit arguments, as in ${11}. */  
   if (legal_number (name, &arg_index))
@@ -6514,40 +7440,75 @@ parameter_brace_expand_word (name, var_is_special, quoted, pflags, indp)
 
       ret = param_expand (tt, &sindex, quoted, (int *)NULL, (int *)NULL,
                          (int *)NULL, (int *)NULL, pflags);
+
+      /* Make sure we note that we saw a quoted null string and pass the flag back
+        to the caller in addition to the value. */
+      if ((quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) && STR_DOLLAR_AT_STAR (name) &&
+         ret && ret->word && QUOTED_NULL (ret->word))
+       ret->flags |= W_HASQUOTEDNULL;
+
       free (tt);
     }
 #if defined (ARRAY_VARS)
   else if (valid_array_reference (name, 0))
     {
 expand_arrayref:
+      var = array_variable_part (name, 0, &tt, (int *)0);
+      /* These are the cases where word splitting will not be performed */
       if (pflags & PF_ASSIGNRHS)
        {
-         var = array_variable_part (name, &tt, (int *)0);
-         if (ALL_ELEMENT_SUB (tt[0]) && tt[1] == ']')
+         if (ALL_ELEMENT_SUB (tt[0]) && tt[1] == RBRACK)
            {
              /* Only treat as double quoted if array variable */
              if (var && (array_p (var) || assoc_p (var)))
-               /* XXX - bash-4.4/bash-5.0 pass AV_ASSIGNRHS */
-               temp = array_value (name, quoted|Q_DOUBLE_QUOTES, AV_ASSIGNRHS, &atype, &ind);
+               temp = array_value (name, quoted|Q_DOUBLE_QUOTES, AV_ASSIGNRHS, &es);
              else              
-               temp = array_value (name, quoted, 0, &atype, &ind);
+               temp = array_value (name, quoted, 0, &es);
            }
          else
-           temp = array_value (name, quoted, 0, &atype, &ind);
+           temp = array_value (name, quoted, 0, &es);
        }
+      /* Posix interp 888 */
+      else if (pflags & PF_NOSPLIT2)
+       {
+         /* Special cases, then general case, for each of A[@], A[*], A[n] */
+#if defined (HANDLE_MULTIBYTE)
+          if (tt[0] == '@' && tt[1] == RBRACK && var && quoted == 0 && ifs_is_set && ifs_is_null == 0 && ifs_firstc[0] != ' ')
+#else
+         if (tt[0] == '@' && tt[1] == RBRACK && var && quoted == 0 && ifs_is_set && ifs_is_null == 0 && ifs_firstc != ' ')
+#endif
+           temp = array_value (name, Q_DOUBLE_QUOTES, AV_ASSIGNRHS, &es);
+         else if (tt[0] == '@' && tt[1] == RBRACK)
+           temp = array_value (name, quoted, 0, &es);
+         else if (tt[0] == '*' && tt[1] == RBRACK && expand_no_split_dollar_star && ifs_is_null)
+           temp = array_value (name, Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT, 0, &es);
+         else if (tt[0] == '*' && tt[1] == RBRACK)
+           temp = array_value (name, quoted, 0, &es);
+         else
+           temp = array_value (name, quoted, 0, &es);
+       }                 
+      else if (tt[0] == '*' && tt[1] == RBRACK && expand_no_split_dollar_star && ifs_is_null)
+       temp = array_value (name, Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT, 0, &es);
       else
-       temp = array_value (name, quoted, 0, &atype, &ind);
-      if (atype == 0 && temp)
+       temp = array_value (name, quoted, 0, &es);
+      if (es.subtype == 0 && temp)
        {
          temp = (*temp && (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)))
                    ? quote_string (temp)
                    : quote_escapes (temp);
          rflags |= W_ARRAYIND;
-         if (indp)
-           *indp = ind;
-       }                 
-      else if (atype == 1 && temp && QUOTED_NULL (temp) && (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)))
+       }
+      /* Note that array[*] and array[@] expanded to a quoted null string by
+        returning the W_HASQUOTEDNULL flag to the caller in addition to TEMP. */
+      else if (es.subtype == 1 && temp && QUOTED_NULL (temp) && (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)))
        rflags |= W_HASQUOTEDNULL;
+      else if (es.subtype == 2 && temp && QUOTED_NULL (temp) && (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)))
+       rflags |= W_HASQUOTEDNULL;
+
+      if (estatep)
+       *estatep = es;  /* structure copy */
+      else
+       flush_eltstate (&es);
     }
 #endif
   else if (var = find_variable (name))
@@ -6555,7 +7516,15 @@ expand_arrayref:
       if (var_isset (var) && invisible_p (var) == 0)
        {
 #if defined (ARRAY_VARS)
-         if (assoc_p (var))
+         /* We avoid a memory leak by saving TT as the memory allocated by
+            assoc_to_string or array_to_string and leaving it 0 otherwise,
+            then freeing TT after quoting temp. */
+         tt = (char *)NULL;
+         if ((pflags & PF_ALLINDS) && assoc_p (var))
+           tt = temp = assoc_empty (assoc_cell (var)) ? (char *)NULL : assoc_to_string (assoc_cell (var), " ", quoted);
+         else if ((pflags & PF_ALLINDS) && array_p (var))
+           tt = temp = array_empty (array_cell (var)) ? (char *)NULL : array_to_string (array_cell (var), " ", quoted);
+         else if (assoc_p (var))
            temp = assoc_reference (assoc_cell (var), "0");
          else if (array_p (var))
            temp = array_reference (array_cell (var), 0);
@@ -6568,7 +7537,9 @@ expand_arrayref:
          if (temp)
            temp = (*temp && (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)))
                      ? quote_string (temp)
-                     : quote_escapes (temp);
+                     : ((pflags & PF_ASSIGNRHS) ? quote_rhs (temp)
+                                                : quote_escapes (temp));
+         FREE (tt);
        }
       else
        temp = (char *)NULL;
@@ -6588,7 +7559,7 @@ expand_arrayref:
       /* y=2 ; typeset -n x=y; echo ${x} is not the same as echo ${2} in ksh */
       if (temp && *temp && legal_identifier (temp) == 0)
         {
-         last_command_exit_value = EXECUTION_FAILURE;
+         set_exit_status (EXECUTION_FAILURE);
          report_error (_("%s: invalid variable name for name reference"), temp);
          temp = &expand_param_error;
         }
@@ -6615,6 +7586,7 @@ parameter_brace_find_indir (name, var_is_special, quoted, find_nameref)
   char *temp, *t;
   WORD_DESC *w;
   SHELL_VAR *v;
+  int pflags, oldex;
 
   if (find_nameref && var_is_special == 0 && (v = find_variable_last_nameref (name, 0)) &&
       nameref_p (v) && (t = nameref_cell (v)) && *t)
@@ -6623,12 +7595,23 @@ parameter_brace_find_indir (name, var_is_special, quoted, find_nameref)
   /* If var_is_special == 0, and name is not an array reference, this does
      more expansion than necessary.  It should really look up the variable's
      value and not try to expand it. */
-  w = parameter_brace_expand_word (name, var_is_special, quoted, PF_IGNUNBOUND, 0);
+  pflags = PF_IGNUNBOUND;
+  /* Note that we're not going to be doing word splitting here */
+  if (var_is_special)
+    {
+      pflags |= PF_ASSIGNRHS;  /* suppresses word splitting */
+      oldex = expand_no_split_dollar_star;
+      expand_no_split_dollar_star = 1;
+    }
+  w = parameter_brace_expand_word (name, var_is_special, quoted, pflags, 0);
+  if (var_is_special)
+    expand_no_split_dollar_star = oldex;
+
   t = w->word;
   /* Have to dequote here if necessary */
   if (t)
     {
-      temp = (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT))
+      temp = ((quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) || var_is_special)
                ? dequote_string (t)
                : dequote_escapes (t);
       free (t);
@@ -6642,12 +7625,12 @@ parameter_brace_find_indir (name, var_is_special, quoted, find_nameref)
 /* Expand an indirect reference to a variable: ${!NAME} expands to the
    value of the variable whose name is the value of NAME. */
 static WORD_DESC *
-parameter_brace_expand_indir (name, var_is_special, quoted, quoted_dollar_atp, contains_dollar_at)
+parameter_brace_expand_indir (name, var_is_special, quoted, pflags, quoted_dollar_atp, contains_dollar_at)
      char *name;
-     int var_is_special, quoted;
+     int var_is_special, quoted, pflags;
      int *quoted_dollar_atp, *contains_dollar_at;
 {
-  char *temp, *t;
+  char *t;
   WORD_DESC *w;
   SHELL_VAR *v;
 
@@ -6667,15 +7650,47 @@ parameter_brace_expand_indir (name, var_is_special, quoted, quoted_dollar_atp, c
        }
     }
 
+  /* An indirect reference to a positional parameter or a special parameter
+     is ok.  Indirect references to array references, as explained above, are
+     ok (currently).  Only references to unset variables are errors at this
+     point. */
+  if (legal_identifier (name) && v == 0)
+    {
+      report_error (_("%s: invalid indirect expansion"), name);
+      w = alloc_word_desc ();
+      w->word = &expand_param_error;
+      w->flags = 0;
+      return (w);
+    }
+      
   t = parameter_brace_find_indir (name, var_is_special, quoted, 0);
 
-  chk_atstar (t, quoted, quoted_dollar_atp, contains_dollar_at);
+  chk_atstar (t, quoted, pflags, quoted_dollar_atp, contains_dollar_at);
+
+#if defined (ARRAY_VARS)
+  /* Array references to unset variables are also an error */
+  if (t == 0 && valid_array_reference (name, 0))
+    {
+      v = array_variable_part (name, 0, (char **)0, (int *)0);
+      if (v == 0)
+       {
+         report_error (_("%s: invalid indirect expansion"), name);
+         w = alloc_word_desc ();
+         w->word = &expand_param_error;
+         w->flags = 0;
+         return (w);
+       }
+      else
+        return (WORD_DESC *)NULL;      
+    }
+#endif
+
   if (t == 0)
     return (WORD_DESC *)NULL;
 
   if (valid_brace_expansion_word (t, SPECIAL_VAR (t, 0)) == 0)
     {
-      report_error (_("%s: bad substitution"), t);
+      report_error (_("%s: invalid variable name"), t);
       free (t);
       w = alloc_word_desc ();
       w->word = &expand_param_error;
@@ -6683,7 +7698,7 @@ parameter_brace_expand_indir (name, var_is_special, quoted, quoted_dollar_atp, c
       return (w);
     }
        
-  w = parameter_brace_expand_word (t, SPECIAL_VAR(t, 0), quoted, 0, 0);
+  w = parameter_brace_expand_word (t, SPECIAL_VAR(t, 0), quoted, pflags, 0);
   free (t);
 
   return w;
@@ -6694,14 +7709,16 @@ parameter_brace_expand_indir (name, var_is_special, quoted, quoted_dollar_atp, c
    "-", "+", or "=".  QUOTED is true if the entire brace expression occurs
    between double quotes. */
 static WORD_DESC *
-parameter_brace_expand_rhs (name, value, c, quoted, pflags, qdollaratp, hasdollarat)
+parameter_brace_expand_rhs (name, value, op, quoted, pflags, qdollaratp, hasdollarat)
      char *name, *value;
-     int c, quoted, pflags, *qdollaratp, *hasdollarat;
+     int op, quoted, pflags, *qdollaratp, *hasdollarat;
 {
   WORD_DESC *w;
-  WORD_LIST *l;
-  char *t, *t1, *temp, *vname;
-  int l_hasdollat, sindex;
+  WORD_LIST *l, *tl;
+  char *t, *t1, *temp, *vname, *newval;
+  int l_hasdollat, sindex, arrayref;
+  SHELL_VAR *v;
+  array_eltstate_t es;
 
 /*itrace("parameter_brace_expand_rhs: %s:%s pflags = %d", name, value, pflags);*/
   /* If the entire expression is between double quotes, we want to treat
@@ -6717,13 +7734,29 @@ parameter_brace_expand_rhs (name, value, c, quoted, pflags, qdollaratp, hasdolla
 
   w = alloc_word_desc ();
   l_hasdollat = 0;
-  /* XXX was 0 not quoted */
-  l = *temp ? expand_string_for_rhs (temp, quoted, &l_hasdollat, (int *)NULL)
+  l = *temp ? expand_string_for_rhs (temp, quoted, op, pflags, &l_hasdollat, (int *)NULL)
            : (WORD_LIST *)0;
   if (hasdollarat)
     *hasdollarat = l_hasdollat || (l && l->next);
   if (temp != value)
     free (temp);
+
+  /* list_string takes multiple CTLNULs and turns them into an empty word
+     with W_SAWQUOTEDNULL set. Turn it back into a single CTLNUL for the
+     rest of this function and the caller. */
+  for (tl = l; tl; tl = tl->next)
+    {
+      if (tl->word && (tl->word->word == 0 || tl->word->word[0] == 0) &&
+           (tl->word->flags | W_SAWQUOTEDNULL))
+       {
+         t = make_quoted_char ('\0');
+         FREE (tl->word->word);
+         tl->word->word = t;
+         tl->word->flags |= W_QUOTED|W_HASQUOTEDNULL;
+         tl->word->flags &= ~W_SAWQUOTEDNULL;
+       }
+    }
+
   if (l)
     {
       /* If l->next is not null, we know that TEMP contained "$@", since that
@@ -6748,8 +7781,14 @@ parameter_brace_expand_rhs (name, value, c, quoted, pflags, qdollaratp, hasdolla
          temp = string_list_internal (l, " ");
          w->flags |= W_SPLITSPACE;
        }
+      else if (l_hasdollat || l->next)
+       temp = string_list_dollar_star (l, quoted, 0);
       else
-       temp = (l_hasdollat || l->next) ? string_list_dollar_star (l) : string_list (l);
+       {
+         temp = string_list (l);
+         if (temp && (QUOTED_NULL (temp) == 0) && (l->word->flags & W_SAWQUOTEDNULL))
+           w->flags |= W_SAWQUOTEDNULL;        /* XXX */
+       }
 
       /* If we have a quoted null result (QUOTED_NULL(temp)) and the word is
         a quoted null (l->next == 0 && QUOTED_NULL(l->word->word)), the
@@ -6790,16 +7829,15 @@ parameter_brace_expand_rhs (name, value, c, quoted, pflags, qdollaratp, hasdolla
   else
     temp = (char *)NULL;
 
-  if (c == '-' || c == '+')
+  if (op == '-' || op == '+')
     {
       w->word = temp;
       return w;
     }
 
-  /* c == '=' */
-  t = temp ? savestring (temp) : savestring ("");
-  t1 = dequote_string (t);
-  free (t);
+  /* op == '=' */
+  t1 = temp ? dequote_string (temp) : savestring ("");
+  free (temp);
 
   /* bash-4.4/5.0 */
   vname = name;
@@ -6811,6 +7849,7 @@ parameter_brace_expand_rhs (name, value, c, quoted, pflags, qdollaratp, hasdolla
        {
          report_error (_("%s: invalid indirect expansion"), name);
          free (vname);
+         free (t1);
          dispose_word (w);
          return &expand_wdesc_error;
        }
@@ -6818,27 +7857,80 @@ parameter_brace_expand_rhs (name, value, c, quoted, pflags, qdollaratp, hasdolla
        {
          report_error (_("%s: invalid variable name"), vname);
          free (vname);
+         free (t1);
          dispose_word (w);
          return &expand_wdesc_error;
        }
     }
     
+  arrayref = 0;
 #if defined (ARRAY_VARS)
   if (valid_array_reference (vname, 0))
-    assign_array_element (vname, t1, 0);
+    {
+      init_eltstate (&es);
+      v = assign_array_element (vname, t1, ASS_ALLOWALLSUB, &es);
+      arrayref = 1;
+      newval = es.value;
+    }
   else
 #endif /* ARRAY_VARS */
-  bind_variable (vname, t1, 0);
+  v = bind_variable (vname, t1, 0);
+
+  if (v == 0 || readonly_p (v) || noassign_p (v))      /* expansion error  */
+    {
+      if ((v == 0 || readonly_p (v)) && interactive_shell == 0 && posixly_correct)
+       {
+         last_command_exit_value = EXECUTION_FAILURE;
+         exp_jump_to_top_level (FORCE_EOF);
+       }
+      else
+       {
+         if (vname != name)
+           free (vname);
+         last_command_exit_value = EX_BADUSAGE;
+         exp_jump_to_top_level (DISCARD);
+       }
+    }
 
   stupidly_hack_special_variables (vname);
 
+  /* "In all cases, the final value of parameter shall be substituted." */
+  if (shell_compatibility_level > 51)
+    {
+      FREE (t1);
+#if defined (ARRAY_VARS)
+      if (arrayref)
+       {
+         t1 = newval;
+         flush_eltstate (&es);
+       }
+      else
+        t1 = get_variable_value (v);
+#else
+      t1 = value_cell (v);
+#endif
+    }
+
   if (vname != name)
     free (vname);
 
   /* From Posix group discussion Feb-March 2010.  Issue 7 0000221 */
-  free (temp);
 
-  w->word = t1;
+  /* If we are double-quoted or if we are not going to be performing word
+     splitting, we want to quote the value we return appropriately, like
+     the other expansions this function handles. */
+  w->word = (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) ? quote_string (t1) : quote_escapes (t1);
+  /* If we have something that's non-null, but not a quoted null string,
+     and we're not going to be performing word splitting (we know we're not
+     because the operator is `='), we can forget we saw a quoted null. */
+  if (w->word && w->word[0] && QUOTED_NULL (w->word) == 0)
+    w->flags &= ~W_SAWQUOTEDNULL;
+
+  /* If we convert a null string into a quoted null, make sure the caller
+     knows it. */
+  if ((quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) && QUOTED_NULL (w->word))
+    w->flags |= W_HASQUOTEDNULL;
+
   return w;
 }
 
@@ -6847,13 +7939,14 @@ parameter_brace_expand_rhs (name, value, c, quoted, pflags, qdollaratp, hasdolla
    used as the error message to print, otherwise a standard message is
    printed. */
 static void
-parameter_brace_expand_error (name, value)
+parameter_brace_expand_error (name, value, check_null)
      char *name, *value;
+     int check_null;
 {
   WORD_LIST *l;
   char *temp;
 
-  last_command_exit_value = EXECUTION_FAILURE; /* ensure it's non-zero */
+  set_exit_status (EXECUTION_FAILURE); /* ensure it's non-zero */
   if (value && *value)
     {
       l = expand_string (value, 0);
@@ -6862,6 +7955,8 @@ parameter_brace_expand_error (name, value)
       FREE (temp);
       dispose_words (l);
     }
+  else if (check_null == 0)
+    report_error (_("%s: parameter not set"), name);
   else
     report_error (_("%s: parameter null or not set"), name);
 
@@ -6895,9 +7990,9 @@ parameter_brace_expand_length (name)
   char *t, *newname;
   intmax_t number, arg_index;
   WORD_LIST *list;
-#if defined (ARRAY_VARS)
   SHELL_VAR *var;
-#endif
+
+  var = (SHELL_VAR *)NULL;
 
   if (name[1] == '\0')                 /* ${#} */
     number = number_of_args ();
@@ -6958,6 +8053,15 @@ parameter_brace_expand_length (name)
          number = MB_STRLEN (t);
        }
 #endif
+      /* Fast path for the common case of taking the length of a non-dynamic
+        scalar variable value. */
+      else if ((var || (var = find_variable (name + 1))) &&
+               invisible_p (var) == 0 &&
+               array_p (var) == 0 && assoc_p (var) == 0 &&
+               var->dynamic_value == 0)
+       number = value_cell (var) ? MB_STRLEN (value_cell (var)) : 0;
+      else if (var == 0 && unbound_vars_is_error == 0)
+       number = 0;
       else                             /* ${#PS1} */
        {
          newname = savestring (name);
@@ -7014,7 +8118,7 @@ verify_substring_values (v, value, substr, vtype, e1p, e2p)
 {
   char *t, *temp1, *temp2;
   arrayind_t len;
-  int expok;
+  int expok, eflag;
 #if defined (ARRAY_VARS)
  ARRAY *a;
  HASH_TABLE *h;
@@ -7027,8 +8131,10 @@ verify_substring_values (v, value, substr, vtype, e1p, e2p)
   else
     t = (char *)0;
 
-  temp1 = expand_arith_string (substr, Q_DOUBLE_QUOTES);
-  *e1p = evalexp (temp1, &expok);
+  temp1 = expand_arith_string (substr, Q_DOUBLE_QUOTES|Q_ARITH);
+  eflag = (shell_compatibility_level > 51) ? 0 : EXP_EXPANDED;
+
+  *e1p = evalexp (temp1, eflag, &expok);
   free (temp1);
   if (expok == 0)
     return (0);
@@ -7083,18 +8189,19 @@ verify_substring_values (v, value, substr, vtype, e1p, e2p)
     {
       t++;
       temp2 = savestring (t);
-      temp1 = expand_arith_string (temp2, Q_DOUBLE_QUOTES);
+      temp1 = expand_arith_string (temp2, Q_DOUBLE_QUOTES|Q_ARITH);
       free (temp2);
       t[-1] = ':';
-      *e2p = evalexp (temp1, &expok);
+      *e2p = evalexp (temp1, eflag, &expok);
       free (temp1);
       if (expok == 0)
        return (0);
+
+      /* Should we allow positional parameter length < 0 to count backwards
+        from end of positional parameters? */
 #if 1
       if ((vtype == VT_ARRAYVAR || vtype == VT_POSPARMS) && *e2p < 0)
-#else
-      /* bash-4.3: allow positional parameter length < 0 to count backwards
-        from end of positional parameters */
+#else /* XXX - postponed; this isn't really a valuable feature */
       if (vtype == VT_ARRAYVAR && *e2p < 0)
 #endif
        {
@@ -7134,24 +8241,22 @@ verify_substring_values (v, value, substr, vtype, e1p, e2p)
    by VARNAME (value of a variable or a reference to an array element).
    QUOTED is the standard description of quoting state, using Q_* defines.
    FLAGS is currently a set of flags to pass to array_value.  If IND is
-   non-null and not INTMAX_MIN, and FLAGS includes AV_USEIND, IND is
+   not INTMAX_MIN, and FLAGS includes AV_USEIND, IND is
    passed to array_value so the array index is not computed again.
    If this returns VT_VARIABLE, the caller assumes that CTLESC and CTLNUL
    characters in the value are quoted with CTLESC and takes appropriate
    steps.  For convenience, *VALP is set to the dequoted VALUE. */
 static int
-get_var_and_type (varname, value, ind, quoted, flags, varp, valp)
+get_var_and_type (varname, value, estatep, quoted, flags, varp, valp)
      char *varname, *value;
-     arrayind_t ind;
+     array_eltstate_t *estatep;
      int quoted, flags;
      SHELL_VAR **varp;
      char **valp;
 {
   int vtype, want_indir;
   char *temp, *vname;
-  WORD_DESC *wd;
   SHELL_VAR *v;
-  arrayind_t lind;
 
   want_indir = *varname == '!' &&
     (legal_variable_starter ((unsigned char)varname[1]) || DIGIT (varname[1])
@@ -7179,10 +8284,13 @@ get_var_and_type (varname, value, ind, quoted, flags, varp, valp)
 #if defined (ARRAY_VARS)
   if (valid_array_reference (vname, 0))
     {
-      v = array_variable_part (vname, &temp, (int *)0);
+      v = array_variable_part (vname, 0, &temp, (int *)0);
       /* If we want to signal array_value to use an already-computed index,
-        set LIND to that index */
-      lind = (ind != INTMAX_MIN && (flags & AV_USEIND)) ? ind : 0;
+        the caller will set ESTATEP->IND to that index and pass AV_USEIND in
+        FLAGS. */
+      if (estatep && (flags & AV_USEIND) == 0)
+       estatep->ind = INTMAX_MIN;
+
       if (v && invisible_p (v))
        {
          vtype = VT_ARRAYMEMBER;
@@ -7190,8 +8298,8 @@ get_var_and_type (varname, value, ind, quoted, flags, varp, valp)
          *valp = (char *)NULL;
        }
       if (v && (array_p (v) || assoc_p (v)))
-       { /* [ */
-         if (ALL_ELEMENT_SUB (temp[0]) && temp[1] == ']')
+       {
+         if (ALL_ELEMENT_SUB (temp[0]) && temp[1] == RBRACK)
            {
              /* Callers have to differentiate between indexed and associative */
              vtype = VT_ARRAYVAR;
@@ -7202,24 +8310,24 @@ get_var_and_type (varname, value, ind, quoted, flags, varp, valp)
          else
            {
              vtype = VT_ARRAYMEMBER;
-             *valp = array_value (vname, Q_DOUBLE_QUOTES, flags, (int *)NULL, &lind);
+             *valp = array_value (vname, Q_DOUBLE_QUOTES, flags, estatep);
            }
          *varp = v;
        }
-      else if (v && (ALL_ELEMENT_SUB (temp[0]) && temp[1] == ']'))
+      else if (v && (ALL_ELEMENT_SUB (temp[0]) && temp[1] == RBRACK))
        {
          vtype = VT_VARIABLE;
          *varp = v;
          if (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT))
-           *valp = dequote_string (value);
+           *valp = value ? dequote_string (value) : (char *)NULL;
          else
-           *valp = dequote_escapes (value);
+           *valp = value ? dequote_escapes (value) : (char *)NULL;
        }
       else
        {
          vtype = VT_ARRAYMEMBER;
          *varp = v;
-         *valp = array_value (vname, Q_DOUBLE_QUOTES, flags, (int *)NULL, &lind);
+         *valp = array_value (vname, Q_DOUBLE_QUOTES, flags, estatep);
        }
     }
   else if ((v = find_variable (vname)) && (invisible_p (v) == 0) && (assoc_p (v) || array_p (v)))
@@ -7249,6 +8357,392 @@ get_var_and_type (varname, value, ind, quoted, flags, varp, valp)
   return vtype;
 }
 
+/***********************************************************/
+/*                                                        */
+/* Functions to perform transformations on variable values */
+/*                                                        */
+/***********************************************************/
+
+static char *
+string_var_assignment (v, s)
+     SHELL_VAR *v;
+     char *s;
+{
+  char flags[MAX_ATTRIBUTES], *ret, *val;
+  int i;
+
+  val = (v && (invisible_p (v) || var_isset (v) == 0)) ? (char *)NULL : sh_quote_reusable (s, 0);
+  i = var_attribute_string (v, 0, flags);
+  if (i == 0 && val == 0)
+    return (char *)NULL;
+
+  ret = (char *)xmalloc (i + STRLEN (val) + strlen (v->name) + 16 + MAX_ATTRIBUTES);
+  if (i > 0 && val == 0)
+    sprintf (ret, "declare -%s %s", flags, v->name);
+  else if (i > 0)
+    sprintf (ret, "declare -%s %s=%s", flags, v->name, val);
+  else
+    sprintf (ret, "%s=%s", v->name, val);
+  free (val);
+  return ret;
+}
+
+#if defined (ARRAY_VARS)
+static char *
+array_var_assignment (v, itype, quoted, atype)
+     SHELL_VAR *v;
+     int itype, quoted, atype;
+{
+  char *ret, *val, flags[MAX_ATTRIBUTES];
+  int i;
+
+  if (v == 0)
+    return (char *)NULL;
+  if (atype == 2)
+    val = array_p (v) ? array_to_kvpair (array_cell (v), 0)
+                     : assoc_to_kvpair (assoc_cell (v), 0);
+  else
+    val = array_p (v) ? array_to_assign (array_cell (v), 0)
+                     : assoc_to_assign (assoc_cell (v), 0);
+
+  if (val == 0 && (invisible_p (v) || var_isset (v) == 0))
+    ;  /* placeholder */
+  else if (val == 0)
+    {
+      val = (char *)xmalloc (3);
+      val[0] = LPAREN;
+      val[1] = RPAREN;
+      val[2] = 0;
+    }
+  else
+    {
+      ret = (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) ? quote_string (val) : quote_escapes (val);
+      free (val);
+      val = ret;
+    }
+
+  if (atype == 2)
+    return val;
+
+  i = var_attribute_string (v, 0, flags);
+  ret = (char *)xmalloc (i + STRLEN (val) + strlen (v->name) + 16);
+  if (val)
+    sprintf (ret, "declare -%s %s=%s", flags, v->name, val);
+  else
+    sprintf (ret, "declare -%s %s", flags, v->name);
+  free (val);
+  return ret;
+}
+#endif
+
+static char *
+pos_params_assignment (list, itype, quoted)
+     WORD_LIST *list;
+     int itype;
+     int quoted;
+{
+  char *temp, *ret;
+
+  /* first, we transform the list to quote each word. */
+  temp = list_transform ('Q', (SHELL_VAR *)0, list, itype, quoted);
+  ret = (char *)xmalloc (strlen (temp) + 8);
+  strcpy (ret, "set -- ");
+  strcpy (ret + 7, temp);
+  free (temp);
+  return ret;
+}
+
+static char *
+string_transform (xc, v, s)
+     int xc;
+     SHELL_VAR *v;
+     char *s;
+{
+  char *ret, flags[MAX_ATTRIBUTES], *t;
+  int i;
+
+  if (((xc == 'A' || xc == 'a') && v == 0))
+    return (char *)NULL;
+  else if (xc != 'a' && xc != 'A' && s == 0)
+    return (char *)NULL;
+
+  switch (xc)
+    {
+      /* Transformations that interrogate the variable */
+      case 'a':
+       i = var_attribute_string (v, 0, flags);
+       ret = (i > 0) ? savestring (flags) : (char *)NULL;
+       break;
+      case 'A':
+       ret = string_var_assignment (v, s);
+       break;
+      case 'K':
+      case 'k':
+       ret = sh_quote_reusable (s, 0);
+       break;
+      /* Transformations that modify the variable's value */
+      case 'E':
+       t = ansiexpand (s, 0, strlen (s), (int *)0);
+       ret = dequote_escapes (t);
+       free (t);
+       break;
+      case 'P':
+       ret = decode_prompt_string (s);
+       break;
+      case 'Q':
+       ret = sh_quote_reusable (s, 0);
+       break;
+      case 'U':
+       ret = sh_modcase (s, 0, CASE_UPPER);
+       break;
+      case 'u':
+       ret = sh_modcase (s, 0, CASE_UPFIRST);  /* capitalize */
+       break;
+      case 'L':
+       ret = sh_modcase (s, 0, CASE_LOWER);
+       break;
+      default:
+       ret = (char *)NULL;
+       break;
+    }
+  return ret;
+}
+
+static char *
+list_transform (xc, v, list, itype, quoted)
+     int xc;
+     SHELL_VAR *v;
+     WORD_LIST *list;
+     int itype, quoted;
+{
+  WORD_LIST *new, *l;
+  WORD_DESC *w;
+  char *tword;
+  int qflags;
+
+  for (new = (WORD_LIST *)NULL, l = list; l; l = l->next)
+    {
+      tword = string_transform (xc, v, l->word->word);
+      w = alloc_word_desc ();
+      w->word = tword ? tword : savestring ("");       /* XXX */
+      new = make_word_list (w, new);
+    }
+  l = REVERSE_LIST (new, WORD_LIST *);
+
+  qflags = quoted;
+  /* If we are expanding in a context where word splitting will not be
+     performed, treat as quoted.  This changes how $* will be expanded. */
+  if (itype == '*' && expand_no_split_dollar_star && ifs_is_null)
+    qflags |= Q_DOUBLE_QUOTES;         /* Posix interp 888 */
+
+  tword = string_list_pos_params (itype, l, qflags, 0);
+  dispose_words (l);
+
+  return (tword);
+}
+
+static char *
+parameter_list_transform (xc, itype, quoted)
+     int xc;
+     int itype;
+     int quoted;
+{
+  char *ret;
+  WORD_LIST *list;
+
+  list = list_rest_of_args ();
+  if (list == 0)
+    return ((char *)NULL);
+  if (xc == 'A')
+    ret = pos_params_assignment (list, itype, quoted);
+  else
+    ret = list_transform (xc, (SHELL_VAR *)0, list, itype, quoted);
+  dispose_words (list);
+  return (ret);
+}
+
+#if defined (ARRAY_VARS)
+static char *
+array_transform (xc, var, starsub, quoted)
+     int xc;
+     SHELL_VAR *var;
+     int starsub;      /* so we can figure out how it's indexed */
+     int quoted;
+{
+  ARRAY *a;
+  HASH_TABLE *h;
+  int itype, qflags;
+  char *ret;
+  WORD_LIST *list;
+  SHELL_VAR *v;
+
+  v = var;     /* XXX - for now */
+
+  itype = starsub ? '*' : '@';
+
+  if (xc == 'A')
+    return (array_var_assignment (v, itype, quoted, 1));
+  else if (xc == 'K')
+    return (array_var_assignment (v, itype, quoted, 2));
+
+  /* special case for unset arrays and attributes */
+  if (xc == 'a' && (invisible_p (v) || var_isset (v) == 0))
+    {
+      char flags[MAX_ATTRIBUTES];
+      int i;
+
+      i = var_attribute_string (v, 0, flags);
+      return ((i > 0) ? savestring (flags) : (char *)NULL);
+    }
+
+  a = (v && array_p (v)) ? array_cell (v) : 0;
+  h = (v && assoc_p (v)) ? assoc_cell (v) : 0;
+
+  /* XXX - for now */
+  if (xc == 'k')
+    {
+      if (v == 0)
+       return ((char *)NULL);
+      list = array_p (v) ? array_to_kvpair_list (a) : assoc_to_kvpair_list (h);
+      qflags = quoted;
+      /* If we are expanding in a context where word splitting will not be
+        performed, treat as quoted.  This changes how $* will be expanded. */
+      if (itype == '*' && expand_no_split_dollar_star && ifs_is_null)
+       qflags |= Q_DOUBLE_QUOTES;              /* Posix interp 888 */
+
+      ret = string_list_pos_params (itype, list, qflags, 0);
+      dispose_words (list);
+      return ret;
+    }
+
+  list = a ? array_to_word_list (a) : (h ? assoc_to_word_list (h) : 0);
+  if (list == 0)
+   return ((char *)NULL);
+  ret = list_transform (xc, v, list, itype, quoted);
+  dispose_words (list);
+
+  return ret;
+}
+#endif /* ARRAY_VARS */
+
+static int
+valid_parameter_transform (xform)
+     char *xform;
+{
+  if (xform[1])
+    return 0;
+
+  /* check for valid values of xform[0] */
+  switch (xform[0])
+    {
+    case 'a':          /* expand to a string with just attributes */
+    case 'A':          /* expand as an assignment statement with attributes */
+    case 'K':          /* expand assoc array to list of key/value pairs */
+    case 'k':          /* XXX - for now */
+    case 'E':          /* expand like $'...' */
+    case 'P':          /* expand like prompt string */
+    case 'Q':          /* quote reusably */
+    case 'U':          /* transform to uppercase */
+    case 'u':          /* transform by capitalizing */
+    case 'L':          /* transform to lowercase */
+      return 1;
+    default:
+      return 0;
+    }
+}
+      
+static char *
+parameter_brace_transform (varname, value, estatep, xform, rtype, quoted, pflags, flags)
+     char *varname, *value;
+     array_eltstate_t *estatep;
+     char *xform;
+     int rtype, quoted, pflags, flags;
+{
+  int vtype, xc, starsub;
+  char *temp1, *val, *oname;
+  SHELL_VAR *v;
+
+  xc = xform[0];
+  if (value == 0 && xc != 'A' && xc != 'a')
+    return ((char *)NULL);
+
+  oname = this_command_name;
+  this_command_name = varname;
+
+  vtype = get_var_and_type (varname, value, estatep, quoted, flags, &v, &val);
+  if (vtype == -1)
+    {
+      this_command_name = oname;
+      return ((char *)NULL);
+    }
+
+  if (xform[0] == 0 || valid_parameter_transform (xform) == 0)
+    {
+      this_command_name = oname;
+      if (vtype == VT_VARIABLE)
+       FREE (val);
+      return (interactive_shell ? &expand_param_error : &expand_param_fatal);
+    }
+
+  starsub = vtype & VT_STARSUB;
+  vtype &= ~VT_STARSUB;
+
+  /* If we are asked to display the attributes of an unset variable, V will
+     be NULL after the call to get_var_and_type. Double-check here. */
+  if ((xc == 'a' || xc == 'A') && vtype == VT_VARIABLE && varname && v == 0)
+    v = find_variable (varname);
+
+  temp1 = (char *)NULL;                /* shut up gcc */
+  switch (vtype)
+    {
+    case VT_VARIABLE:
+    case VT_ARRAYMEMBER:
+      temp1 = string_transform (xc, v, val);
+      if (vtype == VT_VARIABLE)
+       FREE (val);
+      if (temp1)
+       {
+         val = (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES))
+                       ? quote_string (temp1)
+                       : quote_escapes (temp1);
+         free (temp1);
+         temp1 = val;
+       }
+      break;
+#if defined (ARRAY_VARS)
+    case VT_ARRAYVAR:
+      temp1 = array_transform (xc, v, starsub, quoted);
+      if (temp1 && quoted == 0 && ifs_is_null)
+       {
+               /* Posix interp 888 */
+       }
+      else if (temp1 && ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) == 0))
+       {
+         val = quote_escapes (temp1);
+         free (temp1);
+         temp1 = val;
+       }
+      break;
+#endif
+    case VT_POSPARMS:
+      temp1 = parameter_list_transform (xc, varname[0], quoted);
+      if (temp1 && quoted == 0 && ifs_is_null)
+       {
+               /* Posix interp 888 */
+       }
+      else if (temp1 && ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) == 0))
+       {
+         val = quote_escapes (temp1);
+         free (temp1);
+         temp1 = val;
+       }
+      break;
+    }
+
+  this_command_name = oname;
+  return temp1;
+}
+
 /******************************************************/
 /*                                                   */
 /* Functions to extract substrings of variable values */
@@ -7291,11 +8785,11 @@ mb_substring (string, s, e)
    VARNAME.  If VARNAME is an array variable, use the array elements. */
 
 static char *
-parameter_brace_substring (varname, value, ind, substr, quoted, flags)
+parameter_brace_substring (varname, value, estatep, substr, quoted, pflags, flags)
      char *varname, *value;
-     int ind;
+     array_eltstate_t *estatep;
      char *substr;
-     int quoted, flags;
+     int quoted, pflags, flags;
 {
   intmax_t e1, e2;
   int vtype, r, starsub;
@@ -7308,7 +8802,7 @@ parameter_brace_substring (varname, value, ind, substr, quoted, flags)
   oname = this_command_name;
   this_command_name = varname;
 
-  vtype = get_var_and_type (varname, value, ind, quoted, flags, &v, &val);
+  vtype = get_var_and_type (varname, value, estatep, quoted, flags, &v, &val);
   if (vtype == -1)
     {
       this_command_name = oname;
@@ -7347,8 +8841,34 @@ parameter_brace_substring (varname, value, ind, substr, quoted, flags)
       FREE (tt);
       break;
     case VT_POSPARMS:
-      tt = pos_params (varname, e1, e2, quoted);
-      if ((quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) == 0)
+    case VT_ARRAYVAR:
+      if (vtype == VT_POSPARMS)
+       tt = pos_params (varname, e1, e2, quoted, pflags);
+#if defined (ARRAY_VARS)
+        /* assoc_subrange and array_subrange both call string_list_pos_params,
+          so we can treat this case just like VT_POSPARAMS. */
+      else if (assoc_p (v))
+       /* we convert to list and take first e2 elements starting at e1th
+          element -- officially undefined for now */   
+       tt = assoc_subrange (assoc_cell (v), e1, e2, starsub, quoted, pflags);
+      else
+       /* We want E2 to be the number of elements desired (arrays can be
+          sparse, so verify_substring_values just returns the numbers
+          specified and we rely on array_subrange to understand how to
+          deal with them). */
+       tt = array_subrange (array_cell (v), e1, e2, starsub, quoted, pflags);
+#endif
+      /* We want to leave this alone in every case where pos_params/
+        string_list_pos_params quotes the list members */
+      if (tt && quoted == 0 && ifs_is_null)
+       {
+         temp = tt;    /* Posix interp 888 */
+       }
+      else if (tt && quoted == 0 && (pflags & PF_ASSIGNRHS))
+       {
+         temp = tt;    /* Posix interp 888 */
+       }
+      else if ((quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) == 0)
        {
          temp = tt ? quote_escapes (tt) : (char *)NULL;
          FREE (tt);
@@ -7356,21 +8876,7 @@ parameter_brace_substring (varname, value, ind, substr, quoted, flags)
       else
        temp = tt;
       break;
-#if defined (ARRAY_VARS)
-    case VT_ARRAYVAR:
-      if (assoc_p (v))
-       /* we convert to list and take first e2 elements starting at e1th
-          element -- officially undefined for now */   
-       temp = assoc_subrange (assoc_cell (v), e1, e2, starsub, quoted);
-      else
-      /* We want E2 to be the number of elements desired (arrays can be sparse,
-        so verify_substring_values just returns the numbers specified and we
-        rely on array_subrange to understand how to deal with them). */
-       temp = array_subrange (array_cell (v), e1, e2, starsub, quoted);
-      /* array_subrange now calls array_quote_escapes as appropriate, so the
-        caller no longer needs to. */
-      break;
-#endif
+
     default:
       temp = (char *)NULL;
     }
@@ -7384,43 +8890,54 @@ parameter_brace_substring (varname, value, ind, substr, quoted, flags)
 /*                                                             */
 /****************************************************************/
 
-#if 0  /* Unused */
 static int
 shouldexp_replacement (s)
      char *s;
 {
-  register char *p;
+  size_t slen;
+  int sindex, c;
+  DECLARE_MBSTATE;
 
-  for (p = s; p && *p; p++)
+  sindex = 0;
+  slen = STRLEN (s);
+  while (c = s[sindex])
     {
-      if (*p == '\\')
-       p++;
-      else if (*p == '&')
+      if (c == '\\')
+       {
+         sindex++;
+         if (s[sindex] == 0)
+           return 0;
+         /* We want to remove this backslash because we treat it as special
+            in this context. THIS ASSUMES THE STRING IS PROCESSED BY
+            strcreplace() OR EQUIVALENT that handles removing backslashes
+            preceding the special character. */
+         if (s[sindex] == '&')
+           return 1;
+         if (s[sindex] == '\\')
+           return 1;
+       }
+      else if (c == '&')
        return 1;
+      ADVANCE_CHAR (s, slen, sindex);
     }
   return 0;
 }
-#endif
 
 char *
 pat_subst (string, pat, rep, mflags)
      char *string, *pat, *rep;
      int mflags;
 {
-  char *ret, *s, *e, *str, *rstr, *mstr;
+  char *ret, *s, *e, *str, *rstr, *mstr, *send;
   int rptr, mtype, rxpand, mlen;
   size_t rsize, l, replen, rslen;
+  DECLARE_MBSTATE;
 
-  if (string  == 0)
+  if (string == 0)
     return (savestring (""));
 
   mtype = mflags & MATCH_TYPEMASK;
-
-#if 0  /* bash-4.2 ? */
-  rxpand = (rep && *rep) ? shouldexp_replacement (rep) : 0;
-#else
-  rxpand = 0;
-#endif
+  rxpand = mflags & MATCH_EXPREP;
 
   /* Special cases:
    *   1.  A null pattern with mtype == MATCH_BEG means to prefix STRING
@@ -7429,40 +8946,39 @@ pat_subst (string, pat, rep, mflags)
    *       STRING and return the result.
    *   3.  A null STRING with a matching pattern means to append REP to
    *       STRING and return the result.
-   * These don't understand or process `&' in the replacement string.
+   *
+   * These process `&' in the replacement string, like `sed' does when
+   * presented with a BRE of `^' or `$'.
    */
   if ((pat == 0 || *pat == 0) && (mtype == MATCH_BEG || mtype == MATCH_END))
     {
-      replen = STRLEN (rep);
+      rstr = (mflags & MATCH_EXPREP) ? strcreplace (rep, '&', "", 2) : rep;
+      rslen = STRLEN (rstr);
       l = STRLEN (string);
-      ret = (char *)xmalloc (replen + l + 2);
-      if (replen == 0)
+      ret = (char *)xmalloc (rslen + l + 2);
+      if (rslen == 0)
        strcpy (ret, string);
       else if (mtype == MATCH_BEG)
        {
-         strcpy (ret, rep);
-         strcpy (ret + replen, string);
+         strcpy (ret, rstr);
+         strcpy (ret + rslen, string);
        }
       else
        {
          strcpy (ret, string);
-         strcpy (ret + l, rep);
+         strcpy (ret + l, rstr);
        }
+      if (rstr != rep)
+       free (rstr);
       return (ret);
     }
   else if (*string == 0 && (match_pattern (string, pat, mtype, &s, &e) != 0))
-    {
-      replen = STRLEN (rep);
-      ret = (char *)xmalloc (replen + 1);
-      if (replen == 0)
-       ret[0] = '\0';
-      else
-       strcpy (ret, rep);
-      return (ret);
-    }
+    return (mflags & MATCH_EXPREP) ? strcreplace (rep, '&', "", 2)
+                                  : (rep ? savestring (rep) : savestring (""));
 
   ret = (char *)xmalloc (rsize = 64);
   ret[0] = '\0';
+  send = string + strlen (string);
 
   for (replen = STRLEN (rep), rptr = 0, str = string; *str;)
     {
@@ -7478,7 +8994,7 @@ pat_subst (string, pat, rep, mflags)
          for (x = 0; x < mlen; x++)
            mstr[x] = s[x];
          mstr[mlen] = '\0';
-         rstr = strcreplace (rep, '&', mstr, 0);
+         rstr = strcreplace (rep, '&', mstr, 2);
          free (mstr);
          rslen = strlen (rstr);
         }
@@ -7516,16 +9032,28 @@ pat_subst (string, pat, rep, mflags)
        {
          /* On a zero-length match, make sure we copy one character, since
             we increment one character to avoid infinite recursion. */
-         RESIZE_MALLOCED_BUFFER (ret, rptr, 1, rsize, 64);
+         char *p, *origp, *origs;
+         size_t clen;
+
+         RESIZE_MALLOCED_BUFFER (ret, rptr, locale_mb_cur_max, rsize, 64);
+#if defined (HANDLE_MULTIBYTE)
+         p = origp = ret + rptr;
+         origs = str;
+         COPY_CHAR_P (p, str, send);
+         rptr += p - origp;
+         e += str - origs;
+#else
          ret[rptr++] = *str++;
          e++;          /* avoid infinite recursion on zero-length match */
+#endif
        }
     }
 
   /* Now copy the unmatched portion of the input string */
   if (str && *str)
     {
-      RESIZE_MALLOCED_BUFFER (ret, rptr, STRLEN(str) + 1, rsize, 64);
+      l = send - str + 1;
+      RESIZE_MALLOCED_BUFFER (ret, rptr, l, rsize, 64);
       strcpy (ret + rptr, str);
     }
   else
@@ -7543,7 +9071,7 @@ pos_params_pat_subst (string, pat, rep, mflags)
   WORD_LIST *save, *params;
   WORD_DESC *w;
   char *ret;
-  int pchar, qflags;
+  int pchar, qflags, pflags;
 
   save = params = list_rest_of_args ();
   if (save == 0)
@@ -7560,9 +9088,14 @@ pos_params_pat_subst (string, pat, rep, mflags)
 
   pchar = (mflags & MATCH_STARSUB) == MATCH_STARSUB ? '*' : '@';
   qflags = (mflags & MATCH_QUOTED) == MATCH_QUOTED ? Q_DOUBLE_QUOTES : 0;
+  pflags = (mflags & MATCH_ASSIGNRHS) == MATCH_ASSIGNRHS ? PF_ASSIGNRHS : 0;
 
-  ret = string_list_pos_params (pchar, save, qflags);
+  /* If we are expanding in a context where word splitting will not be
+     performed, treat as quoted. This changes how $* will be expanded. */
+  if (pchar == '*' && (mflags & MATCH_ASSIGNRHS) && expand_no_split_dollar_star && ifs_is_null)
+    qflags |= Q_DOUBLE_QUOTES;         /* Posix interp 888 */
 
+  ret = string_list_pos_params (pchar, save, qflags, pflags);
   dispose_words (save);
 
   return (ret);
@@ -7573,24 +9106,28 @@ pos_params_pat_subst (string, pat, rep, mflags)
    and the string to substitute.  QUOTED is a flags word containing
    the type of quoting currently in effect. */
 static char *
-parameter_brace_patsub (varname, value, ind, patsub, quoted, pflags, flags)
+parameter_brace_patsub (varname, value, estatep, patsub, quoted, pflags, flags)
      char *varname, *value;
-     int ind;
+     array_eltstate_t *estatep;
      char *patsub;
      int quoted, pflags, flags;
 {
   int vtype, mflags, starsub, delim;
-  char *val, *temp, *pat, *rep, *p, *lpatsub, *tt;
+  char *val, *temp, *pat, *rep, *p, *lpatsub, *tt, *oname;
   SHELL_VAR *v;
 
   if (value == 0)
     return ((char *)NULL);
 
-  this_command_name = varname;
+  oname = this_command_name;
+  this_command_name = varname;         /* error messages */
 
-  vtype = get_var_and_type (varname, value, ind, quoted, flags, &v, &val);
+  vtype = get_var_and_type (varname, value, estatep, quoted, flags, &v, &val);
   if (vtype == -1)
-    return ((char *)NULL);
+    {
+      this_command_name = oname;
+      return ((char *)NULL);
+    }
 
   starsub = vtype & VT_STARSUB;
   vtype &= ~VT_STARSUB;
@@ -7640,13 +9177,22 @@ parameter_brace_patsub (varname, value, ind, patsub, quoted, pflags, flags)
         the entire expansion is double-quoted because the parser and string
         extraction functions treated quotes in the replacement string as
         special.  THIS IS NOT BACKWARDS COMPATIBLE WITH BASH-4.2. */
-      if (shell_compatibility_level > 42)
+      if (shell_compatibility_level > 42 && patsub_replacement == 0)
        rep = expand_string_if_necessary (rep, quoted & ~(Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT), expand_string_unsplit);
+      else if (shell_compatibility_level > 42 && patsub_replacement)
+       rep = expand_string_for_patsub (rep, quoted & ~(Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT));
       /* This is the bash-4.2 code. */      
       else if ((mflags & MATCH_QUOTED) == 0)
        rep = expand_string_if_necessary (rep, quoted, expand_string_unsplit);
       else
        rep = expand_string_to_string_internal (rep, quoted, expand_string_unsplit);
+
+      /* Check whether or not to replace `&' in the replacement string after
+        expanding it, since we want to treat backslashes quoting the `&'
+        consistently. */
+      if (patsub_replacement && rep && *rep && shouldexp_replacement (rep))
+       mflags |= MATCH_EXPREP;
+
     }
 
   /* ksh93 doesn't allow the match specifier to be a part of the expanded
@@ -7695,8 +9241,22 @@ parameter_brace_patsub (varname, value, ind, patsub, quoted, pflags, flags)
        }
       break;
     case VT_POSPARMS:
+      /* This does the right thing for the case where we are not performing
+        word splitting. MATCH_STARSUB restricts it to ${* /foo/bar}, and
+        pos_params_pat_subst/string_list_pos_params will do the right thing
+        in turn for the case where ifs_is_null. Posix interp 888 */
+      if ((pflags & PF_NOSPLIT2) && (mflags & MATCH_STARSUB))
+        mflags |= MATCH_ASSIGNRHS;
       temp = pos_params_pat_subst (val, p, rep, mflags);
-      if (temp && (mflags & MATCH_QUOTED) == 0)
+      if (temp && quoted == 0 && ifs_is_null)
+       {
+         /* Posix interp 888 */
+       }
+      else if (temp && quoted == 0 && (pflags & PF_ASSIGNRHS))
+       {
+         /* Posix interp 888 */
+       }
+      else if (temp && (mflags & MATCH_QUOTED) == 0)
        {
          tt = quote_escapes (temp);
          free (temp);
@@ -7705,11 +9265,28 @@ parameter_brace_patsub (varname, value, ind, patsub, quoted, pflags, flags)
       break;
 #if defined (ARRAY_VARS)
     case VT_ARRAYVAR:
-      temp = assoc_p (v) ? assoc_patsub (assoc_cell (v), p, rep, mflags)
-                        : array_patsub (array_cell (v), p, rep, mflags);
-      /* Don't call quote_escapes anymore; array_patsub calls
-        array_quote_escapes as appropriate before adding the
-        space separators; ditto for assoc_patsub. */
+      /* If we are expanding in a context where word splitting will not be
+        performed, treat as quoted.  This changes how ${A[*]} will be
+        expanded to make it identical to $*. */
+      if ((mflags & MATCH_STARSUB) && (mflags & MATCH_ASSIGNRHS) && ifs_is_null)
+       mflags |= MATCH_QUOTED;         /* Posix interp 888 */
+
+      /* these eventually call string_list_pos_params */
+      if (assoc_p (v))
+       temp = assoc_patsub (assoc_cell (v), p, rep, mflags);
+      else
+       temp = array_patsub (array_cell (v), p, rep, mflags);
+
+      if (temp && quoted == 0 && ifs_is_null)
+       {
+         /* Posix interp 888 */
+       }
+      else if (temp && (mflags & MATCH_QUOTED) == 0)
+       {
+         tt = quote_escapes (temp);
+         free (temp);
+         temp = tt;
+       }
       break;
 #endif
     }
@@ -7718,6 +9295,8 @@ parameter_brace_patsub (varname, value, ind, patsub, quoted, pflags, flags)
   FREE (rep);
   free (lpatsub);
 
+  this_command_name = oname;
+
   return temp;
 }
 
@@ -7738,7 +9317,7 @@ pos_params_modcase (string, pat, modop, mflags)
   WORD_LIST *save, *params;
   WORD_DESC *w;
   char *ret;
-  int pchar, qflags;
+  int pchar, qflags, pflags;
 
   save = params = list_rest_of_args ();
   if (save == 0)
@@ -7755,8 +9334,14 @@ pos_params_modcase (string, pat, modop, mflags)
 
   pchar = (mflags & MATCH_STARSUB) == MATCH_STARSUB ? '*' : '@';
   qflags = (mflags & MATCH_QUOTED) == MATCH_QUOTED ? Q_DOUBLE_QUOTES : 0;
+  pflags = (mflags & MATCH_ASSIGNRHS) == MATCH_ASSIGNRHS ? PF_ASSIGNRHS : 0;
 
-  ret = string_list_pos_params (pchar, save, qflags);
+  /* If we are expanding in a context where word splitting will not be
+     performed, treat as quoted.  This changes how $* will be expanded. */
+  if (pchar == '*' && (mflags & MATCH_ASSIGNRHS) && ifs_is_null)
+    qflags |= Q_DOUBLE_QUOTES;         /* Posix interp 888 */
+
+  ret = string_list_pos_params (pchar, save, qflags, pflags);
   dispose_words (save);
 
   return (ret);
@@ -7767,24 +9352,29 @@ pos_params_modcase (string, pat, modop, mflags)
    to perform.  QUOTED is a flags word containing the type of quoting
    currently in effect. */
 static char *
-parameter_brace_casemod (varname, value, ind, modspec, patspec, quoted, flags)
+parameter_brace_casemod (varname, value, estatep, modspec, patspec, quoted, pflags, flags)
      char *varname, *value;
-     int ind, modspec;
+     array_eltstate_t *estatep;
+     int modspec;
      char *patspec;
-     int quoted, flags;
+     int quoted, pflags, flags;
 {
   int vtype, starsub, modop, mflags, x;
-  char *val, *temp, *pat, *p, *lpat, *tt;
+  char *val, *temp, *pat, *p, *lpat, *tt, *oname;
   SHELL_VAR *v;
 
   if (value == 0)
     return ((char *)NULL);
 
+  oname = this_command_name;
   this_command_name = varname;
 
-  vtype = get_var_and_type (varname, value, ind, quoted, flags, &v, &val);
+  vtype = get_var_and_type (varname, value, estatep, quoted, flags, &v, &val);
   if (vtype == -1)
-    return ((char *)NULL);
+    {
+      this_command_name = oname;
+      return ((char *)NULL);
+    }
 
   starsub = vtype & VT_STARSUB;
   vtype &= ~VT_STARSUB;
@@ -7795,6 +9385,8 @@ parameter_brace_casemod (varname, value, ind, modspec, patspec, quoted, flags)
     mflags |= MATCH_QUOTED;
   if (starsub)
     mflags |= MATCH_STARSUB;
+  if (pflags & PF_ASSIGNRHS)
+    mflags |= MATCH_ASSIGNRHS;
   
   p = patspec;
   if (modspec == '^')
@@ -7818,7 +9410,7 @@ parameter_brace_casemod (varname, value, ind, modspec, patspec, quoted, flags)
     
   lpat = p ? savestring (p) : 0;
   /* Perform the same expansions on the pattern as performed by the
-     pattern removal expansions.  FOR LATER */
+     pattern removal expansions. */
   pat = lpat ? getpattern (lpat, quoted, 1) : 0;
 
   /* OK, now we do the case modification. */
@@ -7839,7 +9431,11 @@ parameter_brace_casemod (varname, value, ind, modspec, patspec, quoted, flags)
 
     case VT_POSPARMS:
       temp = pos_params_modcase (val, pat, modop, mflags);
-      if (temp && (mflags & MATCH_QUOTED)  == 0)
+      if (temp && quoted == 0 && ifs_is_null)
+       {
+         /* Posix interp 888 */
+       }
+      else if (temp && (mflags & MATCH_QUOTED) == 0)
        {
          tt = quote_escapes (temp);
          free (temp);
@@ -7849,11 +9445,26 @@ parameter_brace_casemod (varname, value, ind, modspec, patspec, quoted, flags)
 
 #if defined (ARRAY_VARS)
     case VT_ARRAYVAR:
+      /* If we are expanding in a context where word splitting will not be
+        performed, treat as quoted.  This changes how ${A[*]} will be
+        expanded to make it identical to $*. */
+      if ((mflags & MATCH_STARSUB) && (mflags & MATCH_ASSIGNRHS) && ifs_is_null)
+       mflags |= MATCH_QUOTED;         /* Posix interp 888 */
+
       temp = assoc_p (v) ? assoc_modcase (assoc_cell (v), pat, modop, mflags)
                         : array_modcase (array_cell (v), pat, modop, mflags);
-      /* Don't call quote_escapes; array_modcase calls array_quote_escapes
-        as appropriate before adding the space separators; ditto for
-        assoc_modcase. */
+
+      if (temp && quoted == 0 && ifs_is_null)
+       {
+         /* Posix interp 888 */
+       }
+      else if (temp && (mflags & MATCH_QUOTED) == 0)
+       {
+         tt = quote_escapes (temp);
+         free (temp);
+         temp = tt;
+       }
+
       break;
 #endif
     }
@@ -7861,6 +9472,8 @@ parameter_brace_casemod (varname, value, ind, modspec, patspec, quoted, flags)
   FREE (pat);
   free (lpat);
 
+  this_command_name = oname;
+
   return temp;
 }
 
@@ -7928,17 +9541,18 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
      int *indexp, quoted, pflags, *quoted_dollar_atp, *contains_dollar_at;
 {
   int check_nullness, var_is_set, var_is_null, var_is_special;
-  int want_substring, want_indir, want_patsub, want_casemod;
+  int want_substring, want_indir, want_patsub, want_casemod, want_attributes;
   char *name, *value, *temp, *temp1;
   WORD_DESC *tdesc, *ret;
-  int t_index, sindex, c, tflag, modspec, all_element_arrayref;
+  int t_index, sindex, c, tflag, modspec, local_pflags, all_element_arrayref;
   intmax_t number;
-  arrayind_t ind;
+  array_eltstate_t es;
 
   temp = temp1 = value = (char *)NULL;
   var_is_set = var_is_null = var_is_special = check_nullness = 0;
-  want_substring = want_indir = want_patsub = want_casemod = 0;
+  want_substring = want_indir = want_patsub = want_casemod = want_attributes = 0;
 
+  local_pflags = 0;
   all_element_arrayref = 0;
 
   sindex = *indexp;
@@ -7949,12 +9563,12 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
   else
 #if defined (CASEMOD_EXPANSIONS)
     /* To enable case-toggling expansions using the `~' operator character
-       change the 1 to 0. */
-#  if defined (CASEMOD_CAPCASE)
+       define CASEMOD_TOGGLECASE in config-top.h */
+#  if defined (CASEMOD_TOGGLECASE)
     name = string_extract (string, &t_index, "#%^,~:-=?+/@}", SX_VARNAME);
 #  else
     name = string_extract (string, &t_index, "#%^,:-=?+/@}", SX_VARNAME);
-#  endif /* CASEMOD_CAPCASE */
+#  endif /* CASEMOD_TOGGLECASE */
 #else
     name = string_extract (string, &t_index, "#%:-=?+/@}", SX_VARNAME);
 #endif /* CASEMOD_EXPANSIONS */
@@ -7968,7 +9582,7 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
       name[1] = '\0';
       t_index++;
     }
-  else if (*name == '!' && t_index > sindex && string[t_index] == '@' && string[t_index+1] == '}')
+  else if (*name == '!' && t_index > sindex && string[t_index] == '@' && string[t_index+1] == RBRACE)
     {
       name = (char *)xrealloc (name, t_index - sindex + 2);
       name[t_index - sindex] = '@';
@@ -7979,7 +9593,10 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
   ret = 0;
   tflag = 0;
 
-  ind = INTMAX_MIN;
+#if defined (ARRAY_VARS)
+  init_eltstate (&es);
+#endif
+  es.ind = INTMAX_MIN; /* XXX */
 
   /* If the name really consists of a special variable, then make sure
      that we have the entire name.  We don't allow indirect references
@@ -8032,6 +9649,12 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
       want_casemod = 1;
     }
 #endif
+  else if (c == '@' && (string[sindex] == 'a' || string[sindex] == 'A') && string[sindex+1] == RBRACE)
+    {
+      /* special case because we do not want to shortcut foo as foo[0] here */
+      want_attributes = 1;
+      local_pflags |= PF_ALLINDS;
+    }
 
   /* Catch the valid and invalid brace expressions that made it through the
      tests above. */
@@ -8061,7 +9684,7 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
     (legal_variable_starter ((unsigned char)name[1]) || DIGIT (name[1])
                                        || VALID_INDIR_PARAM (name[1]));
 
-  /* Determine the value of this variable. */
+  /* Determine the value of this variable whose name is NAME. */
 
   /* Check for special variables, directly referenced. */
   if (SPECIAL_VAR (name, want_indir))
@@ -8084,7 +9707,7 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
       number = parameter_brace_expand_length (name);
       if (number == INTMAX_MIN && unbound_vars_is_error)
        {
-         last_command_exit_value = EXECUTION_FAILURE;
+         set_exit_status (EXECUTION_FAILURE);
          err_unboundvar (name+1);
          free (name);
          return (interactive_shell ? &expand_wdesc_error : &expand_wdesc_fatal);
@@ -8128,7 +9751,7 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
       x = all_variables_matching_prefix (temp1);
       xlist = strvec_to_word_list (x, 0, 0);
       if (string[sindex - 2] == '*')
-       temp = string_list_dollar_star (xlist);
+       temp = string_list_dollar_star (xlist, quoted, 0);
       else
        {
          temp = string_list_dollar_at (xlist, quoted, 0);
@@ -8153,18 +9776,18 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
     }
 
 #if defined (ARRAY_VARS)      
-  /* Process ${!ARRAY[@]} and ${!ARRAY[*]} expansion. */ /* [ */
+  /* Process ${!ARRAY[@]} and ${!ARRAY[*]} expansion. */
   if (want_indir && string[sindex - 1] == RBRACE &&
-      string[sindex - 2] == ']' && valid_array_reference (name+1, 0))
+      string[sindex - 2] == RBRACK && valid_array_reference (name+1, 0))
     {
       char *x, *x1;
 
       temp1 = savestring (name + 1);
-      x = array_variable_name (temp1, &x1, (int *)0);  /* [ */
+      x = array_variable_name (temp1, 0, &x1, (int *)0);
       FREE (x);
-      if (ALL_ELEMENT_SUB (x1[0]) && x1[1] == ']')
+      if (ALL_ELEMENT_SUB (x1[0]) && x1[1] == RBRACK)
        {
-         temp = array_keys (temp1, quoted);    /* handles assoc vars too */
+         temp = array_keys (temp1, quoted, pflags);    /* handles assoc vars too */
          if (x1[0] == '@')
            {
              if ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) && quoted_dollar_atp)
@@ -8199,19 +9822,34 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
 
   if (want_indir)
     {
-      tdesc = parameter_brace_expand_indir (name + 1, var_is_special, quoted, quoted_dollar_atp, contains_dollar_at);
+      tdesc = parameter_brace_expand_indir (name + 1, var_is_special, quoted, pflags|local_pflags, quoted_dollar_atp, contains_dollar_at);
       if (tdesc == &expand_wdesc_error || tdesc == &expand_wdesc_fatal)
        {
          temp = (char *)NULL;
          goto bad_substitution;
        }
+
       /* Turn off the W_ARRAYIND flag because there is no way for this function
         to return the index we're supposed to be using. */
       if (tdesc && tdesc->flags)
        tdesc->flags &= ~W_ARRAYIND;
+
+      /* If the indir expansion contains $@/$*, extend the special treatment
+        of the case of no positional parameters and `set -u' to it. */
+      if (contains_dollar_at && *contains_dollar_at)
+       all_element_arrayref = 1;
     }
   else
-    tdesc = parameter_brace_expand_word (name, var_is_special, quoted, PF_IGNUNBOUND|(pflags&(PF_NOSPLIT2|PF_ASSIGNRHS)), &ind);
+    {
+      local_pflags |= PF_IGNUNBOUND|(pflags&(PF_NOSPLIT2|PF_ASSIGNRHS));
+      tdesc = parameter_brace_expand_word (name, var_is_special, quoted, local_pflags, &es);
+    }
+
+  if (tdesc == &expand_wdesc_error || tdesc == &expand_wdesc_fatal)
+    {
+      tflag = 0;
+      tdesc = 0;
+    }
 
   if (tdesc)
     {
@@ -8220,7 +9858,7 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
       dispose_word_desc (tdesc);
     }
   else
-    temp = (char  *)0;
+    temp = (char *)0;
 
   if (temp == &expand_param_error || temp == &expand_param_fatal)
     {
@@ -8238,13 +9876,18 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
       qflags = quoted;
       /* If in a context where word splitting will not take place, treat as
         if double-quoted.  Has effects with $* and ${array[*]} */
+
       if (pflags & PF_ASSIGNRHS)
        qflags |= Q_DOUBLE_QUOTES;
-      chk_atstar (name, qflags, quoted_dollar_atp, contains_dollar_at);
       /* We duplicate a little code here */
-      t = mbschr (name, '[');
-      if (t && ALL_ELEMENT_SUB (t[1]) && t[2] == ']')
-        all_element_arrayref = 1;
+      t = mbschr (name, LBRACK);
+      if (t && ALL_ELEMENT_SUB (t[1]) && t[2] == RBRACK)
+       {
+         all_element_arrayref = 1;
+         if (expand_no_split_dollar_star && t[1] == '*')       /* XXX */
+           qflags |= Q_DOUBLE_QUOTES;
+       }
+      chk_atstar (name, qflags, pflags, quoted_dollar_atp, contains_dollar_at);
     }
 #endif
 
@@ -8253,6 +9896,14 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
   /* XXX - this may not need to be restricted to special variables */
   if (check_nullness)
     var_is_null |= var_is_set && var_is_special && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) && QUOTED_NULL (temp);
+#if defined (ARRAY_VARS)
+  if (check_nullness)
+    var_is_null |= var_is_set && 
+                  (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) &&
+                  QUOTED_NULL (temp) &&
+                  valid_array_reference (name, 0) &&
+                  chk_atstar (name, 0, 0, (int *)0, (int *)0);
+#endif
 
   /* Get the rest of the stuff inside the braces. */
   if (c && c != RBRACE)
@@ -8272,11 +9923,11 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
 
   /* All the cases where an expansion can possibly generate an unbound
      variable error. */
-  if (want_substring || want_patsub || want_casemod || c == '#' || c == '%' || c == RBRACE)
+  if (want_substring || want_patsub || want_casemod || c == '@' || c == '#' || c == '%' || c == RBRACE)
     {
       if (var_is_set == 0 && unbound_vars_is_error && ((name[0] != '@' && name[0] != '*') || name[1]) && all_element_arrayref == 0)
        {
-         last_command_exit_value = EXECUTION_FAILURE;
+         set_exit_status (EXECUTION_FAILURE);
          err_unboundvar (name);
          FREE (value);
          FREE (temp);
@@ -8288,15 +9939,18 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
   /* If this is a substring spec, process it and add the result. */
   if (want_substring)
     {
-      temp1 = parameter_brace_substring (name, temp, ind, value, quoted, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
-      FREE (name);
+      temp1 = parameter_brace_substring (name, temp, &es, value, quoted, pflags, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
       FREE (value);
       FREE (temp);
+#if defined (ARRAY_VARS)
+      flush_eltstate (&es);
+#endif
 
-      if (temp1 == &expand_param_error)
-       return (&expand_wdesc_error);
-      else if (temp1 == &expand_param_fatal)
-       return (&expand_wdesc_fatal);
+      if (temp1 == &expand_param_error || temp1 == &expand_param_fatal)
+        {
+          FREE (name);
+         return (temp1 == &expand_param_error ? &expand_wdesc_error : &expand_wdesc_fatal);
+        }
 
       ret = alloc_word_desc ();
       ret->word = temp1;
@@ -8304,23 +9958,34 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
         "$@" to take a different code path. In fact, we make sure at the end
         of expand_word_internal that we're only looking at these flags if
         quoted_dollar_at == 0. */
-      if (temp1 && 
+      if (temp1 &&
           (quoted_dollar_atp == 0 || *quoted_dollar_atp == 0) &&
          QUOTED_NULL (temp1) && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
        ret->flags |= W_QUOTED|W_HASQUOTEDNULL;
+      else if (temp1 && (name[0] == '*' && name[1] == 0) && quoted == 0 &&
+               (pflags & PF_ASSIGNRHS))
+       ret->flags |= W_SPLITSPACE;     /* Posix interp 888 */  
+      /* Special handling for $* when unquoted and $IFS is null. Posix interp 888 */
+      else if (temp1 && (name[0] == '*' && name[1] == 0) && quoted == 0 && ifs_is_null)
+       ret->flags |= W_SPLITSPACE;     /* Posix interp 888 */
+
+      FREE (name);
       return ret;
     }
   else if (want_patsub)
     {
-      temp1 = parameter_brace_patsub (name, temp, ind, value, quoted, pflags, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
-      FREE (name);
+      temp1 = parameter_brace_patsub (name, temp, &es, value, quoted, pflags, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
       FREE (value);
       FREE (temp);
+#if defined (ARRAY_VARS)
+      flush_eltstate (&es);
+#endif
 
-      if (temp1 == &expand_param_error)
-       return (&expand_wdesc_error);
-      else if (temp1 == &expand_param_fatal)
-       return (&expand_wdesc_fatal);
+      if (temp1 == &expand_param_error || temp1 == &expand_param_fatal)
+        {
+          FREE (name);
+         return (temp1 == &expand_param_error ? &expand_wdesc_error : &expand_wdesc_fatal);
+        }
 
       ret = alloc_word_desc ();
       ret->word = temp1;
@@ -8328,20 +9993,28 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
           (quoted_dollar_atp == 0 || *quoted_dollar_atp == 0) &&
          QUOTED_NULL (temp1) && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
        ret->flags |= W_QUOTED|W_HASQUOTEDNULL;
+      /* Special handling for $* when unquoted and $IFS is null. Posix interp 888 */
+      else if (temp1 && (name[0] == '*' && name[1] == 0) && quoted == 0 && ifs_is_null)
+       ret->flags |= W_SPLITSPACE;     /* Posix interp 888 */
+
+      FREE (name);
       return ret;
     }
 #if defined (CASEMOD_EXPANSIONS)
   else if (want_casemod)
     {
-      temp1 = parameter_brace_casemod (name, temp, ind, modspec, value, quoted, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
-      FREE (name);
+      temp1 = parameter_brace_casemod (name, temp, &es, modspec, value, quoted, pflags, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
       FREE (value);
       FREE (temp);
+#if defined (ARRAY_VARS)
+      flush_eltstate (&es);
+#endif
 
-      if (temp1 == &expand_param_error)
-       return (&expand_wdesc_error);
-      else if (temp1 == &expand_param_fatal)
-       return (&expand_wdesc_fatal);
+      if (temp1 == &expand_param_error || temp1 == &expand_param_fatal)
+        {
+          FREE (name);
+         return (temp1 == &expand_param_error ? &expand_wdesc_error : &expand_wdesc_fatal);
+        }
 
       ret = alloc_word_desc ();
       ret->word = temp1;
@@ -8349,6 +10022,11 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
           (quoted_dollar_atp == 0 || *quoted_dollar_atp == 0) &&
          QUOTED_NULL (temp1) && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
        ret->flags |= W_QUOTED|W_HASQUOTEDNULL;
+      /* Special handling for $* when unquoted and $IFS is null. Posix interp 888 */
+      else if (temp1 && (name[0] == '*' && name[1] == 0) && quoted == 0 && ifs_is_null)
+       ret->flags |= W_SPLITSPACE;     /* Posix interp 888 */
+
+      FREE (name);
       return ret;
     }
 #endif
@@ -8359,11 +10037,14 @@ parameter_brace_expand (string, indexp, quoted, pflags, quoted_dollar_atp, conta
     default:
     case '\0':
 bad_substitution:
-      last_command_exit_value = EXECUTION_FAILURE;
+      set_exit_status (EXECUTION_FAILURE);
       report_error (_("%s: bad substitution"), string ? string : "??");
       FREE (value);
       FREE (temp);
       free (name);
+#if defined (ARRAY_VARS)
+      flush_eltstate (&es);
+#endif
       if (shell_compatibility_level <= 43)
        return &expand_wdesc_error;
       else
@@ -8373,13 +10054,17 @@ bad_substitution:
       break;
 
     case '@':
-      temp1 = parameter_brace_transform (name, temp, ind, value, c, quoted, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
+      temp1 = parameter_brace_transform (name, temp, &es, value, c, quoted, pflags, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
       free (temp);
       free (value);
-      free (name);
+#if defined (ARRAY_VARS)
+      flush_eltstate (&es);
+#endif
+
       if (temp1 == &expand_param_error || temp1 == &expand_param_fatal)
        {
-         last_command_exit_value = EXECUTION_FAILURE;
+         free (name);
+         set_exit_status (EXECUTION_FAILURE);
          report_error (_("%s: bad substitution"), string ? string : "??");
          return (temp1 == &expand_param_error ? &expand_wdesc_error : &expand_wdesc_fatal);
        }
@@ -8388,6 +10073,11 @@ bad_substitution:
       ret->word = temp1;
       if (temp1 && QUOTED_NULL (temp1) && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
        ret->flags |= W_QUOTED|W_HASQUOTEDNULL;
+      /* Special handling for $* when unquoted and $IFS is null. Posix interp 888 */
+      else if (temp1 && (name[0] == '*' && name[1] == 0) && quoted == 0 && ifs_is_null)
+       ret->flags |= W_SPLITSPACE;     /* Posix interp 888 */
+
+      free (name);
       return ret;
 
     case '#':  /* ${param#[#]pattern} */
@@ -8397,15 +10087,22 @@ bad_substitution:
          FREE (value);
          break;
        }
-      temp1 = parameter_brace_remove_pattern (name, temp, ind, value, c, quoted, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
+      temp1 = parameter_brace_remove_pattern (name, temp, &es, value, c, quoted, (tflag & W_ARRAYIND) ? AV_USEIND : 0);
       free (temp);
       free (value);
-      free (name);
+#if defined (ARRAY_VARS)
+      flush_eltstate (&es);
+#endif
 
       ret = alloc_word_desc ();
       ret->word = temp1;
       if (temp1 && QUOTED_NULL (temp1) && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
        ret->flags |= W_QUOTED|W_HASQUOTEDNULL;
+      /* Special handling for $* when unquoted and $IFS is null. Posix interp 888 */
+      else if (temp1 && (name[0] == '*' && name[1] == 0) && quoted == 0 && ifs_is_null)
+       ret->flags |= W_SPLITSPACE;     /* Posix interp 888 */
+
+      free (name);
       return ret;
 
     case '-':
@@ -8454,19 +10151,33 @@ bad_substitution:
        }
       else     /* VAR not set or VAR is NULL. */
        {
+         /* If we're freeing a quoted null here, we need to remember we saw
+            it so we can restore it later if needed, or the caller can note it.
+            The check against `+' doesn't really matter, since the other cases
+            don't use or return TFLAG, but it's good for clarity. */
+         if (c == '+' && temp && QUOTED_NULL (temp) &&
+             (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)))
+           tflag |= W_HASQUOTEDNULL;
+
          FREE (temp);
          temp = (char *)NULL;
          if (c == '=' && var_is_special)
            {
-             last_command_exit_value = EXECUTION_FAILURE;
+             set_exit_status (EXECUTION_FAILURE);
              report_error (_("$%s: cannot assign in this way"), name);
              free (name);
              free (value);
+#if defined (ARRAY_VARS)
+             flush_eltstate (&es);
+#endif
              return &expand_wdesc_error;
            }
          else if (c == '?')
            {
-             parameter_brace_expand_error (name, value);
+             parameter_brace_expand_error (name, value, check_nullness);
+#if defined (ARRAY_VARS)
+             flush_eltstate (&es);
+#endif
              return (interactive_shell ? &expand_wdesc_error : &expand_wdesc_fatal);
            }
          else if (c != '+')
@@ -8496,6 +10207,9 @@ bad_substitution:
       break;
     }
   free (name);
+#if defined (ARRAY_VARS)
+  flush_eltstate (&es);
+#endif
 
   if (ret == 0)
     {
@@ -8518,13 +10232,13 @@ param_expand (string, sindex, quoted, expanded_something,
      int *quoted_dollar_at_p, *had_quoted_null_p, pflags;
 {
   char *temp, *temp1, uerror[3], *savecmd;
-  int zindex, t_index, expok;
+  int zindex, t_index, expok, eflag;
   unsigned char c;
   intmax_t number;
   SHELL_VAR *var;
-  WORD_LIST *list;
+  WORD_LIST *list, *l;
   WORD_DESC *tdesc, *ret;
-  int tflag;
+  int tflag, nullarg;
 
 /*itrace("param_expand: `%s' pflags = %d", string+*sindex, pflags);*/
   zindex = *sindex;
@@ -8549,12 +10263,13 @@ param_expand (string, sindex, quoted, expanded_something,
     case '8':
     case '9':
       temp1 = dollar_vars[TODIGIT (c)];
+      /* This doesn't get called when (pflags&PF_IGNUNBOUND) != 0 */
       if (unbound_vars_is_error && temp1 == (char *)NULL)
        {
          uerror[0] = '$';
          uerror[1] = c;
          uerror[2] = '\0';
-         last_command_exit_value = EXECUTION_FAILURE;
+         set_exit_status (EXECUTION_FAILURE);
          err_unboundvar (uerror);
          return (interactive_shell ? &expand_wdesc_error : &expand_wdesc_fatal);
        }
@@ -8597,12 +10312,12 @@ param_expand (string, sindex, quoted, expanded_something,
          if (expanded_something)
            *expanded_something = 0;
          temp = (char *)NULL;
-         if (unbound_vars_is_error)
+         if (unbound_vars_is_error && (pflags & PF_IGNUNBOUND) == 0)
            {
              uerror[0] = '$';
              uerror[1] = c;
              uerror[2] = '\0';
-             last_command_exit_value = EXECUTION_FAILURE;
+             set_exit_status (EXECUTION_FAILURE);
              err_unboundvar (uerror);
              return (interactive_shell ? &expand_wdesc_error : &expand_wdesc_fatal);
            }
@@ -8629,7 +10344,7 @@ param_expand (string, sindex, quoted, expanded_something,
          uerror[0] = '$';
          uerror[1] = '*';
          uerror[2] = '\0';
-         last_command_exit_value = EXECUTION_FAILURE;
+         set_exit_status (EXECUTION_FAILURE);
          err_unboundvar (uerror);
          return (interactive_shell ? &expand_wdesc_error : &expand_wdesc_fatal);
        }
@@ -8647,7 +10362,7 @@ param_expand (string, sindex, quoted, expanded_something,
             quote the whole string, including the separators.  If IFS
             is unset, the parameters are separated by ' '; if $IFS is
             null, the parameters are concatenated. */
-         temp = (quoted & (Q_DOUBLE_QUOTES|Q_PATQUOTE)) ? string_list_dollar_star (list) : string_list (list);
+         temp = (quoted & (Q_DOUBLE_QUOTES|Q_PATQUOTE)) ? string_list_dollar_star (list, quoted, 0) : string_list (list);
          if (temp)
            {
              temp1 = (quoted & Q_DOUBLE_QUOTES) ? quote_string (temp) : temp;
@@ -8665,16 +10380,53 @@ param_expand (string, sindex, quoted, expanded_something,
             an assignment statement.  In that case, we don't separate the
             arguments at all.  Otherwise, if the $* is not quoted it is
             identical to $@ */
+         if (expand_no_split_dollar_star && quoted == 0 && ifs_is_set == 0 && (pflags & PF_ASSIGNRHS))
+           {
+             /* Posix interp 888: RHS of assignment, IFS unset: no splitting,
+                separate with space */
+             temp1 = string_list_dollar_star (list, quoted, pflags);
+             temp = temp1 ? quote_string (temp1) : temp1;
+             /* XXX - tentative - note that we saw a quoted null here */
+             if (temp1 && *temp1 == 0 && QUOTED_NULL (temp))
+               tflag |= W_SAWQUOTEDNULL;
+             FREE (temp1);
+           }
+         else if (expand_no_split_dollar_star && quoted == 0 && ifs_is_null && (pflags & PF_ASSIGNRHS))
+           {
+             /* Posix interp 888: RHS of assignment, IFS set to '' */
+             temp1 = string_list_dollar_star (list, quoted, pflags);
+             temp = temp1 ? quote_escapes (temp1) : temp1;
+             FREE (temp1);
+           }
+         else if (expand_no_split_dollar_star && quoted == 0 && ifs_is_set && ifs_is_null == 0 && (pflags & PF_ASSIGNRHS))
+           {
+             /* Posix interp 888: RHS of assignment, IFS set to non-null value */
+             temp1 = string_list_dollar_star (list, quoted, pflags);
+             temp = temp1 ? quote_string (temp1) : temp1;
+
+             /* XXX - tentative - note that we saw a quoted null here */
+             if (temp1 && *temp1 == 0 && QUOTED_NULL (temp))
+               tflag |= W_SAWQUOTEDNULL;
+             FREE (temp1);
+           }
+         /* XXX - should we check ifs_is_set here as well? */
 #  if defined (HANDLE_MULTIBYTE)
-         if (expand_no_split_dollar_star && ifs_firstc[0] == 0)
+         else if (expand_no_split_dollar_star && ifs_firstc[0] == 0)
 #  else
-         if (expand_no_split_dollar_star && ifs_firstc == 0)
+         else if (expand_no_split_dollar_star && ifs_firstc == 0)
 #  endif
-           temp = string_list_dollar_star (list);
+           /* Posix interp 888: not RHS, no splitting, IFS set to '' */
+           temp = string_list_dollar_star (list, quoted, 0);
          else
            {
              temp = string_list_dollar_at (list, quoted, 0);
+             /* Set W_SPLITSPACE to make sure the individual positional
+                parameters are split into separate arguments */
+#if 0
              if (quoted == 0 && (ifs_is_set == 0 || ifs_is_null))
+#else  /* change with bash-5.0 */
+             if (quoted == 0 && ifs_is_null)
+#endif
                tflag |= W_SPLITSPACE;
              /* If we're not quoted but we still don't want word splitting, make
                 we quote the IFS characters to protect them from splitting (e.g.,
@@ -8715,12 +10467,18 @@ param_expand (string, sindex, quoted, expanded_something,
          uerror[0] = '$';
          uerror[1] = '@';
          uerror[2] = '\0';
-         last_command_exit_value = EXECUTION_FAILURE;
+         set_exit_status (EXECUTION_FAILURE);
          err_unboundvar (uerror);
          return (interactive_shell ? &expand_wdesc_error : &expand_wdesc_fatal);
        }
 #endif
 
+      for (nullarg = 0, l = list; l; l = l->next)
+       {
+         if (l->word && (l->word->word == 0 || l->word->word[0] == 0))
+           nullarg = 1;
+       }
+
       /* We want to flag the fact that we saw this.  We can't turn
         off quoting entirely, because other characters in the
         string might need it (consider "\"$@\""), but we need some
@@ -8739,10 +10497,38 @@ param_expand (string, sindex, quoted, expanded_something,
         parameters no matter what IFS is set to. */
       /* XXX - what to do when in a context where word splitting is not
         performed? Even when IFS is not the default, posix seems to imply
-        that we behave like unquoted $* ?  Maybe we should use PF_NOSPLIT2
-        here. */
-      /* XXX - bash-4.4/bash-5.0 passing PFLAGS */
-      temp = string_list_dollar_at (list, (pflags & PF_ASSIGNRHS) ? (quoted|Q_DOUBLE_QUOTES) : quoted, pflags);
+        that we have to expand $@ to all the positional parameters and
+        separate them with spaces, which are preserved because word splitting
+        doesn't take place.  See below for how we use PF_NOSPLIT2 here. */
+
+      /* These are the cases where word splitting will not be performed. */
+      if (pflags & PF_ASSIGNRHS)
+       {
+         temp = string_list_dollar_at (list, (quoted|Q_DOUBLE_QUOTES), pflags);
+         if (nullarg)
+           tflag |= W_HASQUOTEDNULL;   /* we know quoting produces quoted nulls */
+       }
+
+      /* This needs to match what expand_word_internal does with non-quoted $@
+        does with separating with spaces.  Passing Q_DOUBLE_QUOTES means that
+        the characters in LIST will be quoted, and PF_ASSIGNRHS ensures that
+        they will separated by spaces. After doing this, we need the special
+        handling for PF_NOSPLIT2 in expand_word_internal to remove the CTLESC
+        quotes. */
+      else if (pflags & PF_NOSPLIT2)
+        {
+#if defined (HANDLE_MULTIBYTE)
+         if (quoted == 0 && ifs_is_set && ifs_is_null == 0 && ifs_firstc[0] != ' ')
+#else
+         if (quoted == 0 && ifs_is_set && ifs_is_null == 0 && ifs_firstc != ' ')
+#endif
+           /* Posix interp 888 */
+           temp = string_list_dollar_at (list, Q_DOUBLE_QUOTES, pflags);
+         else
+           temp = string_list_dollar_at (list, quoted, pflags);
+       }
+      else
+       temp = string_list_dollar_at (list, quoted, pflags);
 
       tflag |= W_DOLLARAT;
       dispose_words (list);
@@ -8824,7 +10610,9 @@ arithsub:
          /* No error messages. */
          savecmd = this_command_name;
          this_command_name = (char *)NULL;
-         number = evalexp (temp1, &expok);
+
+         eflag = (shell_compatibility_level > 51) ? 0 : EXP_EXPANDED;
+         number = evalexp (temp1, eflag, &expok);
          this_command_name = savecmd;
          free (temp);
          free (temp1);
@@ -8832,7 +10620,7 @@ arithsub:
            {
              if (interactive_shell == 0 && posixly_correct)
                {
-                 last_command_exit_value = EXECUTION_FAILURE;
+                 set_exit_status (EXECUTION_FAILURE);
                  return (&expand_wdesc_fatal);
                }
              else
@@ -8848,7 +10636,7 @@ comsub:
        temp1 = substring (string, *sindex, zindex+1);
       else
        {
-         tdesc = command_substitute (temp, quoted);
+         tdesc = command_substitute (temp, quoted, pflags&PF_ASSIGNRHS);
          temp1 = tdesc ? tdesc->word : (char *)NULL;
          if (tdesc)
            dispose_word_desc (tdesc);
@@ -8859,7 +10647,7 @@ comsub:
 
     /* Do POSIX.2d9-style arithmetic substitution.  This will probably go
        away in a future bash release. */
-    case '[':
+    case '[':          /*]*/
       /* Extract the contents of this arithmetic substitution. */
       t_index = zindex + 1;
       temp = extract_arithmetic_subst (string, &t_index);
@@ -8921,7 +10709,8 @@ comsub:
 
              temp = (*temp && (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)))
                        ? quote_string (temp)
-                       : quote_escapes (temp);
+                       : ((pflags & PF_ASSIGNRHS) ? quote_rhs (temp)
+                                                  : quote_escapes (temp));
            }
 
          free (temp1);
@@ -8936,7 +10725,8 @@ comsub:
 #if defined (ARRAY_VARS)
          if (temp && *temp && valid_array_reference (temp, 0))
            {
-             tdesc = parameter_brace_expand_word (temp, SPECIAL_VAR (temp, 0), quoted, pflags, (arrayind_t *)NULL);
+             chk_atstar (temp, quoted, pflags, quoted_dollar_at_p, contains_dollar_at);
+             tdesc = parameter_brace_expand_word (temp, SPECIAL_VAR (temp, 0), quoted, pflags, 0);
              if (tdesc == &expand_wdesc_error || tdesc == &expand_wdesc_fatal)
                return (tdesc);
              ret = tdesc;
@@ -8947,7 +10737,7 @@ comsub:
          /* y=2 ; typeset -n x=y; echo $x is not the same as echo $2 in ksh */
          if (temp && *temp && legal_identifier (temp) == 0)
            {
-             last_command_exit_value = EXECUTION_FAILURE;
+             set_exit_status (EXECUTION_FAILURE);
              report_error (_("%s: invalid variable name for name reference"), temp);
              return (&expand_wdesc_error);     /* XXX */
            }
@@ -8960,7 +10750,7 @@ comsub:
 unbound_variable:
       if (unbound_vars_is_error)
        {
-         last_command_exit_value = EXECUTION_FAILURE;
+         set_exit_status (EXECUTION_FAILURE);
          err_unboundvar (temp1);
        }
       else
@@ -8970,7 +10760,7 @@ unbound_variable:
        }
 
       free (temp1);
-      last_command_exit_value = EXECUTION_FAILURE;
+      set_exit_status (EXECUTION_FAILURE);
       return ((unbound_vars_is_error && interactive_shell == 0)
                ? &expand_wdesc_fatal
                : &expand_wdesc_error);
@@ -8991,6 +10781,120 @@ return0:
   return ret;
 }
 
+#if defined (ARRAY_VARS)
+/* Characters that need to be backslash-quoted after expanding array subscripts */
+static char abstab[256] = { '\1' };
+
+/* Run an array subscript through the appropriate word expansions. */
+char *
+expand_subscript_string (string, quoted)
+     char *string;
+     int quoted;
+{
+  WORD_DESC td;
+  WORD_LIST *tlist;
+  int oe;
+  char *ret;
+
+  if (string == 0 || *string == 0)
+    return (char *)NULL;
+
+  oe = expand_no_split_dollar_star;
+  ret = (char *)NULL;
+
+#if 0
+  td.flags = W_NOPROCSUB|W_NOTILDE|W_NOSPLIT2; /* XXX - W_NOCOMSUB? */
+#else
+  td.flags = W_NOPROCSUB|W_NOSPLIT2;   /* XXX - W_NOCOMSUB? */
+#endif
+  td.word = savestring (string);               /* in case it's freed on error */
+
+  expand_no_split_dollar_star = 1;
+  tlist = call_expand_word_internal (&td, quoted, 0, (int *)NULL, (int *)NULL);
+  expand_no_split_dollar_star = oe;
+
+  if (tlist)
+    {
+      if (tlist->word)
+       {
+         remove_quoted_nulls (tlist->word->word);
+         tlist->word->flags &= ~W_HASQUOTEDNULL;
+       }
+      dequote_list (tlist);
+      ret = string_list (tlist);
+      dispose_words (tlist);
+    }
+
+  free (td.word);
+  return (ret);
+}
+
+/* Expand the subscript in STRING, which is an array reference. To ensure we
+   only expand it once, we quote the characters that would start another
+   expansion and the bracket characters that are special to array subscripts. */
+static char *
+expand_array_subscript (string, sindex, quoted, flags)
+     char *string;
+     int *sindex;
+     int quoted, flags;
+{
+  char *ret, *exp, *t;
+  size_t slen;
+  int si, ni;
+
+  si = *sindex;
+  slen = STRLEN (string);
+
+  if (abstab[0] == '\1')
+    {
+      /* These are basically the characters that start shell expansions plus
+        the characters that delimit subscripts. */
+      memset (abstab, '\0', sizeof (abstab));
+      abstab[LBRACK] = abstab[RBRACK] = 1;
+      abstab['$'] = abstab['`'] = abstab['~'] = 1;
+      abstab['\\'] = abstab['\''] = 1;
+      abstab['"'] = 1; /* XXX */
+      /* We don't quote `@' or `*' in the subscript at all. */
+    }
+
+  /* string[si] == LBRACK */
+  ni = skipsubscript (string, si, 0);
+  /* These checks mirror the ones in valid_array_reference. The check for
+     (ni - si) == 1 checks for empty subscripts. We don't check that the
+     subscript is a separate word if we're parsing an arithmetic expression. */
+  if (ni >= slen || string[ni] != RBRACK || (ni - si) == 1 ||
+      (string[ni+1] != '\0' && (quoted & Q_ARITH) == 0))
+    {
+      /* let's check and see what fails this check */
+      INTERNAL_DEBUG (("expand_array_subscript: bad subscript string: `%s'", string+si));
+      ret = (char *)xmalloc (2);       /* badly-formed subscript */
+      ret[0] = string[si];
+      ret[1] = '\0';
+      *sindex = si + 1;
+      return ret;
+    }
+
+  /* STRING[ni] == RBRACK */
+  exp = substring (string, si+1, ni);
+  t = expand_subscript_string (exp, quoted & ~(Q_ARITH|Q_DOUBLE_QUOTES));
+  free (exp);
+  exp = t ? sh_backslash_quote (t, abstab, 0) : savestring ("");
+  free (t);
+
+  slen = STRLEN (exp);
+  ret = xmalloc (slen + 2 + 1);
+  ret[0] ='[';
+  strcpy (ret + 1, exp);
+  ret[slen + 1] = ']';
+  ret[slen + 2] = '\0';
+
+  free (exp);
+  *sindex = ni + 1;
+
+  return ret;
+}
+#endif
+
 void
 invalidate_cached_quoted_dollar_at ()
 {
@@ -9041,7 +10945,7 @@ expand_word_internal (word, quoted, isexp, contains_dollar_at, expanded_somethin
   size_t istring_size;
 
   /* Index into ISTRING. */
-  int istring_index;
+  size_t istring_index;
 
   /* Temporary string storage. */
   char *temp, *temp1;
@@ -9065,8 +10969,11 @@ expand_word_internal (word, quoted, isexp, contains_dollar_at, expanded_somethin
 
   /* State flags */
   int had_quoted_null;
+  int has_quoted_ifs;          /* did we add a quoted $IFS character here? */
   int has_dollar_at, temp_has_dollar_at;
+  int internal_tilde;
   int split_on_spaces;
+  int local_expanded;
   int tflag;
   int pflags;                  /* flags passed to param_expand */
   int mb_cur_max;
@@ -9080,7 +10987,9 @@ expand_word_internal (word, quoted, isexp, contains_dollar_at, expanded_somethin
 
   DECLARE_MBSTATE;
 
-  /* OK, let's see if we can optimize a common idiom: "$@" */
+  /* OK, let's see if we can optimize a common idiom: "$@". This needs to make sure
+     that all of the flags callers care about (e.g., W_HASQUOTEDNULL) are set in
+     list->flags. */
   if (STREQ (word->word, "\"$@\"") &&
       (word->flags == (W_HASDOLLAR|W_QUOTED)) &&
       dollar_vars[1])          /* XXX - check IFS here as well? */
@@ -9100,7 +11009,9 @@ expand_word_internal (word, quoted, isexp, contains_dollar_at, expanded_somethin
   istring = (char *)xmalloc (istring_size = DEFAULT_INITIAL_ARRAY_SIZE);
   istring[istring_index = 0] = '\0';
   quoted_dollar_at = had_quoted_null = has_dollar_at = 0;
+  has_quoted_ifs = 0;
   split_on_spaces = 0;
+  internal_tilde = 0;          /* expanding =~ or :~ */
   quoted_state = UNQUOTED;
 
   string = word->word;
@@ -9109,7 +11020,7 @@ expand_word_internal (word, quoted, isexp, contains_dollar_at, expanded_somethin
   mb_cur_max = MB_CUR_MAX;
 
   /* Don't need the string length for the SADD... and COPY_ macros unless
-     multibyte characters are possible. */
+     multibyte characters are possible, but do need it for bounds checking. */
   string_size = (mb_cur_max > 1) ? strlen (string) : 1;
 
   if (contains_dollar_at)
@@ -9163,10 +11074,9 @@ add_string:
        case '<':
        case '>':
          {
-           /* bash-4.4/bash-5.0
-              XXX - technically this should only be expanded at the start
+              /* XXX - technically this should only be expanded at the start
               of a word */
-           if (string[++sindex] != LPAREN || (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) || (word->flags & (W_DQUOTE|W_NOPROCSUB)) || posixly_correct)
+           if (string[++sindex] != LPAREN || (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) || (word->flags & W_NOPROCSUB))
              {
                sindex--;       /* add_character: label increments sindex */
                goto add_character;
@@ -9189,6 +11099,22 @@ add_string:
          }
 #endif /* PROCESS_SUBSTITUTION */
 
+#if defined (ARRAY_VARS)
+       case '[':               /*]*/
+         if ((quoted & Q_ARITH) == 0 || shell_compatibility_level <= 51)
+           {
+             if (isexp == 0 && (word->flags & (W_NOSPLIT|W_NOSPLIT2)) == 0 && isifs (c) && (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) == 0)
+               goto add_ifs_character;
+             else
+               goto add_character;
+           }
+         else
+           {
+             temp = expand_array_subscript (string, &sindex, quoted, word->flags);
+             goto add_string;
+           }
+#endif
+
        case '=':
          /* Posix.2 section 3.6.1 says that tildes following `=' in words
             which are not assignment statements are not expanded.  If the
@@ -9208,32 +11134,30 @@ add_string:
                goto add_character;
            }
          /* If we're not in posix mode or forcing assignment-statement tilde
-            expansion, note where the `=' appears in the word and prepare to
-            do tilde expansion following the first `='. */
+            expansion, note where the first `=' appears in the word and prepare
+            to do tilde expansion following the first `='. We have to keep
+            track of the first `=' (using assignoff) to avoid being confused
+            by an `=' in the rhs of the assignment statement. */
          if ((word->flags & W_ASSIGNMENT) &&
              (posixly_correct == 0 || (word->flags & W_TILDEEXP)) &&
              assignoff == -1 && sindex > 0)
            assignoff = sindex;
          if (sindex == assignoff && string[sindex+1] == '~')   /* XXX */
-           word->flags |= W_ITILDE;
-#if 0
-         else if ((word->flags & W_ASSIGNMENT) &&
-                  (posixly_correct == 0 || (word->flags & W_TILDEEXP)) &&
-                  string[sindex+1] == '~')
-           word->flags |= W_ITILDE;
-#endif
+           internal_tilde = 1;
 
-         /* XXX - bash-4.4/bash-5.0 */
          if (word->flags & W_ASSIGNARG)
            word->flags |= W_ASSIGNRHS;         /* affects $@ */
 
          if (isexp == 0 && (word->flags & (W_NOSPLIT|W_NOSPLIT2)) == 0 && isifs (c))
-           goto add_ifs_character;
+           {
+             has_quoted_ifs++;
+             goto add_ifs_character;
+           }
          else
            goto add_character;
 
        case ':':
-         if (word->flags & W_NOTILDE)
+         if (word->flags & (W_NOTILDE|W_NOASSNTILDE))
            {
              if (isexp == 0 && (word->flags & (W_NOSPLIT|W_NOSPLIT2)) == 0 && isifs (c))
                goto add_ifs_character;
@@ -9241,9 +11165,10 @@ add_string:
                goto add_character;
            }
 
-         if ((word->flags & (W_ASSIGNMENT|W_ASSIGNRHS|W_TILDEEXP)) &&
+         if ((word->flags & (W_ASSIGNMENT|W_ASSIGNRHS)) &&
+             (posixly_correct == 0 || (word->flags & W_TILDEEXP)) &&
              string[sindex+1] == '~')
-           word->flags |= W_ITILDE;
+           internal_tilde = 1;
 
          if (isexp == 0 && (word->flags & (W_NOSPLIT|W_NOSPLIT2)) == 0 && isifs (c))
            goto add_ifs_character;
@@ -9253,14 +11178,14 @@ add_string:
        case '~':
          /* If the word isn't supposed to be tilde expanded, or we're not
             at the start of a word or after an unquoted : or = in an
-            assignment statement, we don't do tilde expansion.  If we don't want
-            tilde expansion when expanding words to be passed to the arithmetic
-            evaluator, remove the check for Q_ARITH. */
-         if ((word->flags & (W_NOTILDE|W_DQUOTE)) ||
-             (sindex > 0 && ((word->flags & W_ITILDE) == 0)) ||
-             ((quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) && ((quoted & Q_ARITH) == 0)))
+            assignment statement, we don't do tilde expansion.  We don't
+            do tilde expansion if quoted or in an arithmetic context. */
+
+         if ((word->flags & W_NOTILDE) ||
+             (sindex > 0 && (internal_tilde == 0)) ||
+             (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)))
            {
-             word->flags &= ~W_ITILDE;
+             internal_tilde = 0;
              if (isexp == 0 && (word->flags & (W_NOSPLIT|W_NOSPLIT2)) == 0 && isifs (c) && (quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) == 0)
                goto add_ifs_character;
              else
@@ -9276,7 +11201,7 @@ add_string:
 
          temp = bash_tilde_find_word (string + sindex, tflag, &t_index);
            
-         word->flags &= ~W_ITILDE;
+         internal_tilde = 0;
 
          if (temp && *temp && t_index > 0)
            {
@@ -9301,6 +11226,7 @@ add_string:
        case '$':
          if (expanded_something)
            *expanded_something = 1;
+         local_expanded = 1;
 
          temp_has_dollar_at = 0;
          pflags = (word->flags & W_NOCOMSUB) ? PF_NOCOMSUB : 0;
@@ -9310,6 +11236,7 @@ add_string:
            pflags |= PF_ASSIGNRHS;
          if (word->flags & W_COMPLETE)
            pflags |= PF_COMPLETE;
+
          tword = param_expand (string, &sindex, quoted, expanded_something,
                               &temp_has_dollar_at, &quoted_dollar_at,
                               &had_quoted_null, pflags);
@@ -9327,7 +11254,9 @@ add_string:
            *contains_dollar_at = 1;
 
          if (tword && (tword->flags & W_HASQUOTEDNULL))
-           had_quoted_null = 1;
+           had_quoted_null = 1;                /* note for later */
+         if (tword && (tword->flags & W_SAWQUOTEDNULL))
+           had_quoted_null = 1;                /* XXX */
 
          temp = tword ? tword->word : (char *)NULL;
          dispose_word_desc (tword);
@@ -9347,7 +11276,7 @@ add_string:
          {
            t_index = sindex++;
 
-           temp = string_extract (string, &sindex, "`", SX_REQMATCH);
+           temp = string_extract (string, &sindex, "`", (word->flags & W_COMPLETE) ? SX_COMPLETE : SX_REQMATCH);
            /* The test of sindex against t_index is to allow bare instances of
               ` to pass through, for backwards compatibility. */
            if (temp == &extract_string_error || temp == &extract_string_fatal)
@@ -9357,7 +11286,7 @@ add_string:
                    sindex = t_index;
                    goto add_character;
                  }
-               last_command_exit_value = EXECUTION_FAILURE;
+               set_exit_status (EXECUTION_FAILURE);
                report_error (_("bad substitution: no closing \"`\" in %s") , string+t_index);
                free (string);
                free (istring);
@@ -9367,6 +11296,7 @@ add_string:
                
            if (expanded_something)
              *expanded_something = 1;
+           local_expanded = 1;
 
            if (word->flags & W_NOCOMSUB)
              /* sindex + 1 because string[sindex] == '`' */
@@ -9374,7 +11304,7 @@ add_string:
            else
              {
                de_backslash (temp);
-               tword = command_substitute (temp, quoted);
+               tword = command_substitute (temp, quoted, PF_BACKQUOTE);
                temp1 = tword ? tword->word : (char *)NULL;
                if (tword)
                  dispose_word_desc (tword);
@@ -9393,7 +11323,12 @@ add_string:
 
          c = string[++sindex];
 
-         if (quoted & Q_HERE_DOCUMENT)
+         /* "However, the double-quote character ( '"' ) shall not be treated
+            specially within a here-document, except when the double-quote
+            appears within "$()", "``", or "${}"." */
+         if ((quoted & Q_HERE_DOCUMENT) && (quoted & Q_DOLBRACE) && c == '"')
+           tflag = CBSDQUOTE;          /* special case */
+         else if (quoted & Q_HERE_DOCUMENT)
            tflag = CBSHDOC;
          else if (quoted & Q_DOUBLE_QUOTES)
            tflag = CBSDQUOTE;
@@ -9417,6 +11352,15 @@ add_string:
 
              SCOPY_CHAR_I (twochars, CTLESC, c, string, sindex, string_size);
            }
+         else if ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) && c == 0)
+           {
+             RESIZE_MALLOCED_BUFFER (istring, istring_index, 2, istring_size,
+                                     DEFAULT_ARRAY_SIZE);
+             istring[istring_index++] = CTLESC;
+             istring[istring_index++] = '\\';
+             istring[istring_index] = '\0';
+             break;          
+           }
          else if ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) && ((sh_syntaxtab[c] & tflag) == 0))
            {
              SCOPY_CHAR_I (twochars, '\\', c, string, sindex, string_size);
@@ -9444,6 +11388,7 @@ add_twochars:
          break;
 
        case '"':
+         /* XXX - revisit this */
          if ((quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) && ((quoted & Q_ARITH) == 0))
            goto add_character;
 
@@ -9461,9 +11406,8 @@ add_twochars:
              tword = alloc_word_desc ();
              tword->word = temp;
 
-             /* XXX - bash-4.4/bash-5.0 */
              if (word->flags & W_ASSIGNARG)
-               tword->flags |= word->flags & (W_ASSIGNARG|W_ASSIGNRHS);        /* affects $@ */
+               tword->flags |= word->flags & (W_ASSIGNARG|W_ASSIGNRHS); /* affects $@ */
              if (word->flags & W_COMPLETE)
                tword->flags |= W_COMPLETE;     /* for command substitutions */
              if (word->flags & W_NOCOMSUB)
@@ -9471,11 +11415,15 @@ add_twochars:
              if (word->flags & W_NOPROCSUB)
                tword->flags |= W_NOPROCSUB;
 
+             if (word->flags & W_ASSIGNRHS)
+               tword->flags |= W_ASSIGNRHS;
+
              temp = (char *)NULL;
 
-             temp_has_dollar_at = 0;   /* XXX */
+             temp_has_dollar_at = 0;   /* does this quoted (sub)string include $@? */
              /* Need to get W_HASQUOTEDNULL flag through this function. */
-             list = expand_word_internal (tword, Q_DOUBLE_QUOTES, 0, &temp_has_dollar_at, (int *)NULL);
+             /* XXX - preserve Q_ARITH here? */
+             list = expand_word_internal (tword, Q_DOUBLE_QUOTES|(quoted&Q_ARITH), 0, &temp_has_dollar_at, (int *)NULL);
              has_dollar_at += temp_has_dollar_at;
 
              if (list == &expand_word_error || list == &expand_word_fatal)
@@ -9493,27 +11441,36 @@ add_twochars:
 
              /* "$@" (a double-quoted dollar-at) expands into nothing,
                 not even a NULL word, when there are no positional
-                parameters. */
+                parameters.  Posix interp 888 says that other parts of the
+                word that expand to quoted nulls result in quoted nulls, so
+                we can't just throw the entire word away if we have "$@"
+                anywhere in it.  We use had_quoted_null to keep track */
              if (list == 0 && temp_has_dollar_at)      /* XXX - was has_dollar_at */
                {
                  quoted_dollar_at++;
                  break;
                }
 
+             /* If this list comes back with a quoted null from expansion,
+                we have either "$x" or "$@" with $1 == ''. In either case,
+                we need to make sure we add a quoted null argument and
+                disable the special handling that "$@" gets. */
+             if (list && list->word && list->next == 0 && (list->word->flags & W_HASQUOTEDNULL))
+               {
+                 if (had_quoted_null && temp_has_dollar_at)
+                   quoted_dollar_at++;
+                 had_quoted_null = 1;          /* XXX */
+               }
+
              /* If we get "$@", we know we have expanded something, so we
                 need to remember it for the final split on $IFS.  This is
                 a special case; it's the only case where a quoted string
                 can expand into more than one word.  It's going to come back
                 from the above call to expand_word_internal as a list with
-                a single word, in which all characters are quoted and
-                separated by blanks.  What we want to do is to turn it back
-                into a list for the next piece of code. */
+                multiple words. */
              if (list)
                dequote_list (list);
 
-             if (list && list->word && (list->word->flags & W_HASQUOTEDNULL))
-               had_quoted_null = 1;            /* XXX */
-
              if (temp_has_dollar_at)           /* XXX - was has_dollar_at */
                {
                  quoted_dollar_at++;
@@ -9521,6 +11478,7 @@ add_twochars:
                    *contains_dollar_at = 1;
                  if (expanded_something)
                    *expanded_something = 1;
+                 local_expanded = 1;
                }
            }
          else
@@ -9528,6 +11486,7 @@ add_twochars:
              /* What we have is "".  This is a minor optimization. */
              FREE (temp);
              list = (WORD_LIST *)NULL;
+             had_quoted_null = 1;      /* note for later */
            }
 
          /* The code above *might* return a list (consider the case of "$@",
@@ -9576,11 +11535,21 @@ add_twochars:
          else
            temp = (char *)NULL;
 
+         if (temp == 0 && quoted_state == PARTIALLY_QUOTED)
+           had_quoted_null = 1;        /* note for later */
+
          /* We do not want to add quoted nulls to strings that are only
             partially quoted; we can throw them away.  The exception to
             this is when we are going to be performing word splitting,
             since we have to preserve a null argument if the next character
             will cause word splitting. */
+         if (temp == 0 && quoted_state == PARTIALLY_QUOTED && quoted == 0 && (word->flags & (W_NOSPLIT|W_EXPANDRHS|W_ASSIGNRHS)) == W_EXPANDRHS)
+           {
+             c = CTLNUL;
+             sindex--;
+             had_quoted_null = 1;
+             goto add_character;
+           }
          if (temp == 0 && quoted_state == PARTIALLY_QUOTED && (word->flags & (W_NOSPLIT|W_NOSPLIT2)))
            continue;
 
@@ -9598,6 +11567,7 @@ add_twochars:
              /* Add NULL arg. */
              c = CTLNUL;
              sindex--;         /* add_character: label increments sindex */
+             had_quoted_null = 1;      /* note for later */
              goto add_character;
            }
 
@@ -9608,7 +11578,7 @@ add_twochars:
            goto add_character;
 
          t_index = ++sindex;
-         temp = string_extract_single_quoted (string, &sindex);
+         temp = string_extract_single_quoted (string, &sindex, 0);
 
          /* If the entire STRING was surrounded by single quotes,
             then the string is wholly quoted. */
@@ -9625,9 +11595,20 @@ add_twochars:
          else
            remove_quoted_escapes (temp);       /* ??? */
 
+         if (temp == 0 && quoted_state == PARTIALLY_QUOTED)
+           had_quoted_null = 1;        /* note for later */
+
          /* We do not want to add quoted nulls to strings that are only
             partially quoted; such nulls are discarded.  See above for the
-            exception, which is when the string is going to be split. */
+            exception, which is when the string is going to be split.
+            Posix interp 888/1129 */
+         if (temp == 0 && quoted_state == PARTIALLY_QUOTED && quoted == 0 && (word->flags & (W_NOSPLIT|W_EXPANDRHS|W_ASSIGNRHS)) == W_EXPANDRHS)
+           {
+             c = CTLNUL;
+             sindex--;
+             goto add_character;
+           }
+
          if (temp == 0 && (quoted_state == PARTIALLY_QUOTED) && (word->flags & (W_NOSPLIT|W_NOSPLIT2)))
            continue;
 
@@ -9643,11 +11624,30 @@ add_twochars:
 
          /* break; */
 
+       case ' ':
+         /* If we are in a context where the word is not going to be split, but
+            we need to account for $@ and $* producing one word for each
+            positional parameter, add quoted spaces so the spaces in the
+            expansion of "$@", if any, behave correctly. We still may need to
+            split if we are expanding the rhs of a word expansion. */
+         if (ifs_is_null || split_on_spaces || ((word->flags & (W_NOSPLIT|W_NOSPLIT2|W_ASSIGNRHS)) && (word->flags & W_EXPANDRHS) == 0))
+           {
+             if (string[sindex])
+               sindex++;
+             twochars[0] = CTLESC;
+             twochars[1] = c;
+             goto add_twochars;
+           }
+         /* FALLTHROUGH */
+         
        default:
          /* This is the fix for " $@ " */
-       add_ifs_character:
+add_ifs_character:
          if ((quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) || (isexp == 0 && isifs (c) && (word->flags & (W_NOSPLIT|W_NOSPLIT2)) == 0))
            {
+             if ((quoted&(Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) == 0)
+               has_quoted_ifs++;
+add_quoted_character:
              if (string[sindex])       /* from old goto dollar_add_string */
                sindex++;
              if (c == 0)
@@ -9658,6 +11658,8 @@ add_twochars:
              else
                {
 #if HANDLE_MULTIBYTE
+                 /* XXX - should make sure that c is actually multibyte,
+                    otherwise we can use the twochars branch */
                  if (mb_cur_max > 1)
                    sindex--;
 
@@ -9677,7 +11679,7 @@ add_twochars:
 
          SADD_MBCHAR (temp, string, sindex, string_size);
 
-       add_character:
+add_character:
          RESIZE_MALLOCED_BUFFER (istring, istring_index, 1, istring_size,
                                  DEFAULT_ARRAY_SIZE);
          istring[istring_index++] = c;
@@ -9717,11 +11719,17 @@ finished_with_string:
 
   if (*istring == '\0')
     {
+#if 0
       if (quoted_dollar_at == 0 && (had_quoted_null || quoted_state == PARTIALLY_QUOTED))
+#else
+      if (had_quoted_null || (quoted_dollar_at == 0 && quoted_state == PARTIALLY_QUOTED))
+#endif
        {
          istring[0] = CTLNUL;
          istring[1] = '\0';
-         tword = make_bare_word (istring);
+         tword = alloc_word_desc ();
+         tword->word = istring;
+         istring = 0;          /* avoid later free() */
          tword->flags |= W_HASQUOTEDNULL;              /* XXX */
          list = make_word_list (tword, (WORD_LIST *)NULL);
          if (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES))
@@ -9734,22 +11742,16 @@ finished_with_string:
         null arguments */
       else  if (quoted_state == UNQUOTED || quoted_dollar_at)
        list = (WORD_LIST *)NULL;
-#if 0
-      else
-       {
-         tword = make_bare_word (istring);
-         if (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES))
-           tword->flags |= W_QUOTED;
-         list = make_word_list (tword, (WORD_LIST *)NULL);
-       }
-#else
       else
        list = (WORD_LIST *)NULL;
-#endif
     }
   else if (word->flags & W_NOSPLIT)
     {
-      tword = make_bare_word (istring);
+      tword = alloc_word_desc ();
+      tword->word = istring;
+      if (had_quoted_null && QUOTED_NULL (istring))
+       tword->flags |= W_HASQUOTEDNULL;
+      istring = 0;             /* avoid later free() */
       if (word->flags & W_ASSIGNMENT)
        tword->flags |= W_ASSIGNMENT;   /* XXX */
       if (word->flags & W_COMPASSIGN)
@@ -9758,18 +11760,26 @@ finished_with_string:
        tword->flags |= W_NOGLOB;       /* XXX */
       if (word->flags & W_NOBRACE)
        tword->flags |= W_NOBRACE;      /* XXX */
-      if (word->flags & W_NOEXPAND)
-       tword->flags |= W_NOEXPAND;     /* XXX */
+      if (word->flags & W_ARRAYREF)
+       tword->flags |= W_ARRAYREF;
       if (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES))
        tword->flags |= W_QUOTED;
+      list = make_word_list (tword, (WORD_LIST *)NULL);
+    }
+  else if (word->flags & W_ASSIGNRHS)
+    {
+      list = list_string (istring, "", quoted);
+      tword = list->word;
       if (had_quoted_null && QUOTED_NULL (istring))
        tword->flags |= W_HASQUOTEDNULL;
-      list = make_word_list (tword, (WORD_LIST *)NULL);
+      free (list);
+      free (istring);
+      istring = 0;                     /* avoid later free() */
+      goto set_word_flags;
     }
   else
     {
       char *ifs_chars;
-      char *tstring;
 
       ifs_chars = (quoted_dollar_at || has_dollar_at) ? ifs_value : (char *)NULL;
 
@@ -9785,7 +11795,16 @@ finished_with_string:
         or we expanded "$@" with IFS null and we need to split the positional
         parameters into separate words. */
       if (split_on_spaces)
-       list = list_string (istring, " ", 1);   /* XXX quoted == 1? */
+       {
+         /* If IFS is not set, and the word is not quoted, we want to split
+            the individual words on $' \t\n'. We rely on previous steps to
+            quote the portions of the word that should not be split */
+         if (ifs_is_set == 0)
+           list = list_string (istring, " \t\n", 1);   /* XXX quoted == 1? */
+         else
+           list = list_string (istring, " ", 1);       /* XXX quoted == 1? */
+       }
+
       /* If we have $@ (has_dollar_at != 0) and we are in a context where we
         don't want to split the result (W_NOSPLIT2), and we are not quoted,
         we have already separated the arguments with the first character of
@@ -9797,35 +11816,49 @@ finished_with_string:
         double-quoted $@ while expanding it. */
       else if (has_dollar_at && quoted_dollar_at == 0 && ifs_chars && quoted == 0 && (word->flags & W_NOSPLIT2))
        {
+         tword = alloc_word_desc ();
          /* Only split and rejoin if we have to */
          if (*ifs_chars && *ifs_chars != ' ')
            {
+             /* list_string dequotes CTLESCs in the string it's passed, so we
+                need it to get the space separation right if space isn't the
+                first character in IFS (but is present) and to remove the 
+                quoting we added back in param_expand(). */
              list = list_string (istring, *ifs_chars ? ifs_chars : " ", 1);
-             tstring = string_list (list);
+             /* This isn't exactly right in the case where we're expanding
+                the RHS of an expansion like ${var-$@} where IFS=: (for
+                example). The W_NOSPLIT2 means we do the separation with :;
+                the list_string removes the quotes and breaks the string into
+                a list, and the string_list rejoins it on spaces. When we
+                return, we expect to be able to split the results, but the
+                space separation means the right split doesn't happen. */
+             tword->word = string_list (list); 
            }
          else
-           tstring = istring;
-         tword = make_bare_word (tstring);
-         if (tstring != istring)
-           free (tstring);
-         goto set_word_flags;
-       }
-      /* This is the attempt to make $* in an assignment context (a=$*) and
-        array variables subscripted with * in an assignment context (a=${foo[*]})
-        behave similarly.  It has side effects that, though they increase
-        compatibility with other shells, are not backwards compatible. */
-#if 0
-      else if (has_dollar_at && quoted == 0 && ifs_chars && (word->flags & W_ASSIGNRHS))
-       {
-         tword = make_bare_word (istring);
+           tword->word = istring;
+         if (had_quoted_null && QUOTED_NULL (istring))
+           tword->flags |= W_HASQUOTEDNULL;    /* XXX */
+         if (tword->word != istring)
+           free (istring);
+         istring = 0;                  /* avoid later free() */
          goto set_word_flags;
        }
-#endif
       else if (has_dollar_at && ifs_chars)
        list = list_string (istring, *ifs_chars ? ifs_chars : " ", 1);
       else
        {
-         tword = make_bare_word (istring);
+         tword = alloc_word_desc ();
+         if (expanded_something && *expanded_something == 0 && has_quoted_ifs)
+           tword->word = remove_quoted_ifs (istring);
+         else
+           tword->word = istring;
+         if (had_quoted_null && QUOTED_NULL (istring)) /* should check for more than one */
+           tword->flags |= W_HASQUOTEDNULL;    /* XXX */
+         else if (had_quoted_null)
+           tword->flags |= W_SAWQUOTEDNULL;    /* XXX */
+         if (tword->word != istring)
+           free (istring);
+         istring = 0;                  /* avoid later free() */
 set_word_flags:
          if ((quoted & (Q_DOUBLE_QUOTES|Q_HERE_DOCUMENT)) || (quoted_state == WHOLLY_QUOTED))
            tword->flags |= W_QUOTED;
@@ -9837,10 +11870,8 @@ set_word_flags:
            tword->flags |= W_NOGLOB;
          if (word->flags & W_NOBRACE)
            tword->flags |= W_NOBRACE;
-         if (word->flags & W_NOEXPAND)
-           tword->flags |= W_NOEXPAND;
-         if (had_quoted_null && QUOTED_NULL (istring))
-           tword->flags |= W_HASQUOTEDNULL;    /* XXX */
+         if (word->flags & W_ARRAYREF)
+           tword->flags |= W_ARRAYREF;
          list = make_word_list (tword, (WORD_LIST *)NULL);
        }
     }
@@ -9901,7 +11932,7 @@ string_quote_removal (string, quoted)
              break;
            }
          tindex = sindex + 1;
-         temp = string_extract_single_quoted (string, &tindex);
+         temp = string_extract_single_quoted (string, &tindex, 0);
          if (temp)
            {
              strcpy (r, temp);
@@ -10000,14 +12031,19 @@ setifs (v)
 #if defined (HANDLE_MULTIBYTE)
   if (ifs_value == 0)
     {
-      ifs_firstc[0] = '\0';
+      ifs_firstc[0] = '\0';    /* XXX - ? */
       ifs_firstc_len = 1;
     }
   else
     {
-      size_t ifs_len;
-      ifs_len = strnlen (ifs_value, MB_CUR_MAX);
-      ifs_firstc_len = MBLEN (ifs_value, ifs_len);
+      if (locale_utf8locale && UTF8_SINGLEBYTE (*ifs_value))
+       ifs_firstc_len = (*ifs_value != 0) ? 1 : 0;
+      else
+       {
+         size_t ifs_len;
+         ifs_len = strnlen (ifs_value, MB_CUR_MAX);
+         ifs_firstc_len = MBLEN (ifs_value, ifs_len);
+       }
       if (ifs_firstc_len == 1 || ifs_firstc_len == 0 || MB_INVALIDCH (ifs_firstc_len))
        {
          ifs_firstc[0] = ifs_value[0];
@@ -10058,10 +12094,30 @@ word_list_split (list)
      WORD_LIST *list;
 {
   WORD_LIST *result, *t, *tresult, *e;
+  WORD_DESC *w;
 
   for (t = list, result = (WORD_LIST *)NULL; t; t = t->next)
     {
       tresult = word_split (t->word, ifs_value);
+      /* POSIX 2.6: "If the complete expansion appropriate for a word results
+        in an empty field, that empty field shall be deleted from the list
+        of fields that form the completely expanded command, unless the
+        original word contained single-quote or double-quote characters."
+        This is where we handle these words that contain quoted null strings
+        and other characters that expand to nothing after word splitting. */
+      if (tresult == 0 && t->word && (t->word->flags & W_SAWQUOTEDNULL))       /* XXX */
+       {
+         w = alloc_word_desc ();
+         w->word = (char *)xmalloc (1);
+         w->word[0] = '\0';
+         tresult = make_word_list (w, (WORD_LIST *)NULL);
+       }
+#if defined (ARRAY_VARS)
+      /* pass W_ARRAYREF through for words that are not split and are
+        identical to the original word. */
+      if (tresult && tresult->next == 0 && t->next == 0 && (t->word->flags & W_ARRAYREF) && STREQ (t->word->word, tresult->word->word))
+       tresult->word->flags |= W_ARRAYREF;
+#endif
       if (result == 0)
         result = e = tresult;
       else
@@ -10089,6 +12145,8 @@ exp_jump_to_top_level (v)
 
   /* Cleanup code goes here. */
   expand_no_split_dollar_star = 0;     /* XXX */
+  if (expanding_redir)
+    undo_partial_redirects ();
   expanding_redir = 0;
   assigning_in_environment = 0;
 
@@ -10251,6 +12309,7 @@ glob_expand_word_list (tlist, eflags)
   register int glob_index;
   WORD_LIST *glob_list, *output_list, *disposables, *next;
   WORD_DESC *tword;
+  int x;
 
   output_list = disposables = (WORD_LIST *)NULL;
   glob_array = (char **)NULL;
@@ -10274,7 +12333,7 @@ glob_expand_word_list (tlist, eflags)
       if ((tlist->word->flags & W_NOGLOB) == 0 &&
          unquoted_glob_pattern_p (tlist->word->word))
        {
-         glob_array = shell_glob_filename (tlist->word->word);
+         glob_array = shell_glob_filename (tlist->word->word, QGLOB_CTLESC);   /* XXX */
 
          /* Handle error cases.
             I don't think we should report errors like "No such file
@@ -10426,8 +12485,10 @@ brace_expand_word_list (tlist, eflags)
 #endif
 
 #if defined (ARRAY_VARS)
-/* Take WORD, a compound associative array assignment, and internally run
-   'declare -A w', where W is the variable name portion of WORD. */
+/* Take WORD, a compound array assignment, and internally run (for example),
+   'declare -A w', where W is the variable name portion of WORD. OPTION is
+   the list of options to supply to `declare'. CMD is the declaration command
+   we are expanding right now; it's unused currently. */
 static int
 make_internal_declare (word, option, cmd)
      char *word;
@@ -10456,7 +12517,275 @@ make_internal_declare (word, option, cmd)
   dispose_words (wl);
   return r;
 }  
+
+/* Expand VALUE in NAME[+]=( VALUE ) to a list of words. FLAGS is 1 if NAME
+   is an associative array.
+
+   If we are  processing an indexed array, expand_compound_array_assignment
+   will expand all the individual words and quote_compound_array_list will
+   single-quote them. If we are processing an associative array, we use
+   parse_string_to_word_list to split VALUE into a list of words instead of
+   faking up a shell variable and calling expand_compound_array_assignment.
+   expand_and_quote_assoc_word expands and single-quotes each word in VALUE
+   together so we don't have problems finding the end of the subscript when
+   quoting it.
+
+   Words in VALUE can be individual words, which are expanded and single-quoted,
+   or words of the form [IND]=VALUE, which end up as explained below, as
+   ['expanded-ind']='expanded-value'. */
+
+static WORD_LIST *
+expand_oneword (value, flags)
+     char *value;
+     int flags;
+{
+  WORD_LIST *l, *nl;
+  char *t;
+  int kvpair;
+  
+  if (flags == 0)
+    {
+      /* Indexed array */
+      l = expand_compound_array_assignment ((SHELL_VAR *)NULL, value, flags);
+      /* Now we quote the results of the expansion above to prevent double
+        expansion. */
+      quote_compound_array_list (l, flags);
+      return l;
+    }
+  else
+    {
+      /* Associative array */
+      l = parse_string_to_word_list (value, 1, "array assign");
+#if ASSOC_KVPAIR_ASSIGNMENT
+      kvpair = kvpair_assignment_p (l);
+#endif
+
+      /* For associative arrays, with their arbitrary subscripts, we have to
+        expand and quote in one step so we don't have to search for the
+        closing right bracket more than once. */
+      for (nl = l; nl; nl = nl->next)
+       {
+#if ASSOC_KVPAIR_ASSIGNMENT
+         if (kvpair)
+           /* keys and values undergo the same set of expansions */
+           t = expand_and_quote_kvpair_word (nl->word->word);
+         else
 #endif
+         if ((nl->word->flags & W_ASSIGNMENT) == 0)
+           t = sh_single_quote (nl->word->word ? nl->word->word : "");
+         else
+           t = expand_and_quote_assoc_word (nl->word->word, flags);
+         free (nl->word->word);
+         nl->word->word = t;
+       }
+      return l;
+    }
+}
+
+/* Expand a single compound assignment argument to a declaration builtin.
+   This word takes the form NAME[+]=( VALUE ). The NAME[+]= is passed through
+   unchanged. The VALUE is expanded and each word in the result is single-
+   quoted. Words of the form [key]=value end up as
+   ['expanded-key']='expanded-value'. Associative arrays have special
+   handling, see expand_oneword() above. The return value is
+   NAME[+]=( expanded-and-quoted-VALUE ). */
+static void
+expand_compound_assignment_word (tlist, flags)
+     WORD_LIST *tlist;
+     int flags;
+{
+  WORD_LIST *l;
+  int wlen, oind, t;
+  char *value, *temp;
+
+/*itrace("expand_compound_assignment_word: original word = -%s-", tlist->word->word);*/
+  t = assignment (tlist->word->word, 0);
+
+  /* value doesn't have the open and close parens */
+  oind = 1;
+  value = extract_array_assignment_list (tlist->word->word + t + 1, &oind);
+  /* This performs one round of expansion on the index/key and value and
+     single-quotes each word in the result. */
+  l = expand_oneword (value, flags);
+  free (value);
+
+  value = string_list (l);
+  dispose_words (l);
+
+  wlen = STRLEN (value);
+
+  /* Now, let's rebuild the string */
+  temp = xmalloc (t + 3 + wlen + 1);   /* name[+]=(value) */
+  memcpy (temp, tlist->word->word, ++t);
+  temp[t++] = '(';
+  if (value)
+    memcpy (temp + t, value, wlen);
+  t += wlen;
+  temp[t++] = ')';
+  temp[t] = '\0';
+/*itrace("expand_compound_assignment_word: reconstructed word = -%s-", temp);*/
+
+  free (tlist->word->word);
+  tlist->word->word = temp;
+
+  free (value);
+}
+
+/* Expand and process an argument to a declaration command. We have already
+   set flags in TLIST->word->flags depending on the declaration command
+   (declare, local, etc.) and the options supplied to it (-a, -A, etc.).
+   TLIST->word->word is of the form NAME[+]=( VALUE ).   
+
+   This does several things, all using pieces of other functions to get the
+   evaluation sequence right. It's called for compound array assignments with
+   the W_ASSIGNMENT flag set (basically, valid identifier names on the lhs).
+   It parses out which flags need to be set for declare to create the variable
+   correctly, then calls declare internally (make_internal_declare) to make
+   sure the variable exists with the correct attributes. Before the variable
+   is created, it calls expand_compound_assignment_word to expand VALUE to a
+   list of words, appropriately quoted for further evaluation. This preserves
+   the semantics of word-expansion-before-calling-builtins. Finally, it calls
+   do_word_assignment to perform the expansion and assignment with the same
+   expansion semantics as a standalone assignment statement (no word splitting,
+   etc.) even though the word is single-quoted so all that needs to happen is
+   quote removal. */
+static WORD_LIST *
+expand_declaration_argument (tlist, wcmd)
+     WORD_LIST *tlist, *wcmd;
+{
+  char opts[16], omap[128];
+  int t, opti, oind, skip, inheriting;
+  WORD_LIST *l;
+
+  inheriting = localvar_inherit;
+  opti = 0;
+  if (tlist->word->flags & (W_ASSIGNASSOC|W_ASSNGLOBAL|W_CHKLOCAL|W_ASSIGNARRAY))
+    opts[opti++] = '-';
+
+  if ((tlist->word->flags & (W_ASSIGNASSOC|W_ASSNGLOBAL)) == (W_ASSIGNASSOC|W_ASSNGLOBAL))
+    {
+      opts[opti++] = 'g';
+      opts[opti++] = 'A';
+    }
+  else if (tlist->word->flags & W_ASSIGNASSOC)
+    {
+      opts[opti++] = 'A';
+    }
+  else if ((tlist->word->flags & (W_ASSIGNARRAY|W_ASSNGLOBAL)) == (W_ASSIGNARRAY|W_ASSNGLOBAL))
+    {
+      opts[opti++] = 'g';
+      opts[opti++] = 'a';
+    }
+  else if (tlist->word->flags & W_ASSIGNARRAY)
+    {
+      opts[opti++] = 'a';
+    }
+  else if (tlist->word->flags & W_ASSNGLOBAL)
+    opts[opti++] = 'g';
+
+  if (tlist->word->flags & W_CHKLOCAL)
+    opts[opti++] = 'G';
+
+  /* If we have special handling note the integer attribute and others
+     that transform the value upon assignment.  What we do is take all
+     of the option arguments and scan through them looking for options
+     that cause such transformations, and add them to the `opts' array. */
+
+  memset (omap, '\0', sizeof (omap));
+  for (l = wcmd->next; l != tlist; l = l->next)
+    {
+      int optchar;
+
+      if (l->word->word[0] != '-' && l->word->word[0] != '+')
+       break;  /* non-option argument */
+      if (l->word->word[0] == '-' && l->word->word[1] == '-' && l->word->word[2] == 0)
+       break;  /* -- signals end of options */
+      optchar = l->word->word[0];
+      for (oind = 1; l->word->word[oind]; oind++)
+       switch (l->word->word[oind])
+         {
+           case 'I':
+             inheriting = 1;
+           case 'i':
+           case 'l':
+           case 'u':
+           case 'c':
+             omap[l->word->word[oind]] = 1;
+             if (opti == 0)
+               opts[opti++] = optchar;
+             break;
+           default:
+             break;
+         }
+    }
+
+  for (oind = 0; oind < sizeof (omap); oind++)
+    if (omap[oind])
+      opts[opti++] = oind;
+
+  /* If there are no -a/-A options, but we have a compound assignment,
+     we have a choice: we can set opts[0]='-', opts[1]='a', since the
+     default is to create an indexed array, and call
+     make_internal_declare with that, or we can just skip the -a and let
+     declare_builtin deal with it.  Once we're here, we're better set
+     up for the latter, since we don't want to deal with looking up
+     any existing variable here -- better to let declare_builtin do it.
+     We need the variable created, though, especially if it's local, so
+     we get the scoping right before we call do_word_assignment.
+     To ensure that make_local_declare gets called, we add `--' if there
+     aren't any options. */
+  if ((tlist->word->flags & (W_ASSIGNASSOC|W_ASSIGNARRAY)) == 0)
+    {
+      if (opti == 0)
+       {
+         opts[opti++] = '-';
+          opts[opti++] = '-';
+       }
+    }
+  opts[opti] = '\0';
+
+  /* This isn't perfect, but it's a start. Improvements later. We expand
+     tlist->word->word and single-quote the results to avoid multiple
+     expansions by, say, do_assignment_internal(). We have to weigh the
+     cost of reconstructing the compound assignment string with its single
+     quoting and letting the declare builtin handle it. The single quotes
+     will prevent any unwanted additional expansion or word splitting. */
+  expand_compound_assignment_word (tlist, (tlist->word->flags & W_ASSIGNASSOC) ? 1 : 0);
+
+  skip = 0;
+  if (opti > 0)
+    {
+      t = make_internal_declare (tlist->word->word, opts, wcmd ? wcmd->word->word : (char *)0);
+      if (t != EXECUTION_SUCCESS)
+       {
+         last_command_exit_value = t;
+         if (tlist->word->flags & W_FORCELOCAL)        /* non-fatal error */
+           skip = 1;
+         else
+           exp_jump_to_top_level (DISCARD);
+       }
+    }
+
+  if (skip == 0)
+    {
+      t = do_word_assignment (tlist->word, 0);
+      if (t == 0)
+       {
+         last_command_exit_value = EXECUTION_FAILURE;
+         exp_jump_to_top_level (DISCARD);
+       }
+    }
+
+  /* Now transform the word as ksh93 appears to do and go on */
+  t = assignment (tlist->word->word, 0);
+  tlist->word->word[t] = '\0';
+  if (tlist->word->word[t - 1] == '+')
+    tlist->word->word[t - 1] = '\0';   /* cut off append op */
+  tlist->word->flags &= ~(W_ASSIGNMENT|W_NOSPLIT|W_COMPASSIGN|W_ASSIGNARG|W_ASSIGNASSOC|W_ASSIGNARRAY);
+
+  return (tlist);
+}
+#endif /* ARRAY_VARS */
 
 static WORD_LIST *
 shell_expand_word_list (tlist, eflags)
@@ -10465,18 +12794,15 @@ shell_expand_word_list (tlist, eflags)
 {
   WORD_LIST *expanded, *orig_list, *new_list, *next, *temp_list, *wcmd;
   int expanded_something, has_dollar_at;
-  char *temp_string;
 
   /* We do tilde expansion all the time.  This is what 1003.2 says. */
-  new_list = (WORD_LIST *)NULL;
-  for (wcmd = tlist; wcmd; wcmd = wcmd->next)
-    if (wcmd->word->flags & W_ASSNBLTIN)
-      break;
+  wcmd = new_list = (WORD_LIST *)NULL;
 
   for (orig_list = tlist; tlist; tlist = next)
     {
-      temp_string = tlist->word->word;
-
+      if (wcmd == 0 && (tlist->word->flags & W_ASSNBLTIN))
+       wcmd = tlist;
+       
       next = tlist->next;
 
 #if defined (ARRAY_VARS)
@@ -10487,94 +12813,7 @@ shell_expand_word_list (tlist, eflags)
          because `declare' does some evaluation of compound assignments on
          its own. */
       if ((tlist->word->flags & (W_COMPASSIGN|W_ASSIGNARG)) == (W_COMPASSIGN|W_ASSIGNARG))
-       {
-         int t;
-         char opts[16], opti;
-
-         opti = 0;
-         if (tlist->word->flags & (W_ASSIGNASSOC|W_ASSNGLOBAL|W_ASSIGNARRAY))
-           opts[opti++] = '-';
-
-         if ((tlist->word->flags & (W_ASSIGNASSOC|W_ASSNGLOBAL)) == (W_ASSIGNASSOC|W_ASSNGLOBAL))
-           {
-             opts[opti++] = 'g';
-             opts[opti++] = 'A';
-           }
-         else if (tlist->word->flags & W_ASSIGNASSOC)
-           opts[opti++] = 'A';
-         else if ((tlist->word->flags & (W_ASSIGNARRAY|W_ASSNGLOBAL)) == (W_ASSIGNARRAY|W_ASSNGLOBAL))
-           {
-             opts[opti++] = 'g';
-             opts[opti++] = 'a';
-           }
-         else if (tlist->word->flags & W_ASSIGNARRAY)
-           opts[opti++] = 'a';
-         else if (tlist->word->flags & W_ASSNGLOBAL)
-           opts[opti++] = 'g';
-
-         /* If we have special handling note the integer attribute and others
-            that transform the value upon assignment.  What we do is take all
-            of the option arguments and scan through them looking for options
-            that cause such transformations, and add them to the `opts' array. */
-/*       if (opti > 0) */
-           {
-             char omap[128];
-             int oind;
-             WORD_LIST *l;
-
-             memset (omap, '\0', sizeof (omap));
-             for (l = orig_list->next; l != tlist; l = l->next)
-               {
-                 if (l->word->word[0] != '-')
-                   break;      /* non-option argument */
-                 if (l->word->word[0] == '-' && l->word->word[1] == '-' && l->word->word[2] == 0)
-                   break;      /* -- signals end of options */
-                 for (oind = 1; l->word->word[oind]; oind++)
-                   switch (l->word->word[oind])
-                     {
-                       case 'i':
-                       case 'l':
-                       case 'u':
-                       case 'c':
-                         omap[l->word->word[oind]] = 1;
-                         if (opti == 0)
-                           opts[opti++] = '-';
-                         break;
-                       default:
-                         break;
-                     }
-               }
-
-             for (oind = 0; oind < sizeof (omap); oind++)
-               if (omap[oind])
-                 opts[opti++] = oind;
-           }
-
-         opts[opti] = '\0';
-         if (opti > 0)
-           {
-             t = make_internal_declare (tlist->word->word, opts, wcmd ? wcmd->word->word : (char *)0);
-             if (t != EXECUTION_SUCCESS)
-               {
-                 last_command_exit_value = t;
-                 exp_jump_to_top_level (DISCARD);
-               }
-           }
-
-         t = do_word_assignment (tlist->word, 0);
-         if (t == 0)
-           {
-             last_command_exit_value = EXECUTION_FAILURE;
-             exp_jump_to_top_level (DISCARD);
-           }
-
-         /* Now transform the word as ksh93 appears to do and go on */
-         t = assignment (tlist->word->word, 0);
-         tlist->word->word[t] = '\0';
-         if (tlist->word->word[t - 1] == '+')
-           tlist->word->word[t - 1] = '\0';    /* cut off append op */
-         tlist->word->flags &= ~(W_ASSIGNMENT|W_NOSPLIT|W_COMPASSIGN|W_ASSIGNARG|W_ASSIGNASSOC|W_ASSIGNARRAY);
-       }
+       expand_declaration_argument (tlist, wcmd);
 #endif
 
       expanded_something = 0;
@@ -10628,6 +12867,87 @@ shell_expand_word_list (tlist, eflags)
   return (new_list);
 }
 
+/* Perform assignment statements optionally preceding a command name COMMAND.
+   If COMMAND == NULL, is_nullcmd usually == 1. Follow the POSIX rules for
+   variable assignment errors. */
+static int
+do_assignment_statements (varlist, command, is_nullcmd)
+     WORD_LIST *varlist;
+     char *command;
+     int is_nullcmd;
+{
+  WORD_LIST *temp_list;
+  char *savecmd;
+  sh_wassign_func_t *assign_func;
+  int is_special_builtin, is_builtin_or_func, tint;
+
+  /* If the remainder of the words expand to nothing, Posix.2 requires
+     that the variable and environment assignments affect the shell's
+     environment (do_word_assignment). */
+  assign_func = is_nullcmd ? do_word_assignment : assign_in_env;
+  tempenv_assign_error = 0;
+
+  is_builtin_or_func = command && (find_shell_builtin (command) || find_function (command));
+  /* Posix says that special builtins exit if a variable assignment error
+     occurs in an assignment preceding it. (XXX - this is old -- current Posix
+     says that any variable assignment error causes a non-interactive shell
+     to exit. See the STRICT_POSIX checks below. */
+  is_special_builtin = posixly_correct && command && find_special_builtin (command);
+
+  savecmd = this_command_name;
+  for (temp_list = varlist; temp_list; temp_list = temp_list->next)
+    {
+      this_command_name = (char *)NULL;
+      assigning_in_environment = is_nullcmd == 0;
+      tint = (*assign_func) (temp_list->word, is_builtin_or_func);
+      assigning_in_environment = 0;
+      this_command_name = savecmd;
+
+      /* Variable assignment errors in non-interactive shells running
+        in posix mode cause the shell to exit. */
+      if (tint == 0)
+       {
+         if (is_nullcmd)       /* assignment statement */
+           {
+             last_command_exit_value = EXECUTION_FAILURE;
+#if defined (STRICT_POSIX)
+             if (posixly_correct && interactive_shell == 0)
+#else
+             if (posixly_correct && interactive_shell == 0 && executing_command_builtin == 0)
+#endif
+               exp_jump_to_top_level (FORCE_EOF);
+             else
+               exp_jump_to_top_level (DISCARD);
+           }
+         /* In posix mode, assignment errors in the temporary environment
+            cause a non-interactive shell executing a special builtin to
+            exit and a non-interactive shell to otherwise jump back to the
+            top level. This is what POSIX says to do for variable assignment
+            errors, and POSIX says errors in assigning to the temporary
+            environment are treated as variable assignment errors.
+            (XXX - this is not what current POSIX says - look at the
+            STRICT_POSIX defines. */
+         else if (posixly_correct)
+           {
+             last_command_exit_value = EXECUTION_FAILURE;
+#if defined (STRICT_POSIX)
+             exp_jump_to_top_level ((interactive_shell == 0) ? FORCE_EOF : DISCARD);
+#else
+             if (interactive_shell == 0 && is_special_builtin)
+               exp_jump_to_top_level (FORCE_EOF);
+             else if (interactive_shell == 0)
+               exp_jump_to_top_level (DISCARD);        /* XXX - maybe change later */
+             else
+               exp_jump_to_top_level (DISCARD);
+#endif
+           }
+         else
+           tempenv_assign_error++;
+       }
+    }
+  return (tempenv_assign_error);
+}
+
 /* The workhorse for expand_words () and expand_words_no_vars ().
    First arg is LIST, a WORD_LIST of words.
    Second arg EFLAGS is a flags word controlling which expansions are
@@ -10647,8 +12967,6 @@ expand_word_list_internal (list, eflags)
      int eflags;
 {
   WORD_LIST *new_list, *temp_list;
-  int tint;
-  char *savecmd;
 
   tempenv_assign_error = 0;
   if (list == 0)
@@ -10661,29 +12979,11 @@ expand_word_list_internal (list, eflags)
       if (new_list == 0)
        {
          if (subst_assign_varlist)
-           {
-             /* All the words were variable assignments, so they are placed
-                into the shell's environment. */
-             for (temp_list = subst_assign_varlist; temp_list; temp_list = temp_list->next)
-               {
-                 savecmd = this_command_name;
-                 this_command_name = (char *)NULL;     /* no arithmetic errors */
-                 tint = do_word_assignment (temp_list->word, 0);
-                 this_command_name = savecmd;
-                 /* Variable assignment errors in non-interactive shells
-                    running in Posix.2 mode cause the shell to exit. */
-                 if (tint == 0)
-                   {
-                     last_command_exit_value = EXECUTION_FAILURE;
-                     if (interactive_shell == 0 && posixly_correct)
-                       exp_jump_to_top_level (FORCE_EOF);
-                     else
-                       exp_jump_to_top_level (DISCARD);
-                   }
-               }
-             dispose_words (subst_assign_varlist);
-             subst_assign_varlist = (WORD_LIST *)NULL;
-           }
+           do_assignment_statements (subst_assign_varlist, (char *)NULL, 1);
+           
+         dispose_words (subst_assign_varlist);
+         subst_assign_varlist = (WORD_LIST *)NULL;
+
          return ((WORD_LIST *)NULL);
        }
     }
@@ -10717,44 +13017,7 @@ expand_word_list_internal (list, eflags)
 
   if ((eflags & WEXP_VARASSIGN) && subst_assign_varlist)
     {
-      sh_wassign_func_t *assign_func;
-      int is_special_builtin, is_builtin_or_func;
-
-      /* If the remainder of the words expand to nothing, Posix.2 requires
-        that the variable and environment assignments affect the shell's
-        environment. */
-      assign_func = new_list ? assign_in_env : do_word_assignment;
-      tempenv_assign_error = 0;
-
-      is_builtin_or_func = (new_list && new_list->word && (find_shell_builtin (new_list->word->word) || find_function (new_list->word->word)));
-      /* Posix says that special builtins exit if a variable assignment error
-        occurs in an assignment preceding it. */
-      is_special_builtin = (posixly_correct && new_list && new_list->word && find_special_builtin (new_list->word->word));
-      
-      for (temp_list = subst_assign_varlist; temp_list; temp_list = temp_list->next)
-       {
-         savecmd = this_command_name;
-         this_command_name = (char *)NULL;
-         assigning_in_environment = (assign_func == assign_in_env);
-         tint = (*assign_func) (temp_list->word, is_builtin_or_func);
-         assigning_in_environment = 0;
-         this_command_name = savecmd;
-         /* Variable assignment errors in non-interactive shells running
-            in Posix.2 mode cause the shell to exit. */
-         if (tint == 0)
-           {
-             if (assign_func == do_word_assignment)
-               {
-                 last_command_exit_value = EXECUTION_FAILURE;
-                 if (interactive_shell == 0 && posixly_correct && is_special_builtin)
-                   exp_jump_to_top_level (FORCE_EOF);
-                 else
-                   exp_jump_to_top_level (DISCARD);
-               }
-             else
-               tempenv_assign_error++;
-           }
-       }
+      do_assignment_statements (subst_assign_varlist, (new_list && new_list->word) ? new_list->word->word : (char *)NULL, new_list == 0);
 
       dispose_words (subst_assign_varlist);
       subst_assign_varlist = (WORD_LIST *)NULL;