ash: parser: Fix old-style command substitution here-document crash
[oweals/busybox.git] / shell / ash.c
index 2b13786943735bfe93f22ac425d4a33bf1dd7589..83cac3fb0a5eb315bacc52a6930829a717a298ba 100644 (file)
@@ -404,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 */
 
@@ -1678,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;
@@ -1736,16 +1737,15 @@ static void *
 growstackstr(void)
 {
        size_t len = stackblocksize();
-       growstackblock();
+       growstackblock(0);
        return (char *)stackblock() + len;
 }
 
 static char *
 growstackto(size_t len)
 {
-       while (stackblocksize() < len)
-               growstackblock();
-
+       if (stackblocksize() < len)
+               growstackblock(len);
        return stackblock();
 }
 
@@ -2476,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.
  */
@@ -3810,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
@@ -4309,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)
@@ -4331,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;
@@ -4432,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;
 
@@ -4460,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)
@@ -4548,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) {
@@ -4666,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
@@ -4705,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: ;
@@ -5261,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 */
@@ -5272,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;
@@ -5357,48 +5349,39 @@ waitforjob(struct job *jp)
 
        TRACE(("waitforjob(%%%d) called\n", jp ? jobno(jp) : 0));
 
-       if (!jp) {
-               int pid = got_sigchld;
-
-               while (pid > 0)
-                       pid = dowait(DOWAIT_NONBLOCK, NULL);
-
+       /* 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;
-       }
-
-       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);
-       }
 
        st = getstatus(jp);
 #if JOBS
@@ -6047,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
  */
@@ -6057,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
@@ -6087,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
@@ -6364,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.
@@ -6465,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;
 
@@ -6482,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 '/':
@@ -6503,7 +6485,7 @@ exptilde(char *startp, char *p, int flags)
        if (!home)
                goto lose;
        *p = c;
-       strtodest(home, SQSYNTAX, quotes);
+       strtodest(home, flag | EXP_QUOTED);
        return p;
  lose:
        *p = c;
@@ -6603,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;
@@ -6617,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;
@@ -6701,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);
@@ -7326,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 '$':
@@ -7347,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);
@@ -7396,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;
@@ -7427,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();
@@ -7513,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;
        }
 
@@ -8111,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 *);
 
@@ -8256,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 */
 }
 
@@ -8717,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 {
@@ -8744,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
@@ -9089,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 */
@@ -9146,7 +9148,8 @@ dotrap(void)
                if (!p)
                        continue;
                evalstring(p, 0);
-               exitstatus = status;
+               if (evalskip != SKIPFUNC)
+                       exitstatus = status;
        }
 
        savestatus = last_status;
@@ -9287,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"));
@@ -9718,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
@@ -9776,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;
 }
 
@@ -9788,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;
@@ -9825,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;
@@ -9837,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);
@@ -9863,7 +9871,7 @@ localcmd(int argc UNUSED_PARAM, char **argv)
 
        argv = argptr;
        while ((name = *argv++) != NULL) {
-               mklocal(name);
+               mklocal(name, 0);
        }
        return 0;
 }
@@ -9921,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[] */
@@ -10123,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;
@@ -10133,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)
@@ -10149,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;
 
@@ -10160,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;
 
-                       for (sp = *spp; sp; sp = sp->next)
-                               argc++;
+                       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;
+
+                       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;
@@ -10209,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. */
@@ -10264,64 +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 (status) {
- bail:
-               exitstatus = status;
-
-               /* We have a redirection error. */
-               if (spclbltin > 0)
-                       raise_exception(EXERROR);
-
-               goto out;
+       if (cmdentry.cmdtype != CMDBUILTIN
+        || !(IS_BUILTIN_REGULAR(cmdentry.u.cmd))
+       ) {
+               path = path ? path : pathval();
+               find_command(argv[0], &cmdentry, cmd_flag | DO_ERR, path);
        }
 
        jp = NULL;
 
        /* Execute the command. */
        switch (cmdentry.cmdtype) {
+       case CMDUNKNOWN:
+               status = 127;
+               flush_stdout_stderr();
+               goto bail;
+
        default: {
 
 #if ENABLE_FEATURE_SH_STANDALONE \
@@ -10348,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.
@@ -10381,16 +10396,10 @@ 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);
-               }
                if (evalbltin(cmdentry.u.cmd, argc, argv, flags)
                 && !(exception_type == EXERROR && spclbltin <= 0)
                ) {
@@ -12868,9 +12877,9 @@ parsebackq: {
                if (readtoken() != TRP)
                        raise_error_unexpected_syntax(TRP);
                setinputstring(nullstr);
-               parseheredoc();
        }
 
+       parseheredoc();
        heredoclist = saveheredoclist;
 
        (*nlpp)->n = n;
@@ -13375,7 +13384,7 @@ cmdloop(int top)
                skip = evalskip;
 
                if (skip) {
-                       evalskip &= ~SKIPFUNC;
+                       evalskip &= ~(SKIPFUNC | SKIPFUNCDEF);
                        break;
                }
        }
@@ -13537,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);
@@ -13554,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 = IS_BUILTIN_REGULAR(cmdp->param.cmd) ? 0 : 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)
@@ -13576,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);
@@ -13687,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;
 
@@ -14134,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.
  */
@@ -14157,15 +14209,17 @@ exitshell(void)
                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(savestatus);
+       _exit(exitstatus);
        /* NOTREACHED */
 }
 
@@ -14180,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;
@@ -14330,46 +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
- * but prior to exitshell.
- */
-static void
-exitreset(void)
-{
-       /* from eval.c: */
-       evalskip = 0;
-       loopnest = 0;
-       if (savestatus >= 0) {
-               exitstatus = savestatus;
-               savestatus = -1;
-       }
-
-       /* 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);
-}
-
 #if PROFILE
 static short profile_buf[16384];
 extern int etext();
@@ -14417,7 +14426,7 @@ int ash_main(int argc UNUSED_PARAM, char **argv)
 
                e = exception_type;
                s = state;
-               if (e == EXEXIT || s == 0 || iflag == 0 || shlvl) {
+               if (e == EXEND || e == EXEXIT || s == 0 || iflag == 0 || shlvl) {
                        exitshell();
                }
 
@@ -14522,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