ash: parser: Fix old-style command substitution here-document crash
[oweals/busybox.git] / shell / ash.c
index 305fb63483855b99c369bb870579a6ba31f29b2e..83cac3fb0a5eb315bacc52a6930829a717a298ba 100644 (file)
@@ -384,6 +384,7 @@ struct globals_misc {
        uint8_t exitstatus;     /* exit status of last command */
        uint8_t back_exitstatus;/* exit status of backquoted command */
        smallint job_warning;   /* user was warned about stopped jobs (can be 2, 1 or 0). */
+       int savestatus;         /* exit status of last command outside traps */
        int rootpid;            /* pid of main shell */
        /* shell level: 0 for the main shell, 1 for its children, and so on */
        int shlvl;
@@ -403,11 +404,11 @@ struct globals_misc {
        volatile /*sig_atomic_t*/ smallint pending_int; /* 1 = got SIGINT */
        volatile /*sig_atomic_t*/ smallint got_sigchld; /* 1 = got SIGCHLD */
        volatile /*sig_atomic_t*/ smallint pending_sig; /* last pending signal */
-       smallint exception_type; /* kind of exception (0..5) */
-       /* exceptions */
+       smallint exception_type; /* kind of exception: */
 #define EXINT 0         /* SIGINT received */
 #define EXERROR 1       /* a generic error */
-#define EXEXIT 4        /* exit the shell */
+#define EXEND 3         /* exit the shell */
+#define EXEXIT 4        /* exit the shell via exitcmd */
 
        char nullstr[1];        /* zero length string */
 
@@ -466,6 +467,7 @@ extern struct globals_misc *BB_GLOBAL_CONST ash_ptr_to_globals_misc;
 #define exitstatus        (G_misc.exitstatus )
 #define back_exitstatus   (G_misc.back_exitstatus )
 #define job_warning       (G_misc.job_warning)
+#define savestatus  (G_misc.savestatus )
 #define rootpid     (G_misc.rootpid    )
 #define shlvl       (G_misc.shlvl      )
 #define errlinno    (G_misc.errlinno   )
@@ -489,8 +491,9 @@ extern struct globals_misc *BB_GLOBAL_CONST ash_ptr_to_globals_misc;
 #define random_gen  (G_misc.random_gen )
 #define backgndpid  (G_misc.backgndpid )
 #define INIT_G_misc() do { \
-       (*(struct globals_misc**)&ash_ptr_to_globals_misc) = xzalloc(sizeof(G_misc)); \
+       (*(struct globals_misc**)not_const_pp(&ash_ptr_to_globals_misc)) = xzalloc(sizeof(G_misc)); \
        barrier(); \
+       savestatus = -1; \
        curdir = nullstr; \
        physdir = nullstr; \
        trap_ptr = trap; \
@@ -710,7 +713,7 @@ fmtstr(char *outbuf, size_t length, const char *fmt, ...)
        ret = vsnprintf(outbuf, length, fmt, ap);
        va_end(ap);
        INT_ON;
-       return ret;
+       return ret > (int)length ? length : ret;
 }
 
 static void
@@ -1542,7 +1545,7 @@ extern struct globals_memstack *BB_GLOBAL_CONST ash_ptr_to_globals_memstack;
 #define g_stacknleft (G_memstack.g_stacknleft)
 #define stackbase    (G_memstack.stackbase   )
 #define INIT_G_memstack() do { \
-       (*(struct globals_memstack**)&ash_ptr_to_globals_memstack) = xzalloc(sizeof(G_memstack)); \
+       (*(struct globals_memstack**)not_const_pp(&ash_ptr_to_globals_memstack)) = xzalloc(sizeof(G_memstack)); \
        barrier(); \
        g_stackp = &stackbase; \
        g_stacknxt = stackbase.space; \
@@ -1675,15 +1678,16 @@ popstackmark(struct stackmark *mark)
  * part of the block that has been used.
  */
 static void
-growstackblock(void)
+growstackblock(size_t min)
 {
        size_t newlen;
 
        newlen = g_stacknleft * 2;
        if (newlen < g_stacknleft)
                ash_msg_and_raise_error(bb_msg_memory_exhausted);
-       if (newlen < 128)
-               newlen += 128;
+       min = SHELL_ALIGN(min | 128);
+       if (newlen < min)
+               newlen += min;
 
        if (g_stacknxt == g_stackp->space && g_stackp != &stackbase) {
                struct stack_block *sp;
@@ -1733,10 +1737,18 @@ static void *
 growstackstr(void)
 {
        size_t len = stackblocksize();
-       growstackblock();
+       growstackblock(0);
        return (char *)stackblock() + len;
 }
 
+static char *
+growstackto(size_t len)
+{
+       if (stackblocksize() < len)
+               growstackblock(len);
+       return stackblock();
+}
+
 /*
  * Called from CHECKSTRSPACE.
  */
@@ -1744,18 +1756,8 @@ static char *
 makestrspace(size_t newlen, char *p)
 {
        size_t len = p - g_stacknxt;
-       size_t size;
 
-       for (;;) {
-               size_t nleft;
-
-               size = stackblocksize();
-               nleft = size - len;
-               if (nleft >= newlen)
-                       break;
-               growstackblock();
-       }
-       return (char *)stackblock() + len;
+       return growstackto(len + newlen) + len;
 }
 
 static char *
@@ -2165,7 +2167,7 @@ extern struct globals_var *BB_GLOBAL_CONST ash_ptr_to_globals_var;
 #endif
 #define INIT_G_var() do { \
        unsigned i; \
-       (*(struct globals_var**)&ash_ptr_to_globals_var) = xzalloc(sizeof(G_var)); \
+       (*(struct globals_var**)not_const_pp(&ash_ptr_to_globals_var)) = xzalloc(sizeof(G_var)); \
        barrier(); \
        for (i = 0; i < ARRAY_SIZE(varinit_data); i++) { \
                varinit[i].flags    = varinit_data[i].flags; \
@@ -2474,24 +2476,6 @@ unsetvar(const char *s)
        setvar(s, NULL, 0);
 }
 
-/*
- * Process a linked list of variable assignments.
- */
-static void
-listsetvar(struct strlist *list_set_var, int flags)
-{
-       struct strlist *lp = list_set_var;
-
-       if (!lp)
-               return;
-       INT_OFF;
-       do {
-               setvareq(lp->text, flags);
-               lp = lp->next;
-       } while (lp);
-       INT_ON;
-}
-
 /*
  * Generate a list of variables satisfying the given conditions.
  */
@@ -2555,51 +2539,102 @@ listvars(int on, int off, struct strlist *lp, char ***end)
 }
 
 
-/* ============ Path search helper
- *
+/* ============ Path search helper */
+static const char *
+legal_pathopt(const char *opt, const char *term, int magic)
+{
+       switch (magic) {
+       case 0:
+               opt = NULL;
+               break;
+
+       case 1:
+               opt = prefix(opt, "builtin") ?: prefix(opt, "func");
+               break;
+
+       default:
+               opt += strcspn(opt, term);
+               break;
+       }
+
+       if (opt && *opt == '%')
+               opt++;
+
+       return opt;
+}
+
+/*
  * The variable path (passed by reference) should be set to the start
- * of the path before the first call; path_advance will update
- * this value as it proceeds.  Successive calls to path_advance will return
+ * of the path before the first call; padvance will update
+ * this value as it proceeds.  Successive calls to padvance will return
  * the possible path expansions in sequence.  If an option (indicated by
  * a percent sign) appears in the path entry then the global variable
  * pathopt will be set to point to it; otherwise pathopt will be set to
  * NULL.
+ *
+ * If magic is 0 then pathopt recognition will be disabled.  If magic is
+ * 1 we shall recognise %builtin/%func.  Otherwise we shall accept any
+ * pathopt.
  */
-static const char *pathopt;     /* set by path_advance */
+static const char *pathopt;     /* set by padvance */
 
-static char *
-path_advance(const char **path, const char *name)
+static int
+padvance_magic(const char **path, const char *name, int magic)
 {
+       const char *term = "%:";
+       const char *lpathopt;
        const char *p;
        char *q;
        const char *start;
+       size_t qlen;
        size_t len;
 
        if (*path == NULL)
-               return NULL;
+               return -1;
+
+       lpathopt = NULL;
        start = *path;
-       for (p = start; *p && *p != ':' && *p != '%'; p++)
-               continue;
-       len = p - start + strlen(name) + 2;     /* "2" is for '/' and '\0' */
-       while (stackblocksize() < len)
-               growstackblock();
-       q = stackblock();
-       if (p != start) {
-               q = mempcpy(q, start, p - start);
-               *q++ = '/';
+
+       if (*start == '%' && (p = legal_pathopt(start + 1, term, magic))) {
+               lpathopt = start + 1;
+               start = p;
+               term = ":";
        }
-       strcpy(q, name);
-       pathopt = NULL;
+
+       len = strcspn(start, term);
+       p = start + len;
+
        if (*p == '%') {
-               pathopt = ++p;
-               while (*p && *p != ':')
-                       p++;
+               size_t extra = strchrnul(p, ':') - p;
+
+               if (legal_pathopt(p + 1, term, magic))
+                       lpathopt = p + 1;
+               else
+                       len += extra;
+
+               p += extra;
        }
-       if (*p == ':')
-               *path = p + 1;
-       else
-               *path = NULL;
-       return stalloc(len);
+
+       pathopt = lpathopt;
+       *path = *p == ':' ? p + 1 : NULL;
+
+       /* "2" is for '/' and '\0' */
+       qlen = len + strlen(name) + 2;
+       q = growstackto(qlen);
+
+       if (len) {
+               q = mempcpy(q, start, len);
+               *q++ = '/';
+       }
+       strcpy(q, name);
+
+       return qlen;
+}
+
+static int
+padvance(const char **path, const char *name)
+{
+       return padvance_magic(path, name, 1);
 }
 
 
@@ -2840,6 +2875,7 @@ cdcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
        char c;
        struct stat statb;
        int flags;
+       int len;
 
        flags = cdopt();
        dest = *argptr;
@@ -2869,9 +2905,10 @@ cdcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
        if (!*dest)
                dest = ".";
        path = bltinlookup("CDPATH");
-       while (path) {
-               c = *path;
-               p = path_advance(&path, dest);
+       while (p = path, (len = padvance(&path, dest)) >= 0) {
+               c = *p;
+               p = stalloc(len);
+
                if (stat(p, &statb) >= 0 && S_ISDIR(statb.st_mode)) {
                        if (c && c != ':')
                                flags |= CD_PRINT;
@@ -3755,8 +3792,6 @@ static struct job *jobtab; //5
 static unsigned njobs; //4
 /* current job */
 static struct job *curjob; //lots
-/* number of presumed living untracked jobs */
-static int jobless; //4
 
 #if 0
 /* Bash has a feature: it restores termios after a successful wait for
@@ -4200,12 +4235,11 @@ fg_bgcmd(int argc UNUSED_PARAM, char **argv)
 #endif
 
 static int
-sprint_status48(char *s, int status, int sigonly)
+sprint_status48(char *os, int status, int sigonly)
 {
-       int col;
+       char *s = os;
        int st;
 
-       col = 0;
        if (!WIFEXITED(status)) {
 #if JOBS
                if (WIFSTOPPED(status))
@@ -4223,17 +4257,17 @@ sprint_status48(char *s, int status, int sigonly)
                }
                st &= 0x7f;
 //TODO: use bbox's get_signame? strsignal adds ~600 bytes to text+rodata
-               col = fmtstr(s, 32, strsignal(st));
+               //s = stpncpy(s, strsignal(st), 32); //not all libc have stpncpy()
+               s += fmtstr(s, 32, strsignal(st));
                if (WCOREDUMP(status)) {
-                       strcpy(s + col, " (core dumped)");
-                       col += sizeof(" (core dumped)")-1;
+                       s = stpcpy(s, " (core dumped)");
                }
        } else if (!sigonly) {
                st = WEXITSTATUS(status);
-               col = fmtstr(s, 16, (st ? "Done(%d)" : "Done"), st);
+               s += fmtstr(s, 16, (st ? "Done(%d)" : "Done"), st);
        }
  out:
-       return col;
+       return s - os;
 }
 
 static int
@@ -4255,8 +4289,19 @@ wait_block_or_sig(int *status)
 #if 1
                sigfillset(&mask);
                sigprocmask2(SIG_SETMASK, &mask); /* mask is updated */
-               while (!got_sigchld && !pending_sig)
+               while (!got_sigchld && !pending_sig) {
                        sigsuspend(&mask);
+                       /* ^^^ add "sigdelset(&mask, SIGCHLD);" before sigsuspend
+                        * to make sure SIGCHLD is not masked off?
+                        * It was reported that this:
+                        *      fn() { : | return; }
+                        *      shopt -s lastpipe
+                        *      fn
+                        *      exec ash SCRIPT
+                        * under bash 4.4.23 runs SCRIPT with SIGCHLD masked,
+                        * making "wait" commands in SCRIPT block forever.
+                        */
+               }
                sigprocmask(SIG_SETMASK, &mask, NULL);
 #else /* unsafe: a signal can set pending_sig after check, but before pause() */
                while (!got_sigchld && !pending_sig)
@@ -4277,7 +4322,7 @@ wait_block_or_sig(int *status)
 #endif
 
 static int
-dowait(int block, struct job *job)
+waitone(int block, struct job *job)
 {
        int pid;
        int status;
@@ -4378,10 +4423,6 @@ dowait(int block, struct job *job)
                goto out;
        }
        /* The process wasn't found in job list */
-#if JOBS
-       if (!WIFSTOPPED(status))
-               jobless--;
-#endif
  out:
        INT_ON;
 
@@ -4406,6 +4447,20 @@ dowait(int block, struct job *job)
        return pid;
 }
 
+static int
+dowait(int block, struct job *jp)
+{
+       int pid = block == DOWAIT_NONBLOCK ? got_sigchld : 1;
+
+       while (jp ? jp->state == JOBRUNNING : pid > 0) {
+               if (!jp)
+                       got_sigchld = 0;
+               pid = waitone(block, jp);
+       }
+
+       return pid;
+}
+
 #if JOBS
 static void
 showjob(struct job *jp, int mode)
@@ -4494,8 +4549,7 @@ showjobs(int mode)
        TRACE(("showjobs(0x%x) called\n", mode));
 
        /* Handle all finished jobs */
-       while (dowait(DOWAIT_NONBLOCK, NULL) > 0)
-               continue;
+       dowait(DOWAIT_NONBLOCK, NULL);
 
        for (jp = curjob; jp; jp = jp->prev_job) {
                if (!(mode & SHOW_CHANGED) || jp->changed) {
@@ -4612,10 +4666,10 @@ waitcmd(int argc UNUSED_PARAM, char **argv)
 #else
                        dowait(DOWAIT_BLOCK_OR_SIG, NULL);
 #endif
-       /* if child sends us a signal *and immediately exits*,
-        * dowait() returns pid > 0. Check this case,
-        * not "if (dowait() < 0)"!
-        */
+                       /* if child sends us a signal *and immediately exits*,
+                        * dowait() returns pid > 0. Check this case,
+                        * not "if (dowait() < 0)"!
+                        */
                        if (pending_sig)
                                goto sigout;
 #if BASH_WAIT_N
@@ -4651,11 +4705,9 @@ waitcmd(int argc UNUSED_PARAM, char **argv)
                        job = getjob(*argv, 0);
                }
                /* loop until process terminated or stopped */
-               while (job->state == JOBRUNNING) {
-                       dowait(DOWAIT_BLOCK_OR_SIG, NULL);
-                       if (pending_sig)
-                               goto sigout;
-               }
+               dowait(DOWAIT_BLOCK_OR_SIG, NULL);
+               if (pending_sig)
+                       goto sigout;
                job->waited = 1;
                retval = getstatus(job);
  repeat: ;
@@ -4797,7 +4849,8 @@ cmdputs(const char *s)
                                str = "${";
                        goto dostr;
                case CTLENDVAR:
-                       str = "\"}" + !(quoted & 1);
+                       str = "\"}";
+                       str += !(quoted & 1);
                        quoted >>= 1;
                        subtype = 0;
                        goto dostr;
@@ -5206,7 +5259,6 @@ forkchild(struct job *jp, union node *n, int mode)
 #endif
        for (jp = curjob; jp; jp = jp->prev_job)
                freejob(jp);
-       jobless = 0;
 }
 
 /* Called after fork(), in parent */
@@ -5217,13 +5269,8 @@ static void
 forkparent(struct job *jp, union node *n, int mode, pid_t pid)
 {
        TRACE(("In parent shell: child = %d\n", pid));
-       if (!jp) {
-               /* jp is NULL when called by openhere() for heredoc support */
-               while (jobless && dowait(DOWAIT_NONBLOCK, NULL) > 0)
-                       continue;
-               jobless++;
+       if (!jp) /* jp is NULL when called by openhere() for heredoc support */
                return;
-       }
 #if JOBS
        if (mode != FORK_NOJOB && jp->jobctl) {
                int pgrp;
@@ -5300,43 +5347,41 @@ waitforjob(struct job *jp)
 {
        int st;
 
-       TRACE(("waitforjob(%%%d) called\n", jobno(jp)));
+       TRACE(("waitforjob(%%%d) called\n", jp ? jobno(jp) : 0));
 
-       INT_OFF;
-       while (jp->state == JOBRUNNING) {
-               /* In non-interactive shells, we _can_ get
-                * a keyboard signal here and be EINTRed,
-                * but we just loop back, waiting for command to complete.
-                *
-                * man bash:
-                * "If bash is waiting for a command to complete and receives
-                * a signal for which a trap has been set, the trap
-                * will not be executed until the command completes."
-                *
-                * Reality is that even if trap is not set, bash
-                * will not act on the signal until command completes.
-                * Try this. sleep5intoff.c:
-                * #include <signal.h>
-                * #include <unistd.h>
-                * int main() {
-                *         sigset_t set;
-                *         sigemptyset(&set);
-                *         sigaddset(&set, SIGINT);
-                *         sigaddset(&set, SIGQUIT);
-                *         sigprocmask(SIG_BLOCK, &set, NULL);
-                *         sleep(5);
-                *         return 0;
-                * }
-                * $ bash -c './sleep5intoff; echo hi'
-                * ^C^C^C^C <--- pressing ^C once a second
-                * $ _
-                * $ bash -c './sleep5intoff; echo hi'
-                * ^\^\^\^\hi <--- pressing ^\ (SIGQUIT)
-                * $ _
-                */
-               dowait(DOWAIT_BLOCK, jp);
-       }
-       INT_ON;
+       /* In non-interactive shells, we _can_ get
+        * a keyboard signal here and be EINTRed, but we just loop
+        * inside dowait(), waiting for command to complete.
+        *
+        * man bash:
+        * "If bash is waiting for a command to complete and receives
+        * a signal for which a trap has been set, the trap
+        * will not be executed until the command completes."
+        *
+        * Reality is that even if trap is not set, bash
+        * will not act on the signal until command completes.
+        * Try this. sleep5intoff.c:
+        * #include <signal.h>
+        * #include <unistd.h>
+        * int main() {
+        *         sigset_t set;
+        *         sigemptyset(&set);
+        *         sigaddset(&set, SIGINT);
+        *         sigaddset(&set, SIGQUIT);
+        *         sigprocmask(SIG_BLOCK, &set, NULL);
+        *         sleep(5);
+        *         return 0;
+        * }
+        * $ bash -c './sleep5intoff; echo hi'
+        * ^C^C^C^C <--- pressing ^C once a second
+        * $ _
+        * $ bash -c './sleep5intoff; echo hi'
+        * ^\^\^\^\hi <--- pressing ^\ (SIGQUIT)
+        * $ _
+        */
+       dowait(jp ? DOWAIT_BLOCK : DOWAIT_NONBLOCK, jp);
+       if (!jp)
+               return exitstatus;
 
        st = getstatus(jp);
 #if JOBS
@@ -5985,6 +6030,8 @@ static int substr_atoi(const char *s)
 #define EXP_VARTILDE2   0x20    /* expand tildes after colons only */
 #define EXP_WORD        0x40    /* expand word in parameter expansion */
 #define EXP_QUOTED      0x100   /* expand word in double quotes */
+#define EXP_KEEPNUL     0x200   /* do not skip NUL characters */
+
 /*
  * rmescape() flags
  */
@@ -5995,8 +6042,6 @@ static int substr_atoi(const char *s)
 
 /* Add CTLESC when necessary. */
 #define QUOTES_ESC     (EXP_FULL | EXP_CASE)
-/* Do not skip NUL characters. */
-#define QUOTES_KEEPNUL EXP_TILDE
 
 /*
  * Structure specifying which parts of the string should be searched
@@ -6025,26 +6070,6 @@ static struct ifsregion *ifslastp;
 /* holds expanded arg list */
 static struct arglist exparg;
 
-/*
- * Our own itoa().
- * cvtnum() is used even if math support is off (to prepare $? values and such).
- */
-static int
-cvtnum(arith_t num)
-{
-       int len;
-
-       /* 32-bit and wider ints require buffer size of bytes*3 (or less) */
-       len = sizeof(arith_t) * 3;
-       /* If narrower: worst case, 1-byte ints: need 5 bytes: "-127<NUL>" */
-       if (sizeof(arith_t) < 4) len += 2;
-
-       expdest = makestrspace(len, expdest);
-       len = fmtstr(expdest, len, ARITH_FMT, num);
-       STADJUST(len, expdest);
-       return len;
-}
-
 /*
  * Break the argument string into pieces based upon IFS and add the
  * strings to the argument list.  The regions of the string to be
@@ -6302,43 +6327,63 @@ preglob(const char *pattern, int flag)
 /*
  * Put a string on the stack.
  */
-static void
-memtodest(const char *p, size_t len, int syntax, int quotes)
+static size_t
+memtodest(const char *p, size_t len, int flags)
 {
+       int syntax = flags & EXP_QUOTED ? DQSYNTAX : BASESYNTAX;
        char *q;
+       char *s;
 
        if (!len)
-               return;
+               return 0;
 
-       q = makestrspace((quotes & QUOTES_ESC) ? len * 2 : len, expdest);
+       q = makestrspace(len * 2, expdest);
+       s = q;
 
        do {
                unsigned char c = *p++;
                if (c) {
-                       if (quotes & QUOTES_ESC) {
+                       if (flags & QUOTES_ESC) {
                                int n = SIT(c, syntax);
                                if (n == CCTL
-                                || (syntax != BASESYNTAX && n == CBACK)
+                                || ((flags & EXP_QUOTED) && n == CBACK)
                                ) {
                                        USTPUTC(CTLESC, q);
                                }
                        }
-               } else if (!(quotes & QUOTES_KEEPNUL))
+               } else if (!(flags & EXP_KEEPNUL))
                        continue;
                USTPUTC(c, q);
        } while (--len);
 
        expdest = q;
+       return q - s;
 }
 
 static size_t
-strtodest(const char *p, int syntax, int quotes)
+strtodest(const char *p, int flags)
 {
        size_t len = strlen(p);
-       memtodest(p, len, syntax, quotes);
+       memtodest(p, len, flags);
        return len;
 }
 
+/*
+ * Our own itoa().
+ * cvtnum() is used even if math support is off (to prepare $? values and such).
+ */
+static int
+cvtnum(arith_t num, int flags)
+{
+       /* 32-bit and wider ints require buffer size of bytes*3 (or less) */
+       /* If narrower: worst case, 1-byte ints: need 5 bytes: "-127<NUL>" */
+       int len = (sizeof(arith_t) >= 4) ? sizeof(arith_t) * 3 : sizeof(arith_t) * 3 + 2;
+       char buf[len];
+
+       len = fmtstr(buf, len, ARITH_FMT, num);
+       return memtodest(buf, len, flags);
+}
+
 /*
  * Record the fact that we have to scan this region of the
  * string for IFS characters.
@@ -6403,13 +6448,12 @@ removerecordregions(int endoff)
 }
 
 static char *
-exptilde(char *startp, char *p, int flags)
+exptilde(char *startp, char *p, int flag)
 {
        unsigned char c;
        char *name;
        struct passwd *pw;
        const char *home;
-       int quotes = flags & QUOTES_ESC;
 
        name = p + 1;
 
@@ -6420,7 +6464,7 @@ exptilde(char *startp, char *p, int flags)
                case CTLQUOTEMARK:
                        return startp;
                case ':':
-                       if (flags & EXP_VARTILDE)
+                       if (flag & EXP_VARTILDE)
                                goto done;
                        break;
                case '/':
@@ -6438,10 +6482,10 @@ exptilde(char *startp, char *p, int flags)
                        goto lose;
                home = pw->pw_dir;
        }
-       if (!home || !*home)
+       if (!home)
                goto lose;
        *p = c;
-       strtodest(home, SQSYNTAX, quotes);
+       strtodest(home, flag | EXP_QUOTED);
        return p;
  lose:
        *p = c;
@@ -6541,7 +6585,6 @@ expbackq(union node *cmd, int flag)
        char *p;
        char *dest;
        int startloc;
-       int syntax = flag & EXP_QUOTED ? DQSYNTAX : BASESYNTAX;
        struct stackmark smark;
 
        INT_OFF;
@@ -6555,7 +6598,7 @@ expbackq(union node *cmd, int flag)
        if (i == 0)
                goto read;
        for (;;) {
-               memtodest(p, i, syntax, flag & QUOTES_ESC);
+               memtodest(p, i, flag);
  read:
                if (in.fd < 0)
                        break;
@@ -6575,7 +6618,7 @@ expbackq(union node *cmd, int flag)
 
        /* Eat all trailing newlines */
        dest = expdest;
-       for (; dest > (char *)stackblock() && dest[-1] == '\n';)
+       for (; dest > ((char *)stackblock() + startloc) && dest[-1] == '\n';)
                STUNPUTC(dest);
        expdest = dest;
 
@@ -6639,7 +6682,7 @@ expari(int flag)
        if (flag & QUOTES_ESC)
                rmescapes(p + 1, 0, NULL);
 
-       len = cvtnum(ash_arith(p + 1));
+       len = cvtnum(ash_arith(p + 1), flag);
 
        if (!(flag & EXP_QUOTED))
                recordregion(begoff, begoff + len, 0);
@@ -7264,11 +7307,10 @@ varvalue(char *name, int varflags, int flags, int quoted)
        int sep;
        int subtype = varflags & VSTYPE;
        int discard = subtype == VSPLUS || subtype == VSLENGTH;
-       int quotes = (discard ? 0 : (flags & QUOTES_ESC)) | QUOTES_KEEPNUL;
-       int syntax;
 
+       flags |= EXP_KEEPNUL;
+       flags &= discard ? ~QUOTES_ESC : ~0;
        sep = (flags & EXP_FULL) << CHAR_BIT;
-       syntax = quoted ? DQSYNTAX : BASESYNTAX;
 
        switch (*name) {
        case '$':
@@ -7285,7 +7327,7 @@ varvalue(char *name, int varflags, int flags, int quoted)
                if (num == 0)
                        return -1;
  numvar:
-               len = cvtnum(num);
+               len = cvtnum(num, flags);
                goto check_1char_name;
        case '-':
                expdest = makestrspace(NOPTS, expdest);
@@ -7334,11 +7376,11 @@ varvalue(char *name, int varflags, int flags, int quoted)
                if (!ap)
                        return -1;
                while ((p = *ap++) != NULL) {
-                       len += strtodest(p, syntax, quotes);
+                       len += strtodest(p, flags);
 
                        if (*ap && sep) {
                                len++;
-                               memtodest(&sepc, 1, syntax, quotes);
+                               memtodest(&sepc, 1, flags);
                        }
                }
                break;
@@ -7365,7 +7407,7 @@ varvalue(char *name, int varflags, int flags, int quoted)
                if (!p)
                        return -1;
 
-               len = strtodest(p, syntax, quotes);
+               len = strtodest(p, flags);
 #if ENABLE_UNICODE_SUPPORT
                if (subtype == VSLENGTH && len > 0) {
                        reinit_unicode_for_ash();
@@ -7451,7 +7493,7 @@ evalvar(char *p, int flag)
                varunset(p, var, 0, 0);
 
        if (subtype == VSLENGTH) {
-               cvtnum(varlen > 0 ? varlen : 0);
+               cvtnum(varlen > 0 ? varlen : 0, flag);
                goto record;
        }
 
@@ -7504,6 +7546,8 @@ evalvar(char *p, int flag)
                goto record;
        }
 
+       varlen = 0;
+
  end:
        if (subtype != VSNORMAL) {      /* skip to end of alternative */
                int nesting = 1;
@@ -8047,7 +8091,7 @@ struct cmdentry {
 #define DO_ABS          0x02    /* checks absolute paths */
 #define DO_NOFUNC       0x04    /* don't return shell functions, for command */
 #define DO_ALTPATH      0x08    /* using alternate path */
-#define DO_ALTBLTIN     0x20    /* %builtin in alt. path */
+#define DO_REGBLTIN     0x10    /* regular built-ins and functions only */
 
 static void find_command(char *, struct cmdentry *, int, const char *);
 
@@ -8167,13 +8211,13 @@ static void shellexec(char *prog, char **argv, const char *path, int idx)
        } else {
  try_PATH:
                e = ENOENT;
-               while ((cmdname = path_advance(&path, prog)) != NULL) {
+               while (padvance(&path, argv[0]) >= 0) {
+                       cmdname = stackblock();
                        if (--idx < 0 && pathopt == NULL) {
                                tryexec(IF_FEATURE_SH_STANDALONE(-1,) cmdname, argv, envp);
                                if (errno != ENOENT && errno != ENOTDIR)
                                        e = errno;
                        }
-                       stunalloc(cmdname);
                }
        }
 
@@ -8192,7 +8236,7 @@ static void shellexec(char *prog, char **argv, const char *path, int idx)
        exitstatus = exerrno;
        TRACE(("shellexec failed for %s, errno %d, suppress_int %d\n",
                prog, e, suppress_int));
-       ash_msg_and_raise(EXEXIT, "%s: %s", prog, errmsg(e, "not found"));
+       ash_msg_and_raise(EXEND, "%s: %s", prog, errmsg(e, "not found"));
        /* NOTREACHED */
 }
 
@@ -8206,18 +8250,17 @@ printentry(struct tblentry *cmdp)
        idx = cmdp->param.index;
        path = pathval();
        do {
-               name = path_advance(&path, cmdp->cmdname);
-               stunalloc(name);
+               padvance(&path, cmdp->cmdname);
        } while (--idx >= 0);
+       name = stackblock();
        out1fmt("%s%s\n", name, (cmdp->rehash ? "*" : nullstr));
 }
 
 /*
- * Clear out command entries.  The argument specifies the first entry in
- * PATH which has changed.
+ * Clear out command entries.
  */
 static void
-clearcmdentry(int firstchange)
+clearcmdentry(void)
 {
        struct tblentry **tblp;
        struct tblentry **pp;
@@ -8227,10 +8270,11 @@ clearcmdentry(int firstchange)
        for (tblp = cmdtable; tblp < &cmdtable[CMDTABLESIZE]; tblp++) {
                pp = tblp;
                while ((cmdp = *pp) != NULL) {
-                       if ((cmdp->cmdtype == CMDNORMAL &&
-                            cmdp->param.index >= firstchange)
-                        || (cmdp->cmdtype == CMDBUILTIN &&
-                            builtinloc >= firstchange)
+                       if (cmdp->cmdtype == CMDNORMAL
+                        || (cmdp->cmdtype == CMDBUILTIN
+                           && !IS_BUILTIN_REGULAR(cmdp->param.cmd)
+                           && builtinloc > 0
+                           )
                        ) {
                                *pp = cmdp->next;
                                free(cmdp);
@@ -8330,7 +8374,7 @@ hashcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
        char *name;
 
        if (nextopt("r") != '\0') {
-               clearcmdentry(0);
+               clearcmdentry();
                return 0;
        }
 
@@ -8349,7 +8393,11 @@ hashcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
                cmdp = cmdlookup(name, 0);
                if (cmdp != NULL
                 && (cmdp->cmdtype == CMDNORMAL
-                    || (cmdp->cmdtype == CMDBUILTIN && builtinloc >= 0))
+                   || (cmdp->cmdtype == CMDBUILTIN
+                       && !IS_BUILTIN_REGULAR(cmdp->param.cmd)
+                       && builtinloc > 0
+                       )
+                   )
                ) {
                        delete_cmd_entry();
                }
@@ -8391,42 +8439,28 @@ hashcd(void)
  * Called with interrupts off.
  */
 static void FAST_FUNC
-changepath(const char *new)
+changepath(const char *newval)
 {
-       const char *old;
-       int firstchange;
+       const char *new;
        int idx;
-       int idx_bltin;
+       int bltin;
 
-       old = pathval();
-       firstchange = 9999;     /* assume no change */
+       new = newval;
        idx = 0;
-       idx_bltin = -1;
+       bltin = -1;
        for (;;) {
-               if (*old != *new) {
-                       firstchange = idx;
-                       if ((*old == '\0' && *new == ':')
-                        || (*old == ':' && *new == '\0')
-                       ) {
-                               firstchange++;
-                       }
-                       old = new;      /* ignore subsequent differences */
+               if (*new == '%' && prefix(new + 1, "builtin")) {
+                       bltin = idx;
+                       break;
                }
-               if (*new == '\0')
+               new = strchr(new, ':');
+               if (!new)
                        break;
-               if (*new == '%' && idx_bltin < 0 && prefix(new + 1, "builtin"))
-                       idx_bltin = idx;
-               if (*new == ':')
-                       idx++;
+               idx++;
                new++;
-               old++;
        }
-       if (builtinloc < 0 && idx_bltin >= 0)
-               builtinloc = idx_bltin;             /* zap builtins */
-       if (builtinloc >= 0 && idx_bltin < 0)
-               firstchange = 0;
-       clearcmdentry(firstchange);
-       builtinloc = idx_bltin;
+       builtinloc = bltin;
+       clearcmdentry();
 }
 enum {
        TEOF,
@@ -8601,9 +8635,9 @@ describe_command(char *command, const char *path, int describe_command_verbose)
                        p = command;
                } else {
                        do {
-                               p = path_advance(&path, command);
-                               stunalloc(p);
+                               padvance(&path, command);
                        } while (--j >= 0);
+                       p = stackblock();
                }
                if (describe_command_verbose) {
                        out1fmt(" is %s", p);
@@ -8663,24 +8697,43 @@ typecmd(int argc UNUSED_PARAM, char **argv)
 }
 
 #if ENABLE_ASH_CMDCMD
+static struct strlist *
+fill_arglist(struct arglist *arglist, union node **argpp)
+{
+       struct strlist **lastp = arglist->lastp;
+       union node *argp;
+
+       while ((argp = *argpp) != NULL) {
+               expandarg(argp, arglist, EXP_FULL | EXP_TILDE);
+               *argpp = argp->narg.next;
+               if (*lastp)
+                       break;
+       }
+
+       return *lastp;
+}
+
 /* Is it "command [-p] PROG ARGS" bltin, no other opts? Return ptr to "PROG" if yes */
-static char **
-parse_command_args(char **argv, const char **path)
+static int
+parse_command_args(struct arglist *arglist, union node **argpp, const char **path)
 {
+       struct strlist *sp = arglist->list;
        char *cp, c;
 
        for (;;) {
-               cp = *++argv;
-               if (!cp)
-                       return NULL;
+               sp = sp->next ? sp->next : fill_arglist(arglist, argpp);
+               if (!sp)
+                       return 0;
+               cp = sp->text;
                if (*cp++ != '-')
                        break;
                c = *cp++;
                if (!c)
                        break;
                if (c == '-' && !*cp) {
-                       if (!*++argv)
-                               return NULL;
+                       if (!sp->next && !fill_arglist(arglist, argpp))
+                               return 0;
+                       sp = sp->next;
                        break;
                }
                do {
@@ -8690,12 +8743,14 @@ parse_command_args(char **argv, const char **path)
                                break;
                        default:
                                /* run 'typecmd' for other options */
-                               return NULL;
+                               return 0;
                        }
                        c = *cp++;
                } while (c);
        }
-       return argv;
+
+       arglist->list = sp;
+       return DO_NOFUNC;
 }
 
 static int FAST_FUNC
@@ -9035,6 +9090,7 @@ defun(union node *func)
 #define SKIPBREAK      (1 << 0)
 #define SKIPCONT       (1 << 1)
 #define SKIPFUNC       (1 << 2)
+#define SKIPFUNCDEF    (1 << 3)
 static smallint evalskip;       /* set to SKIPxxx if we are skipping commands */
 static int skipcount;           /* number of levels to skip */
 static int loopnest;            /* current loop nesting level */
@@ -9055,12 +9111,17 @@ dotrap(void)
 {
        uint8_t *g;
        int sig;
-       uint8_t last_status;
+       int status, last_status;
 
        if (!pending_sig)
                return;
 
-       last_status = exitstatus;
+       status = savestatus;
+       last_status = status;
+       if (status < 0) {
+               status = exitstatus;
+               savestatus = status;
+       }
        pending_sig = 0;
        barrier();
 
@@ -9087,8 +9148,11 @@ dotrap(void)
                if (!p)
                        continue;
                evalstring(p, 0);
+               if (evalskip != SKIPFUNC)
+                       exitstatus = status;
        }
-       exitstatus = last_status;
+
+       savestatus = last_status;
        TRACE(("dotrap returns\n"));
 }
 
@@ -9226,9 +9290,9 @@ evaltree(union node *n, int flags)
        dotrap();
 
        if (checkexit & status)
-               raise_exception(EXEXIT);
+               raise_exception(EXEND);
        if (flags & EV_EXIT)
-               raise_exception(EXEXIT);
+               raise_exception(EXEND);
 
        popstackmark(&smark);
        TRACE(("leaving evaltree (no interrupts)\n"));
@@ -9434,11 +9498,10 @@ expredir(union node *n)
                case NFROMFD:
                case NTOFD: /* >& */
                        if (redir->ndup.vname) {
-                               expandarg(redir->ndup.vname, &fn, EXP_FULL | EXP_TILDE);
+                               expandarg(redir->ndup.vname, &fn, EXP_TILDE | EXP_REDIR);
                                if (fn.list == NULL)
                                        ash_msg_and_raise_error("redir error");
 #if BASH_REDIR_OUTPUT
-//FIXME: we used expandarg with different args!
                                if (!isdigit_str9(fn.list->text)) {
                                        /* >&file, not >&fd */
                                        if (redir->nfile.fd != 1) /* 123>&file - BAD */
@@ -9523,6 +9586,11 @@ evalpipe(union node *n, int flags)
        return status;
 }
 
+/* setinteractive needs this forward reference */
+#if EDITING_HAS_get_exe_name
+static const char *get_builtin_name(int i) FAST_FUNC;
+#endif
+
 /*
  * Controls whether the shell is interactive or not.
  */
@@ -9554,8 +9622,12 @@ setinteractive(int on)
                }
 #endif
 #if ENABLE_FEATURE_EDITING
-               if (!line_input_state)
+               if (!line_input_state) {
                        line_input_state = new_line_input_t(FOR_SHELL | WITH_PATH_LOOKUP);
+# if EDITING_HAS_get_exe_name
+                       line_input_state->get_exe_name = get_builtin_name;
+# endif
+               }
 #endif
        }
 }
@@ -9649,18 +9721,23 @@ poplocalvars(int keep)
  * Create a new localvar environment.
  */
 static struct localvar_list *
-pushlocalvars(void)
+pushlocalvars(int push)
 {
        struct localvar_list *ll;
+       struct localvar_list *top;
+
+       top = localvar_stack;
+       if (!push)
+               goto out;
 
        INT_OFF;
        ll = ckzalloc(sizeof(*ll));
        /*ll->lv = NULL; - zalloc did it */
-       ll->next = localvar_stack;
+       ll->next = top;
        localvar_stack = ll;
        INT_ON;
-
-       return ll->next;
+ out:
+       return top;
 }
 
 static void
@@ -9707,7 +9784,7 @@ evalfun(struct funcnode *func, int argc, char **argv, int flags)
        shellparam = saveparam;
        exception_handler = savehandler;
        INT_ON;
-       evalskip &= ~SKIPFUNC;
+       evalskip &= ~(SKIPFUNC | SKIPFUNCDEF);
        return e;
 }
 
@@ -9719,7 +9796,7 @@ evalfun(struct funcnode *func, int argc, char **argv, int flags)
  * (options will be restored on return from the function).
  */
 static void
-mklocal(char *name)
+mklocal(char *name, int flags)
 {
        struct localvar *lvp;
        struct var **vpp;
@@ -9756,9 +9833,9 @@ mklocal(char *name)
                if (vp == NULL) {
                        /* variable did not exist yet */
                        if (eq)
-                               vp = setvareq(name, VSTRFIXED);
+                               vp = setvareq(name, VSTRFIXED | flags);
                        else
-                               vp = setvar(name, NULL, VSTRFIXED);
+                               vp = setvar(name, NULL, VSTRFIXED | flags);
                        lvp->flags = VUNSET;
                } else {
                        lvp->text = vp->var_text;
@@ -9768,7 +9845,7 @@ mklocal(char *name)
                         */
                        vp->flags |= VSTRFIXED|VTEXTFIXED;
                        if (eq)
-                               setvareq(name, 0);
+                               setvareq(name, flags);
                        else
                                /* "local VAR" unsets VAR: */
                                setvar0(name, NULL);
@@ -9794,7 +9871,7 @@ localcmd(int argc UNUSED_PARAM, char **argv)
 
        argv = argptr;
        while ((name = *argv++) != NULL) {
-               mklocal(name);
+               mklocal(name, 0);
        }
        return 0;
 }
@@ -9852,12 +9929,23 @@ execcmd(int argc UNUSED_PARAM, char **argv)
 static int FAST_FUNC
 returncmd(int argc UNUSED_PARAM, char **argv)
 {
+       int skip;
+       int status;
+
        /*
         * If called outside a function, do what ksh does;
         * skip the rest of the file.
         */
-       evalskip = SKIPFUNC;
-       return argv[1] ? number(argv[1]) : exitstatus;
+       if (argv[1]) {
+               skip = SKIPFUNC;
+               status = number(argv[1]);
+       } else {
+               skip = SKIPFUNCDEF;
+               status = exitstatus;
+       }
+       evalskip = skip;
+
+       return status;
 }
 
 /* Forward declarations for builtintab[] */
@@ -9944,7 +10032,7 @@ static const struct builtincmd builtintab[] = {
 #if ENABLE_ASH_GETOPTS
        { BUILTIN_REGULAR       "getopts" , getoptscmd },
 #endif
-       { BUILTIN_NOSPEC        "hash"    , hashcmd    },
+       { BUILTIN_REGULAR       "hash"    , hashcmd    },
 #if ENABLE_ASH_HELP
        { BUILTIN_NOSPEC        "help"    , helpcmd    },
 #endif
@@ -9962,7 +10050,7 @@ static const struct builtincmd builtintab[] = {
 #if ENABLE_ASH_PRINTF
        { BUILTIN_REGULAR       "printf"  , printfcmd  },
 #endif
-       { BUILTIN_NOSPEC        "pwd"     , pwdcmd     },
+       { BUILTIN_REGULAR       "pwd"     , pwdcmd     },
        { BUILTIN_REGULAR       "read"    , readcmd    },
        { BUILTIN_SPEC_REG_ASSG "readonly", exportcmd  },
        { BUILTIN_SPEC_REG      "return"  , returncmd  },
@@ -9977,8 +10065,8 @@ static const struct builtincmd builtintab[] = {
        { BUILTIN_SPEC_REG      "times"   , timescmd   },
        { BUILTIN_SPEC_REG      "trap"    , trapcmd    },
        { BUILTIN_REGULAR       "true"    , truecmd    },
-       { BUILTIN_NOSPEC        "type"    , typecmd    },
-       { BUILTIN_NOSPEC        "ulimit"  , ulimitcmd  },
+       { BUILTIN_REGULAR       "type"    , typecmd    },
+       { BUILTIN_REGULAR       "ulimit"  , ulimitcmd  },
        { BUILTIN_REGULAR       "umask"   , umaskcmd   },
 #if ENABLE_ASH_ALIAS
        { BUILTIN_REGULAR       "unalias" , unaliascmd },
@@ -10023,6 +10111,14 @@ find_builtin(const char *name)
        return bp;
 }
 
+#if EDITING_HAS_get_exe_name
+static const char * FAST_FUNC
+get_builtin_name(int i)
+{
+       return /*i >= 0 &&*/ i < ARRAY_SIZE(builtintab) ? builtintab[i].name + 1 : NULL;
+}
+#endif
+
 /*
  * Execute a simple command.
  */
@@ -10046,7 +10142,7 @@ static int
 evalcommand(union node *cmd, int flags)
 {
        static const struct builtincmd null_bltin = {
-               "\0\0", bltincmd /* why three NULs? */
+               BUILTIN_REGULAR "", bltincmd
        };
        struct localvar_list *localvar_stop;
        struct parsefile *file_stop;
@@ -10056,15 +10152,19 @@ evalcommand(union node *cmd, int flags)
        struct arglist varlist;
        char **argv;
        int argc;
+       struct strlist *osp;
        const struct strlist *sp;
        struct cmdentry cmdentry;
        struct job *jp;
        char *lastarg;
        const char *path;
        int spclbltin;
+       int cmd_flag;
        int status;
        char **nargv;
        smallint cmd_is_exec;
+       int vflags;
+       int vlocal;
 
        errlinno = lineno = cmd->ncmd.linno;
        if (funcline)
@@ -10072,7 +10172,6 @@ evalcommand(union node *cmd, int flags)
 
        /* First expand the arguments. */
        TRACE(("evalcommand(0x%lx, %d) called\n", (long)cmd, flags));
-       localvar_stop = pushlocalvars();
        file_stop = g_parsefile;
        back_exitstatus = 0;
 
@@ -10083,28 +10182,58 @@ evalcommand(union node *cmd, int flags)
        arglist.lastp = &arglist.list;
        *arglist.lastp = NULL;
 
+       cmd_flag = 0;
+       cmd_is_exec = 0;
+       spclbltin = -1;
+       vflags = 0;
+       vlocal = 0;
+       path = NULL;
+
        argc = 0;
-       if (cmd->ncmd.args) {
-               struct builtincmd *bcmd;
-               smallint pseudovarflag;
+       argp = cmd->ncmd.args;
+       osp = fill_arglist(&arglist, &argp);
+       if (osp) {
+               int pseudovarflag = 0;
 
-               bcmd = find_builtin(cmd->ncmd.args->narg.text);
-               pseudovarflag = bcmd && IS_BUILTIN_ASSIGN(bcmd);
+               for (;;) {
+                       find_command(arglist.list->text, &cmdentry,
+                                       cmd_flag | DO_REGBLTIN, pathval());
 
-               for (argp = cmd->ncmd.args; argp; argp = argp->narg.next) {
-                       struct strlist **spp;
+                       vlocal++;
 
-                       spp = arglist.lastp;
-                       if (pseudovarflag && isassignment(argp->narg.text))
-                               expandarg(argp, &arglist, EXP_VARTILDE);
-                       else
-                               expandarg(argp, &arglist, EXP_FULL | EXP_TILDE);
+                       /* implement bltin and command here */
+                       if (cmdentry.cmdtype != CMDBUILTIN)
+                               break;
+
+                       pseudovarflag = IS_BUILTIN_ASSIGN(cmdentry.u.cmd);
+                       if (spclbltin < 0) {
+                               spclbltin = IS_BUILTIN_SPECIAL(cmdentry.u.cmd);
+                               vlocal = !spclbltin;
+                       }
+                       cmd_is_exec = cmdentry.u.cmd == EXECCMD;
+                       if (cmdentry.u.cmd != COMMANDCMD)
+                               break;
 
-                       for (sp = *spp; sp; sp = sp->next)
-                               argc++;
+                       cmd_flag = parse_command_args(&arglist, &argp, &path);
+                       if (!cmd_flag)
+                               break;
                }
+
+               for (; argp; argp = argp->narg.next)
+                       expandarg(argp, &arglist,
+                                       pseudovarflag &&
+                                       isassignment(argp->narg.text) ?
+                                       EXP_VARTILDE : EXP_FULL | EXP_TILDE);
+
+               for (sp = arglist.list; sp; sp = sp->next)
+                       argc++;
+
+               if (cmd_is_exec && argc > 1)
+                       vflags = VEXPORT;
        }
 
+       localvar_stop = pushlocalvars(vlocal);
+
        /* Reserve one extra spot at the front for shellexec. */
        nargv = stalloc(sizeof(char *) * (argc + 2));
        argv = ++nargv;
@@ -10132,23 +10261,27 @@ evalcommand(union node *cmd, int flags)
        }
        status = redirectsafe(cmd->ncmd.redirect, REDIR_PUSH | REDIR_SAVEFD2);
 
-       path = vpath.var_text;
+       if (status) {
+ bail:
+               exitstatus = status;
+
+               /* We have a redirection error. */
+               if (spclbltin > 0)
+                       raise_exception(EXERROR);
+
+               goto out;
+       }
+
        for (argp = cmd->ncmd.assign; argp; argp = argp->narg.next) {
                struct strlist **spp;
-               char *p;
 
                spp = varlist.lastp;
                expandarg(argp, &varlist, EXP_VARTILDE);
 
-               mklocal((*spp)->text);
-
-               /*
-                * Modify the command lookup path, if a PATH= assignment
-                * is present
-                */
-               p = (*spp)->text;
-               if (varcmp(p, path) == 0)
-                       path = p;
+               if (vlocal)
+                       mklocal((*spp)->text, VEXPORT);
+               else
+                       setvareq((*spp)->text, vflags);
        }
 
        /* Print the command if xflag is set. */
@@ -10187,62 +10320,23 @@ evalcommand(union node *cmd, int flags)
                safe_write(preverrout_fd, "\n", 1);
        }
 
-       cmd_is_exec = 0;
-       spclbltin = -1;
-
        /* Now locate the command. */
-       if (argc) {
-               int cmd_flag = DO_ERR;
-#if ENABLE_ASH_CMDCMD
-               const char *oldpath = path + 5;
-#endif
-               path += 5;
-               for (;;) {
-                       find_command(argv[0], &cmdentry, cmd_flag, path);
-                       if (cmdentry.cmdtype == CMDUNKNOWN) {
-                               flush_stdout_stderr();
-                               status = 127;
-                               goto bail;
-                       }
-
-                       /* implement bltin and command here */
-                       if (cmdentry.cmdtype != CMDBUILTIN)
-                               break;
-                       if (spclbltin < 0)
-                               spclbltin = IS_BUILTIN_SPECIAL(cmdentry.u.cmd);
-                       if (cmdentry.u.cmd == EXECCMD)
-                               cmd_is_exec = 1;
-#if ENABLE_ASH_CMDCMD
-                       if (cmdentry.u.cmd == COMMANDCMD) {
-                               path = oldpath;
-                               nargv = parse_command_args(argv, &path);
-                               if (!nargv)
-                                       break;
-                               /* It's "command [-p] PROG ARGS" (that is, no -Vv).
-                                * nargv => "PROG". path is updated if -p.
-                                */
-                               argc -= nargv - argv;
-                               argv = nargv;
-                               cmd_flag |= DO_NOFUNC;
-                       } else
-#endif
-                               break;
-               }
+       if (cmdentry.cmdtype != CMDBUILTIN
+        || !(IS_BUILTIN_REGULAR(cmdentry.u.cmd))
+       ) {
+               path = path ? path : pathval();
+               find_command(argv[0], &cmdentry, cmd_flag | DO_ERR, path);
        }
 
-       if (status) {
- bail:
-               exitstatus = status;
-
-               /* We have a redirection error. */
-               if (spclbltin > 0)
-                       raise_exception(EXERROR);
-
-               goto out;
-       }
+       jp = NULL;
 
        /* Execute the command. */
        switch (cmdentry.cmdtype) {
+       case CMDUNKNOWN:
+               status = 127;
+               flush_stdout_stderr();
+               goto bail;
+
        default: {
 
 #if ENABLE_FEATURE_SH_STANDALONE \
@@ -10269,7 +10363,7 @@ evalcommand(union node *cmd, int flags)
                         * and/or wait for user input ineligible for NOFORK:
                         * for example, "yes" or "rm" (rm -i waits for input).
                         */
-                       status = run_nofork_applet(applet_no, argv);
+                       exitstatus = run_nofork_applet(applet_no, argv);
                        environ = sv_environ;
                        /*
                         * Try enabling NOFORK for "yes" applet.
@@ -10295,8 +10389,6 @@ evalcommand(union node *cmd, int flags)
                        jp = makejob(/*cmd,*/ 1);
                        if (forkshell(jp, cmd, FORK_FG) != 0) {
                                /* parent */
-                               status = waitforjob(jp);
-                               INT_ON;
                                TRACE(("forked child exited with %d\n", status));
                                break;
                        }
@@ -10304,43 +10396,27 @@ evalcommand(union node *cmd, int flags)
                        FORCE_INT_ON;
                        /* fall through to exec'ing external program */
                }
-               listsetvar(varlist.list, VEXPORT|VSTACK);
                shellexec(argv[0], argv, path, cmdentry.u.index);
                /* NOTREACHED */
        } /* default */
        case CMDBUILTIN:
-               if (spclbltin > 0 || argc == 0) {
-                       poplocalvars(1);
-                       if (cmd_is_exec && argc > 1)
-                               listsetvar(varlist.list, VEXPORT);
-               }
-
-               /* Tight loop with builtins only:
-                * "while kill -0 $child; do true; done"
-                * will never exit even if $child died, unless we do this
-                * to reap the zombie and make kill detect that it's gone: */
-               dowait(DOWAIT_NONBLOCK, NULL);
-
-               if (evalbltin(cmdentry.u.cmd, argc, argv, flags)) {
-                       if (exception_type == EXERROR && spclbltin <= 0) {
-                               FORCE_INT_ON;
-                               goto readstatus;
-                       }
+               if (evalbltin(cmdentry.u.cmd, argc, argv, flags)
+                && !(exception_type == EXERROR && spclbltin <= 0)
+               ) {
  raise:
                        longjmp(exception_handler->loc, 1);
                }
-               goto readstatus;
+               break;
 
        case CMDFUNCTION:
-               /* See above for the rationale */
-               dowait(DOWAIT_NONBLOCK, NULL);
                if (evalfun(cmdentry.u.func, argc, argv, flags))
                        goto raise;
- readstatus:
-               status = exitstatus;
                break;
        } /* switch */
 
+       status = waitforjob(jp);
+       FORCE_INT_ON;
+
  out:
        if (cmd->ncmd.redirect)
                popredir(/*drop:*/ cmd_is_exec);
@@ -10812,6 +10888,12 @@ struct synstack {
        struct synstack *next;
 };
 
+static int
+pgetc_top(struct synstack *stack)
+{
+       return stack->syntax == SQSYNTAX ? pgetc() : pgetc_eatbnl();
+}
+
 static void
 synstack_push(struct synstack **stack, struct synstack *next, int syntax)
 {
@@ -10989,8 +11071,12 @@ chkmail(void)
        mpath = mpathset() ? mpathval() : mailval();
        new_hash = 0;
        for (;;) {
-               p = path_advance(&mpath, nullstr);
-               if (p == NULL)
+               int len;
+
+               len = padvance_magic(&mpath, nullstr, 2);
+               if (!len)
+                       break;
+               p = stackblock();
                        break;
                if (*p == '\0')
                        continue;
@@ -12177,7 +12263,7 @@ readtoken1(int c, int syntax, char *eofmark, int striptabs)
                        }
                        USTPUTC(c, out);
                        nlprompt();
-                       c = pgetc();
+                       c = pgetc_top(synstack);
                        goto loop;              /* continue outer loop */
                case CWORD:
                        USTPUTC(c, out);
@@ -12209,8 +12295,6 @@ readtoken1(int c, int syntax, char *eofmark, int striptabs)
                                USTPUTC(CTLESC, out);
                                USTPUTC('\\', out);
                                pungetc();
-                       } else if (c == '\n') {
-                               nlprompt();
                        } else {
                                if (pssyntax && c == '$') {
                                        USTPUTC(CTLESC, out);
@@ -12330,7 +12414,7 @@ readtoken1(int c, int syntax, char *eofmark, int striptabs)
                        IF_ASH_ALIAS(if (c != PEOA))
                                USTPUTC(c, out);
                }
-               c = pgetc();
+               c = pgetc_top(synstack);
        } /* for (;;) */
  endword:
 
@@ -12393,7 +12477,13 @@ checkend: {
                for (p = eofmark; STPUTC(c, out), *p; p++) {
                        if (c != *p)
                                goto more_heredoc;
-
+                       /* FIXME: fails for backslash-newlined terminator:
+                        * cat <<EOF
+                        * ...
+                        * EO\
+                        * F
+                        * (see heredoc_bkslash_newline2.tests)
+                        */
                        c = pgetc_without_PEOA();
                }
 
@@ -12564,7 +12654,7 @@ parsesub: {
                        do {
                                STPUTC(c, out);
                                c = pgetc_eatbnl();
-                       } while (isdigit(c));
+                       } while (!subtype && isdigit(c));
                } else if (c != '}') {
                        /* $[{[#]]<specialchar>[}] */
                        int cc = c;
@@ -12695,6 +12785,7 @@ parsebackq: {
        union node *n;
        char *str;
        size_t savelen;
+       struct heredoc *saveheredoclist;
        smallint saveprompt = 0;
 
        str = NULL;
@@ -12770,6 +12861,9 @@ parsebackq: {
        *nlpp = stzalloc(sizeof(**nlpp));
        /* (*nlpp)->next = NULL; - stzalloc did it */
 
+       saveheredoclist = heredoclist;
+       heredoclist = NULL;
+
        if (oldstyle) {
                saveprompt = doprompt;
                doprompt = 0;
@@ -12779,21 +12873,22 @@ parsebackq: {
 
        if (oldstyle)
                doprompt = saveprompt;
-       else if (readtoken() != TRP)
-               raise_error_unexpected_syntax(TRP);
+       else {
+               if (readtoken() != TRP)
+                       raise_error_unexpected_syntax(TRP);
+               setinputstring(nullstr);
+       }
+
+       parseheredoc();
+       heredoclist = saveheredoclist;
 
        (*nlpp)->n = n;
-       if (oldstyle) {
-               /*
-                * Start reading from old file again, ignoring any pushed back
-                * tokens left from the backquote parsing
-                */
-               popfile();
+       /* Start reading from old file again. */
+       popfile();
+       /* Ignore any pushed back tokens left from the backquote parsing. */
+       if (oldstyle)
                tokpushback = 0;
-       }
-       while (stackblocksize() <= savelen)
-               growstackblock();
-       STARTSTACKSTR(out);
+       out = growstackto(savelen + 1);
        if (str) {
                memcpy(out, str, savelen);
                STADJUST(savelen, out);
@@ -13070,8 +13165,10 @@ parseheredoc(void)
        while (here) {
                tokpushback = 0;
                setprompt_if(needprompt, 2);
-               readtoken1(pgetc(), here->here->type == NHERE ? SQSYNTAX : DQSYNTAX,
-                               here->eofmark, here->striptabs);
+               if (here->here->type == NHERE)
+                       readtoken1(pgetc(), SQSYNTAX, here->eofmark, here->striptabs);
+               else
+                       readtoken1(pgetc_eatbnl(), DQSYNTAX, here->eofmark, here->striptabs);
                n = stzalloc(sizeof(struct narg));
                n->narg.type = NARG;
                /*n->narg.next = NULL; - stzalloc did it */
@@ -13092,29 +13189,27 @@ expandstr(const char *ps, int syntax_type)
        volatile int saveint;
        struct jmploc *volatile savehandler = exception_handler;
        struct jmploc jmploc;
+       const char *volatile result;
+       int err;
 
        /* XXX Fix (char *) cast. */
        setinputstring((char *)ps);
 
        saveprompt = doprompt;
        doprompt = 0;
+       result = ps;
+
+       SAVE_INT(saveint);
+       err = setjmp(jmploc.loc);
+       if (err)
+               goto out;
 
        /* readtoken1() might die horribly.
         * Try a prompt with syntactically wrong command:
         * PS1='$(date "+%H:%M:%S) > '
         */
-       SAVE_INT(saveint);
-       if (setjmp(jmploc.loc) == 0) {
-               exception_handler = &jmploc;
-               readtoken1(pgetc(), syntax_type, FAKEEOFMARK, 0);
-       }
-       exception_handler = savehandler;
-       RESTORE_INT(saveint);
-
-       doprompt = saveprompt;
-
-       /* Try: PS1='`xxx(`' */
-       unwindfiles(file_stop);
+       exception_handler = &jmploc;
+       readtoken1(pgetc(), syntax_type, FAKEEOFMARK, 0);
 
        n.narg.type = NARG;
        n.narg.next = NULL;
@@ -13124,17 +13219,20 @@ expandstr(const char *ps, int syntax_type)
        /* expandarg() might fail too:
         * PS1='$((123+))'
         */
-       SAVE_INT(saveint);
-       if (setjmp(jmploc.loc) == 0) {
-               exception_handler = &jmploc;
-               expandarg(&n, NULL, EXP_QUOTED);
-       } else if (exception_type == EXEXIT) {
-               exitshell();
-       }
+       expandarg(&n, NULL, EXP_QUOTED);
+       result = stackblock();
+
+out:
        exception_handler = savehandler;
+       if (err && exception_type != EXERROR)
+               longjmp(exception_handler->loc, 1);
        RESTORE_INT(saveint);
 
-       return stackblock();
+       doprompt = saveprompt;
+       /* Try: PS1='`xxx(`' */
+       unwindfiles(file_stop);
+
+       return result;
 }
 
 static inline int
@@ -13263,8 +13361,12 @@ cmdloop(int top)
                        if (!top || numeof >= 50)
                                break;
                        if (!stoppedjobs()) {
-                               if (!Iflag)
+                               if (!Iflag) {
+                                       if (iflag) {
+                                               newline_and_flush(stderr);
+                                       }
                                        break;
+                               }
                                out2str("\nUse \"exit\" to leave shell.\n");
                        }
                        numeof++;
@@ -13282,7 +13384,7 @@ cmdloop(int top)
                skip = evalskip;
 
                if (skip) {
-                       evalskip &= ~SKIPFUNC;
+                       evalskip &= ~(SKIPFUNC | SKIPFUNCDEF);
                        break;
                }
        }
@@ -13294,33 +13396,32 @@ cmdloop(int top)
  * search for the file, which is necessary to find sub-commands.
  */
 static char *
-find_dot_file(char *name)
+find_dot_file(char *basename)
 {
        char *fullname;
        const char *path = pathval();
        struct stat statb;
+       int len;
 
        /* don't try this for absolute or relative paths */
-       if (strchr(name, '/'))
-               return name;
+       if (strchr(basename, '/'))
+               return basename;
 
-       while ((fullname = path_advance(&path, name)) != NULL) {
-               if ((stat(fullname, &statb) == 0) && S_ISREG(statb.st_mode)) {
-                       /*
-                        * Don't bother freeing here, since it will
-                        * be freed by the caller.
-                        */
-                       return fullname;
+       while ((len = padvance(&path, basename)) >= 0) {
+               fullname = stackblock();
+               if ((!pathopt || *pathopt == 'f')
+                && !stat(fullname, &statb) && S_ISREG(statb.st_mode)
+               ) {
+                       /* This will be freed by the caller. */
+                       return stalloc(len);
                }
-               if (fullname != name)
-                       stunalloc(fullname);
        }
        /* not found in PATH */
 
 #if ENABLE_ASH_BASH_SOURCE_CURDIR
-       return name;
+       return basename;
 #else
-       ash_msg_and_raise_error("%s: not found", name);
+       ash_msg_and_raise_error("%s: not found", basename);
        /* NOTREACHED */
 #endif
 }
@@ -13386,8 +13487,10 @@ exitcmd(int argc UNUSED_PARAM, char **argv)
 {
        if (stoppedjobs())
                return 0;
+
        if (argv[1])
-               exitstatus = number(argv[1]);
+               savestatus = number(argv[1]);
+
        raise_exception(EXEXIT);
        /* NOTREACHED */
 }
@@ -13421,6 +13524,7 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
        int e;
        int updatetbl;
        struct builtincmd *bcmd;
+       int len;
 
        /* If name contains a slash, don't use PATH or hash table */
        if (strchr(name, '/') != NULL) {
@@ -13442,11 +13546,8 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
 /* #if ENABLE_FEATURE_SH_STANDALONE... moved after builtin check */
 
        updatetbl = (path == pathval());
-       if (!updatetbl) {
+       if (!updatetbl)
                act |= DO_ALTPATH;
-               if (strstr(path, "%builtin") != NULL)
-                       act |= DO_ALTBLTIN;
-       }
 
        /* If name is in the table, check answer will be ok */
        cmdp = cmdlookup(name, 0);
@@ -13459,16 +13560,19 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
                        abort();
 #endif
                case CMDNORMAL:
-                       bit = DO_ALTPATH;
+                       bit = DO_ALTPATH | DO_REGBLTIN;
                        break;
                case CMDFUNCTION:
                        bit = DO_NOFUNC;
                        break;
                case CMDBUILTIN:
-                       bit = DO_ALTBLTIN;
+                       bit = IS_BUILTIN_REGULAR(cmdp->param.cmd) ? 0 : DO_REGBLTIN;
                        break;
                }
                if (act & bit) {
+                       if (act & bit & DO_REGBLTIN)
+                               goto fail;
+
                        updatetbl = 0;
                        cmdp = NULL;
                } else if (cmdp->rehash == 0)
@@ -13481,14 +13585,15 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
        if (bcmd) {
                if (IS_BUILTIN_REGULAR(bcmd))
                        goto builtin_success;
-               if (act & DO_ALTPATH) {
-                       if (!(act & DO_ALTBLTIN))
-                               goto builtin_success;
-               } else if (builtinloc <= 0) {
+               if (act & DO_ALTPATH)
+                       goto builtin_success;
+               if (builtinloc <= 0)
                        goto builtin_success;
-               }
        }
 
+       if (act & DO_REGBLTIN)
+               goto fail;
+
 #if ENABLE_FEATURE_SH_STANDALONE
        {
                int applet_no = find_applet_by_name(name);
@@ -13512,20 +13617,20 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
        e = ENOENT;
        idx = -1;
  loop:
-       while ((fullname = path_advance(&path, name)) != NULL) {
-               stunalloc(fullname);
-               /* NB: code below will still use fullname
-                * despite it being "unallocated" */
+       while ((len = padvance(&path, name)) >= 0) {
+               const char *lpathopt = pathopt;
+
+               fullname = stackblock();
                idx++;
-               if (pathopt) {
-                       if (prefix(pathopt, "builtin")) {
+               if (lpathopt) {
+                       if (*lpathopt == 'b') {
                                if (bcmd)
                                        goto builtin_success;
                                continue;
-                       }
-                       if ((act & DO_NOFUNC)
-                        || !prefix(pathopt, "func")
-                       ) {     /* ignore unimplemented options */
+                       } else if (!(act & DO_NOFUNC)) {
+                               /* handled below */
+                       } else {
+                               /* ignore unimplemented options */
                                continue;
                        }
                }
@@ -13548,8 +13653,8 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
                e = EACCES;     /* if we fail, this will be the error */
                if (!S_ISREG(statb.st_mode))
                        continue;
-               if (pathopt) {          /* this is a %func directory */
-                       stalloc(strlen(fullname) + 1);
+               if (lpathopt) {          /* this is a %func directory */
+                       stalloc(len);
                        /* NB: stalloc will return space pointed by fullname
                         * (because we don't have any intervening allocations
                         * between stunalloc above and this stalloc) */
@@ -13592,6 +13697,7 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
 #endif
                ash_msg("%s: %s", name, errmsg(e, "not found"));
        }
+ fail:
        entry->cmdtype = CMDUNKNOWN;
        return;
 
@@ -14039,6 +14145,47 @@ ulimitcmd(int argc UNUSED_PARAM, char **argv)
 
 /* ============ main() and helpers */
 
+/*
+ * This routine is called when an error or an interrupt occurs in an
+ * interactive shell and control is returned to the main command loop
+ * but prior to exitshell.
+ */
+static void
+exitreset(void)
+{
+       /* from eval.c: */
+       if (savestatus >= 0) {
+               if (exception_type == EXEXIT || evalskip == SKIPFUNCDEF)
+                       exitstatus = savestatus;
+               savestatus = -1;
+       }
+       evalskip = 0;
+       loopnest = 0;
+
+       /* from expand.c: */
+       ifsfree();
+
+       /* from redir.c: */
+       unwindredir(NULL);
+}
+
+/*
+ * This routine is called when an error or an interrupt occurs in an
+ * interactive shell and control is returned to the main command loop.
+ * (In dash, this function is auto-generated by build machinery).
+ */
+static void
+reset(void)
+{
+       /* from input.c: */
+       g_parsefile->left_in_buffer = 0;
+       g_parsefile->left_in_line = 0;      /* clear input buffer */
+       popallfiles();
+
+       /* from var.c: */
+       unwindlocalvars(NULL);
+}
+
 /*
  * Called to exit the shell.
  */
@@ -14047,34 +14194,32 @@ exitshell(void)
 {
        struct jmploc loc;
        char *p;
-       int status;
 
 #if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
        if (line_input_state)
                save_history(line_input_state);
 #endif
-       status = exitstatus;
-       TRACE(("pid %d, exitshell(%d)\n", getpid(), status));
-       if (setjmp(loc.loc)) {
-               if (exception_type == EXEXIT)
-                       status = exitstatus;
+       savestatus = exitstatus;
+       TRACE(("pid %d, exitshell(%d)\n", getpid(), savestatus));
+       if (setjmp(loc.loc))
                goto out;
-       }
        exception_handler = &loc;
        p = trap[0];
        if (p) {
                trap[0] = NULL;
                evalskip = 0;
                evalstring(p, 0);
+               evalskip = SKIPFUNCDEF;
                /*free(p); - we'll exit soon */
        }
  out:
+       exitreset();
        /* dash wraps setjobctl(0) in "if (setjmp(loc.loc) == 0) {...}".
         * our setjobctl(0) does not panic if tcsetpgrp fails inside it.
         */
        setjobctl(0);
        flush_stdout_stderr();
-       _exit(status);
+       _exit(exitstatus);
        /* NOTREACHED */
 }
 
@@ -14089,11 +14234,6 @@ init(void)
        sigmode[SIGCHLD - 1] = S_DFL; /* ensure we install handler even if it is SIG_IGNed */
        setsignal(SIGCHLD);
 
-       /* bash re-enables SIGHUP which is SIG_IGNed on entry.
-        * Try: "trap '' HUP; bash; echo RET" and type "kill -HUP $$"
-        */
-       signal(SIGHUP, SIG_DFL);
-
        {
                char **envp;
                const char *p;
@@ -14239,33 +14379,6 @@ read_profile(const char *name)
        popfile();
 }
 
-/*
- * This routine is called when an error or an interrupt occurs in an
- * interactive shell and control is returned to the main command loop.
- * (In dash, this function is auto-generated by build machinery).
- */
-static void
-reset(void)
-{
-       /* from eval.c: */
-       evalskip = 0;
-       loopnest = 0;
-
-       /* from expand.c: */
-       ifsfree();
-
-       /* from input.c: */
-       g_parsefile->left_in_buffer = 0;
-       g_parsefile->left_in_line = 0;      /* clear input buffer */
-       popallfiles();
-
-       /* from redir.c: */
-       unwindredir(NULL);
-
-       /* from var.c: */
-       unwindlocalvars(NULL);
-}
-
 #if PROFILE
 static short profile_buf[16384];
 extern int etext();
@@ -14309,13 +14422,16 @@ int ash_main(int argc UNUSED_PARAM, char **argv)
                smallint e;
                smallint s;
 
-               reset();
+               exitreset();
 
                e = exception_type;
                s = state;
-               if (e == EXEXIT || s == 0 || iflag == 0 || shlvl) {
+               if (e == EXEND || e == EXEXIT || s == 0 || iflag == 0 || shlvl) {
                        exitshell();
                }
+
+               reset();
+
                if (e == EXINT) {
                        newline_and_flush(stderr);
                }
@@ -14415,6 +14531,14 @@ int ash_main(int argc UNUSED_PARAM, char **argv)
                }
 #endif
  state4: /* XXX ??? - why isn't this before the "if" statement */
+
+               /* Interactive bash re-enables SIGHUP which is SIG_IGNed on entry.
+                * Try:
+                * trap '' hup; bash; echo RET  # type "kill -hup $$", see SIGHUP having effect
+                * trap '' hup; bash -c 'kill -hup $$; echo ALIVE'  # here SIGHUP is SIG_IGNed
+                */
+               signal(SIGHUP, SIG_DFL);
+
                cmdloop(1);
        }
 #if PROFILE