ash: parser: Do not push token back before parseheredoc
[oweals/busybox.git] / shell / ash.c
index 30354219797623308475ddeef4a27a5eefabe244..5fb67c0fa18f316f23ec8643b24e328a05b9c3dc 100644 (file)
@@ -16,7 +16,7 @@
  * Licensed under GPLv2 or later, see file LICENSE in this source tree.
  */
 //config:config ASH
-//config:      bool "ash (77 kb)"
+//config:      bool "ash (78 kb)"
 //config:      default y
 //config:      depends on !NOMMU
 //config:      help
 #include <sys/times.h>
 #include <sys/utsname.h> /* for setting $HOSTNAME */
 #include "busybox.h" /* for applet_names */
+#if ENABLE_FEATURE_SH_EMBEDDED_SCRIPTS
+# include "embedded_scripts.h"
+#else
+# define NUM_SCRIPTS 0
+#endif
 
 /* So far, all bash compat is controlled by one config option */
 /* Separate defines document which part of code implements what */
 #define IF_BASH_PATTERN_SUBST       IF_ASH_BASH_COMPAT
 #define    BASH_SUBSTR          ENABLE_ASH_BASH_COMPAT
 #define IF_BASH_SUBSTR              IF_ASH_BASH_COMPAT
-/* [[ EXPR ]] */
+/* BASH_TEST2: [[ EXPR ]]
+ * Status of [[ support:
+ * We replace && and || with -a and -o
+ * TODO:
+ * singleword+noglob expansion:
+ *   v='a b'; [[ $v = 'a b' ]]; echo 0:$?
+ *   [[ /bin/n* ]]; echo 0:$?
+ * -a/-o are not AND/OR ops! (they are just strings)
+ * quoting needs to be considered (-f is an operator, "-f" and ""-f are not; etc)
+ * = is glob match operator, not equality operator: STR = GLOB
+ * (in GLOB, quoting is significant on char-by-char basis: a*cd"*")
+ * == same as =
+ * add =~ regex match operator: STR =~ REGEX
+ */
 #define    BASH_TEST2           (ENABLE_ASH_BASH_COMPAT * ENABLE_ASH_TEST)
 #define    BASH_SOURCE          ENABLE_ASH_BASH_COMPAT
 #define    BASH_PIPEFAIL        ENABLE_ASH_BASH_COMPAT
 #define    BASH_HOSTNAME_VAR    ENABLE_ASH_BASH_COMPAT
+#define    BASH_EPOCH_VARS      ENABLE_ASH_BASH_COMPAT
 #define    BASH_SHLVL_VAR       ENABLE_ASH_BASH_COMPAT
 #define    BASH_XTRACEFD        ENABLE_ASH_BASH_COMPAT
 #define    BASH_READ_D          ENABLE_ASH_BASH_COMPAT
 #define IF_BASH_READ_D              IF_ASH_BASH_COMPAT
+#define    BASH_WAIT_N          ENABLE_ASH_BASH_COMPAT
 
 #if defined(__ANDROID_API__) && __ANDROID_API__ <= 24
 /* Bionic at least up to version 24 has no glob() */
@@ -295,10 +315,18 @@ static const char *const optletters_optnames[] = {
        "e"   "errexit",
        "f"   "noglob",
        "I"   "ignoreeof",
-       "i"   "interactive",
+/* The below allowed this invocation:
+ * ash -c 'set -i; echo $-; sleep 5; echo $-'
+ * to be ^C-ed and get to interactive ash prompt.
+ * bash does not support such "set -i".
+ * In our code, this is denoted by empty long name:
+ */
+       "i"   "",
        "m"   "monitor",
        "n"   "noexec",
-       "s"   "stdin",
+/* Ditto: bash has no "set -s" */
+       "s"   "",
+       "c"   "",
        "x"   "xtrace",
        "v"   "verbose",
        "C"   "noclobber",
@@ -314,6 +342,20 @@ static const char *const optletters_optnames[] = {
        ,"\0"  "debug"
 #endif
 };
+//bash 4.4.23 also has these opts (with these defaults):
+//braceexpand           on
+//emacs                 on
+//errtrace              off
+//functrace             off
+//hashall               on
+//histexpand            off
+//history               on
+//interactive-comments  on
+//keyword               off
+//onecmd                off
+//physical              off
+//posix                 off
+//privileged            off
 
 #define optletters(n)  optletters_optnames[n][0]
 #define optnames(n)   (optletters_optnames[n] + 1)
@@ -342,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;
@@ -361,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 */
 
@@ -377,21 +420,22 @@ struct globals_misc {
 #define mflag optlist[4]
 #define nflag optlist[5]
 #define sflag optlist[6]
-#define xflag optlist[7]
-#define vflag optlist[8]
-#define Cflag optlist[9]
-#define aflag optlist[10]
-#define bflag optlist[11]
-#define uflag optlist[12]
-#define viflag optlist[13]
+#define cflag optlist[7]
+#define xflag optlist[8]
+#define vflag optlist[9]
+#define Cflag optlist[10]
+#define aflag optlist[11]
+#define bflag optlist[12]
+#define uflag optlist[13]
+#define viflag optlist[14]
 #if BASH_PIPEFAIL
-# define pipefail optlist[14]
+# define pipefail optlist[15]
 #else
 # define pipefail 0
 #endif
 #if DEBUG
-# define nolog optlist[14 + BASH_PIPEFAIL]
-# define debug optlist[15 + BASH_PIPEFAIL]
+# define nolog optlist[15 + BASH_PIPEFAIL]
+# define debug optlist[16 + BASH_PIPEFAIL]
 #endif
 
        /* trap handler commands */
@@ -423,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   )
@@ -446,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; \
@@ -667,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
@@ -1499,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; \
@@ -1632,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;
@@ -1690,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.
  */
@@ -1701,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 *
@@ -2034,6 +2079,10 @@ static void changepath(const char *) FAST_FUNC;
 #if ENABLE_ASH_RANDOM_SUPPORT
 static void change_random(const char *) FAST_FUNC;
 #endif
+#if BASH_EPOCH_VARS
+static void change_seconds(const char *) FAST_FUNC;
+static void change_realtime(const char *) FAST_FUNC;
+#endif
 
 static const struct {
        int flags;
@@ -2060,6 +2109,10 @@ static const struct {
 #if ENABLE_ASH_RANDOM_SUPPORT
        { VSTRFIXED|VTEXTFIXED|VUNSET|VDYNAMIC, "RANDOM", change_random },
 #endif
+#if BASH_EPOCH_VARS
+       { VSTRFIXED|VTEXTFIXED|VUNSET|VDYNAMIC, "EPOCHSECONDS", change_seconds },
+       { VSTRFIXED|VTEXTFIXED|VUNSET|VDYNAMIC, "EPOCHREALTIME", change_realtime },
+#endif
 #if ENABLE_LOCALE_SUPPORT
        { VSTRFIXED|VTEXTFIXED|VUNSET, "LC_ALL"    , change_lc_all   },
        { VSTRFIXED|VTEXTFIXED|VUNSET, "LC_CTYPE"  , change_lc_ctype },
@@ -2091,30 +2144,30 @@ extern struct globals_var *BB_GLOBAL_CONST ash_ptr_to_globals_var;
 #define linenovar     (G_var.linenovar    )
 #define vifs      varinit[0]
 #if ENABLE_ASH_MAIL
-# define vmail    (&vifs)[1]
-# define vmpath   (&vmail)[1]
-# define vpath    (&vmpath)[1]
-#else
-# define vpath    (&vifs)[1]
-#endif
-#define vps1      (&vpath)[1]
-#define vps2      (&vps1)[1]
-#define vps4      (&vps2)[1]
+# define vmail    varinit[1]
+# define vmpath   varinit[2]
+#endif
+#define VAR_OFFSET1 (ENABLE_ASH_MAIL*2)
+#define vpath     varinit[VAR_OFFSET1 + 1]
+#define vps1      varinit[VAR_OFFSET1 + 2]
+#define vps2      varinit[VAR_OFFSET1 + 3]
+#define vps4      varinit[VAR_OFFSET1 + 4]
 #if ENABLE_ASH_GETOPTS
-# define voptind  (&vps4)[1]
-# define vlineno  (&voptind)[1]
-# if ENABLE_ASH_RANDOM_SUPPORT
-#  define vrandom (&vlineno)[1]
-# endif
-#else
-# define vlineno  (&vps4)[1]
-# if ENABLE_ASH_RANDOM_SUPPORT
-#  define vrandom (&vlineno)[1]
-# endif
+# define voptind  varinit[VAR_OFFSET1 + 5]
+#endif
+#define VAR_OFFSET2 (VAR_OFFSET1 + ENABLE_ASH_GETOPTS)
+#define vlineno   varinit[VAR_OFFSET2 + 5]
+#if ENABLE_ASH_RANDOM_SUPPORT
+# define vrandom  varinit[VAR_OFFSET2 + 6]
+#endif
+#define VAR_OFFSET3 (VAR_OFFSET2 + ENABLE_ASH_RANDOM_SUPPORT)
+#if BASH_EPOCH_VARS
+# define vepochs  varinit[VAR_OFFSET3 + 6]
+# define vepochr  varinit[VAR_OFFSET3 + 7]
 #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; \
@@ -2249,7 +2302,7 @@ lookupvar(const char *name)
 
        v = *findvar(hashvar(name), name);
        if (v) {
-#if ENABLE_ASH_RANDOM_SUPPORT
+#if ENABLE_ASH_RANDOM_SUPPORT || BASH_EPOCH_VARS
        /*
         * Dynamic variables are implemented roughly the same way they are
         * in bash. Namely, they're "special" so long as they aren't unset.
@@ -2345,6 +2398,10 @@ setvareq(char *s, int flags)
                }
 
                flags |= vp->flags & ~(VTEXTFIXED|VSTACK|VNOSAVE|VUNSET);
+#if ENABLE_ASH_RANDOM_SUPPORT || BASH_EPOCH_VARS
+               if (flags & VUNSET)
+                       flags &= ~VDYNAMIC;
+#endif
        } else {
                /* variable s is not found */
                if (flags & VNOSET)
@@ -2392,13 +2449,12 @@ setvar(const char *name, const char *val, int flags)
        }
 
        INT_OFF;
-       nameeq = ckmalloc(namelen + vallen + 2);
+       nameeq = ckzalloc(namelen + vallen + 2);
        p = mempcpy(nameeq, name, namelen);
        if (val) {
                *p++ = '=';
-               p = mempcpy(p, val, vallen);
+               memcpy(p, val, vallen);
        }
-       *p = '\0';
        vp = setvareq(nameeq, flags | VNOSAVE);
        INT_ON;
 
@@ -2420,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.
  */
@@ -2501,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);
 }
 
 
@@ -2786,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;
@@ -2815,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;
@@ -3505,7 +3596,7 @@ struct procstat {
 
 struct job {
        struct procstat ps0;    /* status of process */
-       struct procstat *ps;    /* status or processes when more than one */
+       struct procstat *ps;    /* status of processes when more than one */
 #if JOBS
        int stopstatus;         /* status of a stopped job */
 #endif
@@ -3701,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
@@ -4146,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))
@@ -4169,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
@@ -4200,9 +4288,20 @@ wait_block_or_sig(int *status)
                /* Children exist, but none are ready. Sleep until interesting signal */
 #if 1
                sigfillset(&mask);
-               sigprocmask(SIG_SETMASK, &mask, &mask);
-               while (!got_sigchld && !pending_sig)
+               sigprocmask2(SIG_SETMASK, &mask); /* mask is updated */
+               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)
@@ -4218,14 +4317,21 @@ wait_block_or_sig(int *status)
 #define DOWAIT_NONBLOCK 0
 #define DOWAIT_BLOCK    1
 #define DOWAIT_BLOCK_OR_SIG 2
+#if BASH_WAIT_N
+# define DOWAIT_JOBSTATUS 0x10   /* OR this to get job's exitstatus instead of pid */
+#endif
 
 static int
-dowait(int block, struct job *job)
+waitone(int block, struct job *job)
 {
        int pid;
        int status;
        struct job *jp;
-       struct job *thisjob = NULL;
+       struct job *thisjob;
+#if BASH_WAIT_N
+       bool want_jobexitstatus = (block & DOWAIT_JOBSTATUS);
+       block = (block & ~DOWAIT_JOBSTATUS);
+#endif
 
        TRACE(("dowait(0x%x) called\n", block));
 
@@ -4262,10 +4368,10 @@ dowait(int block, struct job *job)
        }
        TRACE(("wait returns pid=%d, status=0x%x, errno=%d(%s)\n",
                                pid, status, errno, strerror(errno)));
+       thisjob = NULL;
        if (pid <= 0)
                goto out;
 
-       thisjob = NULL;
        for (jp = curjob; jp; jp = jp->prev_job) {
                int jobstate;
                struct procstat *ps;
@@ -4317,13 +4423,16 @@ 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;
 
+#if BASH_WAIT_N
+       if (want_jobexitstatus) {
+               pid = -1;
+               if (thisjob && thisjob->state == JOBDONE)
+                       pid = thisjob->ps[thisjob->nprocs - 1].ps_status;
+       }
+#endif
        if (thisjob && thisjob == job) {
                char s[48 + 1];
                int len;
@@ -4338,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)
@@ -4426,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) {
@@ -4506,15 +4628,24 @@ waitcmd(int argc UNUSED_PARAM, char **argv)
        struct job *job;
        int retval;
        struct job *jp;
-
+#if BASH_WAIT_N
+       int status;
+       char one = nextopt("n");
+#else
        nextopt(nullstr);
+#endif
        retval = 0;
 
        argv = argptr;
-       if (!*argv) {
-               /* wait for all jobs */
+       if (!argv[0]) {
+               /* wait for all jobs / one job if -n */
                for (;;) {
                        jp = curjob;
+#if BASH_WAIT_N
+                       if (one && !jp)
+                               /* exitcode of "wait -n" with nothing to wait for is 127, not 0 */
+                               retval = 127;
+#endif
                        while (1) {
                                if (!jp) /* no running procs */
                                        goto ret;
@@ -4530,13 +4661,31 @@ waitcmd(int argc UNUSED_PARAM, char **argv)
         * with an exit status greater than 128, immediately after which
         * the trap is executed."
         */
+#if BASH_WAIT_N
+                       status = dowait(DOWAIT_BLOCK_OR_SIG | DOWAIT_JOBSTATUS, NULL);
+#else
                        dowait(DOWAIT_BLOCK_OR_SIG, NULL);
-       /* if child sends us a signal *and immediately exits*,
-        * dowait() returns pid > 0. Check this case,
-        * not "if (dowait() < 0)"!
-        */
+#endif
+                       /* 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
+                       if (one) {
+                               /* wait -n waits for one _job_, not one _process_.
+                                *  date; sleep 3 & sleep 2 | sleep 1 & wait -n; date
+                                * should wait for 2 seconds. Not 1 or 3.
+                                */
+                               if (status != -1 && !WIFSTOPPED(status)) {
+                                       retval = WEXITSTATUS(status);
+                                       if (WIFSIGNALED(status))
+                                               retval = WTERMSIG(status) + 128;
+                                       goto ret;
+                               }
+                       }
+#endif
                }
        }
 
@@ -4556,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: ;
@@ -4702,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;
@@ -5111,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 */
@@ -5122,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;
@@ -5205,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
@@ -5889,7 +6029,9 @@ static int substr_atoi(const char *s)
 #define EXP_CASE        0x10    /* keeps quotes around for CASE pattern */
 #define EXP_VARTILDE2   0x20    /* expand tildes after colons only */
 #define EXP_WORD        0x40    /* expand word in parameter expansion */
-#define EXP_QUOTED      0x80    /* expand word in double quotes */
+#define EXP_QUOTED      0x100   /* expand word in double quotes */
+#define EXP_KEEPNUL     0x200   /* do not skip NUL characters */
+
 /*
  * rmescape() flags
  */
@@ -5900,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
@@ -5930,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
@@ -6207,45 +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
-                                || (((quotes & EXP_FULL) || 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.
@@ -6310,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;
 
@@ -6327,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 '/':
@@ -6345,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;
@@ -6448,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;
@@ -6462,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;
@@ -6482,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;
 
@@ -6546,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);
@@ -6841,8 +6977,15 @@ subevalvar(char *p, char *varname, int strloc, int subtype,
        if (subtype == VSREPLACE || subtype == VSREPLACEALL) {
                /* Find '/' and replace with NUL */
                repl = p;
+               /* The pattern can't be empty.
+                * IOW: if the first char after "${v//" is a slash,
+                * it does not terminate the pattern - it's the first char of the pattern:
+                *  v=/dev/ram; echo ${v////-}  prints -dev-ram (pattern is "/")
+                *  v=/dev/ram; echo ${v///r/-} prints /dev-am  (pattern is "/r")
+                */
+               if (*repl == '/')
+                       repl++;
                for (;;) {
-                       /* Handle escaped slashes, e.g. "${v/\//_}" (they are CTLESC'ed by this point) */
                        if (*repl == '\0') {
                                repl = NULL;
                                break;
@@ -6851,6 +6994,7 @@ subevalvar(char *p, char *varname, int strloc, int subtype,
                                *repl = '\0';
                                break;
                        }
+                       /* Handle escaped slashes, e.g. "${v/\//_}" (they are CTLESC'ed by this point) */
                        if ((unsigned char)*repl == CTLESC && repl[1])
                                repl++;
                        repl++;
@@ -7154,21 +7298,19 @@ subevalvar(char *p, char *varname, int strloc, int subtype,
  * ash -c 'echo ${#1#}'  name:'1=#'
  */
 static NOINLINE ssize_t
-varvalue(char *name, int varflags, int flags, int *quotedp)
+varvalue(char *name, int varflags, int flags, int quoted)
 {
        const char *p;
        int num;
        int i;
        ssize_t len = 0;
        int sep;
-       int quoted = *quotedp;
        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 '$':
@@ -7185,7 +7327,7 @@ varvalue(char *name, int varflags, int flags, int *quotedp)
                if (num == 0)
                        return -1;
  numvar:
-               len = cvtnum(num);
+               len = cvtnum(num, flags);
                goto check_1char_name;
        case '-':
                expdest = makestrspace(NOPTS, expdest);
@@ -7209,22 +7351,36 @@ varvalue(char *name, int varflags, int flags, int *quotedp)
        case '*': {
                char **ap;
                char sepc;
+               char c;
 
-               if (quoted)
-                       sep = 0;
-               sep |= ifsset() ? ifsval()[0] : ' ';
+               /* We will set c to 0 or ~0 depending on whether
+                * we're doing field splitting.  We won't do field
+                * splitting if either we're quoted or sep is zero.
+                *
+                * Instead of testing (quoted || !sep) the following
+                * trick optimises away any branches by using the
+                * fact that EXP_QUOTED (which is the only bit that
+                * can be set in quoted) is the same as EXP_FULL <<
+                * CHAR_BIT (which is the only bit that can be set
+                * in sep).
+                */
+#if EXP_QUOTED >> CHAR_BIT != EXP_FULL
+#error The following two lines expect EXP_QUOTED == EXP_FULL << CHAR_BIT
+#endif
+               c = !((quoted | ~sep) & EXP_QUOTED) - 1;
+               sep &= ~quoted;
+               sep |= ifsset() ? (unsigned char)(c & ifsval()[0]) : ' ';
  param:
                sepc = sep;
-               *quotedp = !sepc;
                ap = shellparam.p;
                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;
@@ -7251,7 +7407,7 @@ varvalue(char *name, int varflags, int flags, int *quotedp)
                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();
@@ -7280,7 +7436,6 @@ evalvar(char *p, int flag)
        char varflags;
        char subtype;
        int quoted;
-       char easy;
        char *var;
        int patloc;
        int startloc;
@@ -7294,12 +7449,11 @@ evalvar(char *p, int flag)
 
        quoted = flag & EXP_QUOTED;
        var = p;
-       easy = (!quoted || (*var == '@' && shellparam.nparam));
        startloc = expdest - (char *)stackblock();
        p = strchr(p, '=') + 1; //TODO: use var_end(p)?
 
  again:
-       varlen = varvalue(var, varflags, flag, &quoted);
+       varlen = varvalue(var, varflags, flag, quoted);
        if (varflags & VSNUL)
                varlen--;
 
@@ -7339,14 +7493,17 @@ 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;
        }
 
        if (subtype == VSNORMAL) {
  record:
-               if (!easy)
-                       goto end;
+               if (quoted) {
+                       quoted = *var == '@' && shellparam.nparam;
+                       if (!quoted)
+                               goto end;
+               }
                recordregion(startloc, expdest - (char *)stackblock(), quoted);
                goto end;
        }
@@ -7389,6 +7546,8 @@ evalvar(char *p, int flag)
                goto record;
        }
 
+       varlen = 0;
+
  end:
        if (subtype != VSNORMAL) {      /* skip to end of alternative */
                int nesting = 1;
@@ -7562,9 +7721,16 @@ expandmeta(struct strlist *str /*, int flag*/)
 /*
  * Do metacharacter (i.e. *, ?, [...]) expansion.
  */
+typedef struct exp_t {
+       char *dir;
+       unsigned dir_max;
+} exp_t;
 static void
-expmeta(char *expdir, char *enddir, char *name)
+expmeta(exp_t *exp, char *name, unsigned name_len, unsigned expdir_len)
 {
+#define expdir exp->dir
+#define expdir_max exp->dir_max
+       char *enddir = expdir + expdir_len;
        char *p;
        const char *cp;
        char *start;
@@ -7597,7 +7763,7 @@ expmeta(char *expdir, char *enddir, char *name)
                                }
                        }
                } else {
-                       if (*p == '\\')
+                       if (*p == '\\' && p[1])
                                esc++;
                        if (p[esc] == '/') {
                                if (metaflag)
@@ -7607,15 +7773,15 @@ expmeta(char *expdir, char *enddir, char *name)
                }
        }
        if (metaflag == 0) {    /* we've reached the end of the file name */
-               if (enddir != expdir)
-                       metaflag++;
+               if (!expdir_len)
+                       return;
                p = name;
                do {
-                       if (*p == '\\')
+                       if (*p == '\\' && p[1])
                                p++;
                        *enddir++ = *p;
                } while (*p++);
-               if (metaflag == 0 || lstat(expdir, &statb) >= 0)
+               if (lstat(expdir, &statb) == 0)
                        addfname(expdir);
                return;
        }
@@ -7623,24 +7789,19 @@ expmeta(char *expdir, char *enddir, char *name)
        if (name < start) {
                p = name;
                do {
-                       if (*p == '\\')
+                       if (*p == '\\' && p[1])
                                p++;
                        *enddir++ = *p++;
                } while (p < start);
        }
-       if (enddir == expdir) {
+       *enddir = '\0';
+       cp = expdir;
+       expdir_len = enddir - cp;
+       if (!expdir_len)
                cp = ".";
-       } else if (enddir == expdir + 1 && *expdir == '/') {
-               cp = "/";
-       } else {
-               cp = expdir;
-               enddir[-1] = '\0';
-       }
        dirp = opendir(cp);
        if (dirp == NULL)
                return;
-       if (enddir != expdir)
-               enddir[-1] = '/';
        if (*endname == 0) {
                atend = 1;
        } else {
@@ -7648,6 +7809,7 @@ expmeta(char *expdir, char *enddir, char *name)
                *endname = '\0';
                endname += esc + 1;
        }
+       name_len -= endname - name;
        matchdot = 0;
        p = start;
        if (*p == '\\')
@@ -7662,16 +7824,30 @@ expmeta(char *expdir, char *enddir, char *name)
                                strcpy(enddir, dp->d_name);
                                addfname(expdir);
                        } else {
-                               for (p = enddir, cp = dp->d_name; (*p++ = *cp++) != '\0';)
-                                       continue;
-                               p[-1] = '/';
-                               expmeta(expdir, p, endname);
+                               unsigned offset;
+                               unsigned len;
+
+                               p = stpcpy(enddir, dp->d_name);
+                               *p = '/';
+
+                               offset = p - expdir + 1;
+                               len = offset + name_len + NAME_MAX;
+                               if (len > expdir_max) {
+                                       len += PATH_MAX;
+                                       expdir = ckrealloc(expdir, len);
+                                       expdir_max = len;
+                               }
+
+                               expmeta(exp, endname, name_len, offset);
+                               enddir = expdir + expdir_len;
                        }
                }
        }
        closedir(dirp);
        if (!atend)
                endname[-esc - 1] = esc ? '\\' : '/';
+#undef expdir
+#undef expdir_max
 }
 
 static struct strlist *
@@ -7744,10 +7920,11 @@ expandmeta(struct strlist *str /*, int flag*/)
        /* TODO - EXP_REDIR */
 
        while (str) {
-               char *expdir;
+               exp_t exp;
                struct strlist **savelastp;
                struct strlist *sp;
                char *p;
+               unsigned len;
 
                if (fflag)
                        goto nometa;
@@ -7757,13 +7934,12 @@ expandmeta(struct strlist *str /*, int flag*/)
 
                INT_OFF;
                p = preglob(str->text, RMESCAPE_ALLOC | RMESCAPE_HEAP);
-               {
-                       int i = strlen(str->text);
-//BUGGY estimation of how long expanded name can be
-                       expdir = ckmalloc(i < 2048 ? 2048 : i+1);
-               }
-               expmeta(expdir, expdir, p);
-               free(expdir);
+               len = strlen(p);
+               exp.dir_max = len + PATH_MAX;
+               exp.dir = ckmalloc(exp.dir_max);
+
+               expmeta(&exp, p, len, 0);
+               free(exp.dir);
                if (p != str->text)
                        free(p);
                INT_ON;
@@ -7915,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 *);
 
@@ -7974,6 +8150,7 @@ tryexec(IF_FEATURE_SH_STANDALONE(int applet_no,) const char *cmd, char **argv, c
 #else
        execve(cmd, argv, envp);
 #endif
+
        if (cmd != bb_busybox_exec_path && errno == ENOEXEC) {
                /* Run "cmd" as a shell script:
                 * http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
@@ -8034,32 +8211,32 @@ 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);
                }
        }
 
        /* Map to POSIX errors */
        switch (e) {
-       case EACCES:
+       default:
                exerrno = 126;
                break;
+       case ELOOP:
+       case ENAMETOOLONG:
        case ENOENT:
+       case ENOTDIR:
                exerrno = 127;
                break;
-       default:
-               exerrno = 2;
-               break;
        }
        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 */
 }
 
@@ -8073,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;
@@ -8094,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);
@@ -8197,7 +8374,7 @@ hashcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
        char *name;
 
        if (nextopt("r") != '\0') {
-               clearcmdentry(0);
+               clearcmdentry();
                return 0;
        }
 
@@ -8216,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();
                }
@@ -8258,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,
@@ -8468,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);
@@ -8482,7 +8649,8 @@ describe_command(char *command, const char *path, int describe_command_verbose)
 
        case CMDFUNCTION:
                if (describe_command_verbose) {
-                       out1str(" is a shell function");
+                       /*out1str(" is a shell function");*/
+                       out1str(" is a function"); /* bash says this */
                } else {
                        out1str(command);
                }
@@ -8529,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 {
@@ -8556,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
@@ -8901,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 */
@@ -8921,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();
 
@@ -8953,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"));
 }
 
@@ -8978,8 +9176,11 @@ evaltree(union node *n, int flags)
 {
        int checkexit = 0;
        int (*evalfn)(union node *, int);
+       struct stackmark smark;
        int status = 0;
 
+       setstackmark(&smark);
+
        if (n == NULL) {
                TRACE(("evaltree(NULL) called\n"));
                goto out;
@@ -9089,10 +9290,11 @@ 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"));
        return exitstatus;
 }
@@ -9153,14 +9355,12 @@ evalfor(union node *n, int flags)
        struct arglist arglist;
        union node *argp;
        struct strlist *sp;
-       struct stackmark smark;
        int status = 0;
 
        errlinno = lineno = n->ncase.linno;
        if (funcline)
                lineno -= funcline - 1;
 
-       setstackmark(&smark);
        arglist.list = NULL;
        arglist.lastp = &arglist.list;
        for (argp = n->nfor.args; argp; argp = argp->narg.next) {
@@ -9177,7 +9377,6 @@ evalfor(union node *n, int flags)
                        break;
        }
        loopnest--;
-       popstackmark(&smark);
 
        return status;
 }
@@ -9188,14 +9387,12 @@ evalcase(union node *n, int flags)
        union node *cp;
        union node *patp;
        struct arglist arglist;
-       struct stackmark smark;
        int status = 0;
 
        errlinno = lineno = n->ncase.linno;
        if (funcline)
                lineno -= funcline - 1;
 
-       setstackmark(&smark);
        arglist.list = NULL;
        arglist.lastp = &arglist.list;
        expandarg(n->ncase.expr, &arglist, EXP_TILDE);
@@ -9214,8 +9411,6 @@ evalcase(union node *n, int flags)
                }
        }
  out:
-       popstackmark(&smark);
-
        return status;
 }
 
@@ -9303,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 */
@@ -9392,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.
  */
@@ -9406,8 +9605,8 @@ setinteractive(int on)
        setsignal(SIGINT);
        setsignal(SIGQUIT);
        setsignal(SIGTERM);
-#if !ENABLE_FEATURE_SH_EXTRA_QUIET
        if (is_interactive > 1) {
+#if !ENABLE_FEATURE_SH_EXTRA_QUIET
                /* Looks like they want an interactive shell */
                static smallint did_banner;
 
@@ -9421,8 +9620,16 @@ setinteractive(int on)
                        );
                        did_banner = 1;
                }
-       }
 #endif
+#if ENABLE_FEATURE_EDITING
+               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
+       }
 }
 
 static void
@@ -9434,10 +9641,12 @@ optschanged(void)
        setinteractive(iflag);
        setjobctl(mflag);
 #if ENABLE_FEATURE_EDITING_VI
-       if (viflag)
-               line_input_state->flags |= VI_MODE;
-       else
-               line_input_state->flags &= ~VI_MODE;
+       if (line_input_state) {
+               if (viflag)
+                       line_input_state->flags |= VI_MODE;
+               else
+                       line_input_state->flags &= ~VI_MODE;
+       }
 #else
        viflag = 0; /* forcibly keep the option off */
 #endif
@@ -9512,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
@@ -9561,9 +9775,7 @@ evalfun(struct funcnode *func, int argc, char **argv, int flags)
        shellparam.optind = 1;
        shellparam.optoff = -1;
 #endif
-       pushlocalvars();
        evaltree(func->n.ndefun.body, flags & EV_TESTED);
-       poplocalvars(0);
  funcdone:
        INT_OFF;
        funcline = savefuncline;
@@ -9572,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;
 }
 
@@ -9584,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;
@@ -9621,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;
@@ -9633,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);
@@ -9659,7 +9871,7 @@ localcmd(int argc UNUSED_PARAM, char **argv)
 
        argv = argptr;
        while ((name = *argv++) != NULL) {
-               mklocal(name);
+               mklocal(name, 0);
        }
        return 0;
 }
@@ -9717,13 +9929,24 @@ 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[] */
 static int breakcmd(int, char **) FAST_FUNC;
@@ -9809,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
@@ -9827,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  },
@@ -9842,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 },
@@ -9888,9 +10111,18 @@ 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.
  */
+static void unwindfiles(struct parsefile *stop);
 static int
 isassignment(const char *p)
 {
@@ -9910,25 +10142,29 @@ 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;
        struct redirtab *redir_stop;
-       struct stackmark smark;
        union node *argp;
        struct arglist arglist;
        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)
@@ -9936,8 +10172,7 @@ evalcommand(union node *cmd, int flags)
 
        /* First expand the arguments. */
        TRACE(("evalcommand(0x%lx, %d) called\n", (long)cmd, flags));
-       setstackmark(&smark);
-       localvar_stop = pushlocalvars();
+       file_stop = g_parsefile;
        back_exitstatus = 0;
 
        cmdentry.cmdtype = CMDBUILTIN;
@@ -9947,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;
@@ -9996,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. */
@@ -10051,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 \
@@ -10133,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.
@@ -10159,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;
                        }
@@ -10168,48 +10396,32 @@ 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:
-               poplocalvars(1);
-               /* 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);
        unwindredir(redir_stop);
+       unwindfiles(file_stop);
        unwindlocalvars(localvar_stop);
        if (lastarg) {
                /* dsl: I think this is intended to be used to support
@@ -10218,7 +10430,6 @@ evalcommand(union node *cmd, int flags)
                 */
                setvar0("_", lastarg);
        }
-       popstackmark(&smark);
 
        return status;
 }
@@ -10413,13 +10624,11 @@ preadfd(void)
        else {
 # if ENABLE_ASH_IDLE_TIMEOUT
                int timeout = -1;
-               if (iflag) {
-                       const char *tmout_var = lookupvar("TMOUT");
-                       if (tmout_var) {
-                               timeout = atoi(tmout_var) * 1000;
-                               if (timeout <= 0)
-                                       timeout = -1;
-                       }
+               const char *tmout_var = lookupvar("TMOUT");
+               if (tmout_var) {
+                       timeout = atoi(tmout_var) * 1000;
+                       if (timeout <= 0)
+                               timeout = -1;
                }
                line_input_state->timeout = timeout;
 # endif
@@ -10679,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)
 {
@@ -10732,14 +10947,20 @@ popfile(void)
        INT_ON;
 }
 
+static void
+unwindfiles(struct parsefile *stop)
+{
+       while (g_parsefile != stop)
+               popfile();
+}
+
 /*
  * Return to top level.
  */
 static void
 popallfiles(void)
 {
-       while (g_parsefile != &basepf)
-               popfile();
+       unwindfiles(&basepf);
 }
 
 /*
@@ -10850,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;
@@ -10952,6 +11177,8 @@ plus_minus_o(char *name, int val)
                return 1;
        }
        for (i = 0; i < NOPTS; i++) {
+               if (optnames(i)[0] == '\0')
+                       continue;
                if (val) {
                        out1fmt("%-16s%s\n", optnames(i), optlist[i] ? "on" : "off");
                } else {
@@ -10966,7 +11193,7 @@ setoption(int flag, int val)
        int i;
 
        for (i = 0; i < NOPTS; i++) {
-               if (optletters(i) == flag) {
+               if (optletters(i) == flag && optnames(i)[0] != '\0') {
                        optlist[i] = val;
                        return;
                }
@@ -10974,14 +11201,17 @@ setoption(int flag, int val)
        ash_msg_and_raise_error("illegal option %c%c", val ? '-' : '+', flag);
        /* NOTREACHED */
 }
+/* If login_sh is not NULL, we are called to parse command line opts,
+ * not "set -opts"
+ */
 static int
-options(int cmdline, int *login_sh)
+options(int *login_sh)
 {
        char *p;
        int val;
        int c;
 
-       if (cmdline)
+       if (login_sh)
                minusc = NULL;
        while ((p = *argptr) != NULL) {
                c = *p++;
@@ -10992,7 +11222,7 @@ options(int cmdline, int *login_sh)
                if (c == '-') {
                        val = 1;
                        if (p[0] == '\0' || LONE_DASH(p)) {
-                               if (!cmdline) {
+                               if (!login_sh) {
                                        /* "-" means turn off -x and -v */
                                        if (p[0] == '\0')
                                                xflag = vflag = 0;
@@ -11005,26 +11235,40 @@ options(int cmdline, int *login_sh)
                }
                /* first char was + or - */
                while ((c = *p++) != '\0') {
-                       /* bash 3.2 indeed handles -c CMD and +c CMD the same */
-                       if (c == 'c' && cmdline) {
-                               minusc = p;     /* command is after shell args */
-                       } else if (c == 'o') {
+                       if (login_sh) {
+                               /* bash 3.2 indeed handles -c CMD and +c CMD the same */
+                               if (c == 'c') {
+                                       minusc = p; /* command is after shell args */
+                                       cflag = 1;
+                                       continue;
+                               }
+                               if (c == 's') { /* -s, +s */
+                                       sflag = 1;
+                                       continue;
+                               }
+                               if (c == 'i') { /* -i, +i */
+                                       iflag = 1;
+                                       continue;
+                               }
+                               if (c == 'l') {
+                                       *login_sh = 1; /* -l or +l == --login */
+                                       continue;
+                               }
+                               /* bash does not accept +-login, we also won't */
+                               if (val && (c == '-')) { /* long options */
+                                       if (strcmp(p, "login") == 0) {
+                                               *login_sh = 1;
+                                       }
+                                       break;
+                               }
+                       }
+                       if (c == 'o') {
                                if (plus_minus_o(*argptr, val)) {
                                        /* it already printed err message */
                                        return 1; /* error */
                                }
                                if (*argptr)
                                        argptr++;
-                       } else if (cmdline && (c == 'l')) { /* -l or +l == --login */
-                               if (login_sh)
-                                       *login_sh = 1;
-                       /* bash does not accept +-login, we also won't */
-                       } else if (cmdline && val && (c == '-')) { /* long options */
-                               if (strcmp(p, "login") == 0) {
-                                       if (login_sh)
-                                               *login_sh = 1;
-                               }
-                               break;
                        } else {
                                setoption(c, val);
                        }
@@ -11115,7 +11359,7 @@ setcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
                return showvars(nullstr, 0, VUNSET);
 
        INT_OFF;
-       retval = options(/*cmdline:*/ 0, NULL);
+       retval = options(/*login_sh:*/ NULL);
        if (retval == 0) { /* if no parse error... */
                optschanged();
                if (*argptr != NULL) {
@@ -11146,6 +11390,32 @@ change_random(const char *value)
 }
 #endif
 
+#if BASH_EPOCH_VARS
+static void FAST_FUNC
+change_epoch(struct var *vepoch, const char *fmt)
+{
+       struct timeval tv;
+       char buffer[sizeof("%lu.nnnnnn") + sizeof(long)*3];
+
+       gettimeofday(&tv, NULL);
+       sprintf(buffer, fmt, (unsigned long)tv.tv_sec, (unsigned)tv.tv_usec);
+       setvar(vepoch->var_text, buffer, VNOFUNC);
+       vepoch->flags &= ~VNOFUNC;
+}
+
+static void FAST_FUNC
+change_seconds(const char *value UNUSED_PARAM)
+{
+       change_epoch(&vepochs, "%lu");
+}
+
+static void FAST_FUNC
+change_realtime(const char *value UNUSED_PARAM)
+{
+       change_epoch(&vepochr, "%lu.%06u");
+}
+#endif
+
 #if ENABLE_ASH_GETOPTS
 static int
 getopts(char *optstr, char *optvar, char **optfirst)
@@ -11337,7 +11607,7 @@ list(int nlflag)
 
        n1 = NULL;
        for (;;) {
-               switch (peektoken()) {
+               switch (readtoken()) {
                case TNL:
                        if (!(nlflag & 1))
                                break;
@@ -11348,9 +11618,12 @@ list(int nlflag)
                        if (!n1 && (nlflag & 1))
                                n1 = NODE_EOF;
                        parseheredoc();
+                       tokpushback++;
+                       lasttoken = TEOF;
                        return n1;
                }
 
+               tokpushback++;
                checkkwd = CHKNL | CHKKWD | CHKALIAS;
                if (nlflag == 2 && ((1 << peektoken()) & tokendlist))
                        return n1;
@@ -11611,10 +11884,12 @@ simplecmd(void)
                                case TLP:
                                        function_flag = 0;
                                        break;
+# if BASH_TEST2
                                case TWORD:
                                        if (strcmp("[[", wordtext) == 0)
                                                goto do_func;
                                        /* fall through */
+# endif
                                default:
                                        raise_error_unexpected_syntax(-1);
                                }
@@ -11984,11 +12259,14 @@ readtoken1(int c, int syntax, char *eofmark, int striptabs)
                CHECKSTRSPACE(4, out);  /* permit 4 calls to USTPUTC */
                switch (SIT(c, synstack->syntax)) {
                case CNL:       /* '\n' */
-                       if (synstack->syntax == BASESYNTAX)
+                       if (synstack->syntax == BASESYNTAX
+                        && !synstack->varnest
+                       ) {
                                goto endword;   /* exit outer loop */
+                       }
                        USTPUTC(c, out);
                        nlprompt();
-                       c = pgetc();
+                       c = pgetc_top(synstack);
                        goto loop;              /* continue outer loop */
                case CWORD:
                        USTPUTC(c, out);
@@ -12020,8 +12298,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);
@@ -12141,7 +12417,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:
 
@@ -12204,7 +12480,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();
                }
 
@@ -12375,8 +12657,8 @@ parsesub: {
                        do {
                                STPUTC(c, out);
                                c = pgetc_eatbnl();
-                       } while (isdigit(c));
-               } else {
+                       } while (!subtype && isdigit(c));
+               } else if (c != '}') {
                        /* $[{[#]]<specialchar>[}] */
                        int cc = c;
 
@@ -12402,7 +12684,8 @@ parsesub: {
                        }
 
                        USTPUTC(cc, out);
-               }
+               } else
+                       goto badsub;
 
                if (c != '}' && subtype == VSLENGTH) {
                        /* ${#VAR didn't end with } */
@@ -12505,6 +12788,7 @@ parsebackq: {
        union node *n;
        char *str;
        size_t savelen;
+       struct heredoc *saveheredoclist;
        smallint saveprompt = 0;
 
        str = NULL;
@@ -12580,6 +12864,9 @@ parsebackq: {
        *nlpp = stzalloc(sizeof(**nlpp));
        /* (*nlpp)->next = NULL; - stzalloc did it */
 
+       saveheredoclist = heredoclist;
+       heredoclist = NULL;
+
        if (oldstyle) {
                saveprompt = doprompt;
                doprompt = 0;
@@ -12589,21 +12876,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);
@@ -12878,9 +13166,12 @@ parseheredoc(void)
        heredoclist = NULL;
 
        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 */
@@ -12897,41 +13188,54 @@ expandstr(const char *ps, int syntax_type)
 {
        union node n;
        int saveprompt;
+       struct parsefile *file_stop = g_parsefile;
+       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) > '
         */
-       {
-               volatile int saveint;
-               struct jmploc *volatile savehandler = exception_handler;
-               struct jmploc jmploc;
-               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;
-
-       popfile();
+       exception_handler = &jmploc;
+       readtoken1(pgetc(), syntax_type, FAKEEOFMARK, 0);
 
        n.narg.type = NARG;
        n.narg.next = NULL;
        n.narg.text = wordtext;
        n.narg.backquote = backquotelist;
 
+       /* expandarg() might fail too:
+        * PS1='$((123+))'
+        */
        expandarg(&n, NULL, EXP_QUOTED);
-       return stackblock();
+       result = stackblock();
+
+out:
+       exception_handler = savehandler;
+       if (err && exception_type != EXERROR)
+               longjmp(exception_handler->loc, 1);
+       RESTORE_INT(saveint);
+
+       doprompt = saveprompt;
+       /* Try: PS1='`xxx(`' */
+       unwindfiles(file_stop);
+
+       return result;
 }
 
 static inline int
@@ -13060,8 +13364,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++;
@@ -13079,7 +13387,7 @@ cmdloop(int top)
                skip = evalskip;
 
                if (skip) {
-                       evalskip &= ~SKIPFUNC;
+                       evalskip &= ~(SKIPFUNC | SKIPFUNCDEF);
                        break;
                }
        }
@@ -13091,33 +13399,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
 }
@@ -13183,8 +13490,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 */
 }
@@ -13218,6 +13527,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) {
@@ -13239,11 +13549,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);
@@ -13256,16 +13563,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)
@@ -13278,14 +13588,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);
@@ -13309,20 +13620,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;
                        }
                }
@@ -13345,8 +13656,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) */
@@ -13389,6 +13700,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;
 
@@ -13530,7 +13842,8 @@ helpcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
 static int FAST_FUNC
 historycmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
 {
-       show_history(line_input_state);
+       if (line_input_state)
+               show_history(line_input_state);
        return EXIT_SUCCESS;
 }
 #endif
@@ -13707,38 +14020,35 @@ letcmd(int argc UNUSED_PARAM, char **argv)
 static int FAST_FUNC
 readcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
 {
-       char *opt_n = NULL;
-       char *opt_p = NULL;
-       char *opt_t = NULL;
-       char *opt_u = NULL;
-       char *opt_d = NULL; /* optimized out if !BASH */
-       int read_flags = 0;
+       struct builtin_read_params params;
        const char *r;
        int i;
 
+       memset(&params, 0, sizeof(params));
+
        while ((i = nextopt("p:u:rt:n:sd:")) != '\0') {
                switch (i) {
                case 'p':
-                       opt_p = optionarg;
+                       params.opt_p = optionarg;
                        break;
                case 'n':
-                       opt_n = optionarg;
+                       params.opt_n = optionarg;
                        break;
                case 's':
-                       read_flags |= BUILTIN_READ_SILENT;
+                       params.read_flags |= BUILTIN_READ_SILENT;
                        break;
                case 't':
-                       opt_t = optionarg;
+                       params.opt_t = optionarg;
                        break;
                case 'r':
-                       read_flags |= BUILTIN_READ_RAW;
+                       params.read_flags |= BUILTIN_READ_RAW;
                        break;
                case 'u':
-                       opt_u = optionarg;
+                       params.opt_u = optionarg;
                        break;
 #if BASH_READ_D
                case 'd':
-                       opt_d = optionarg;
+                       params.opt_d = optionarg;
                        break;
 #endif
                default:
@@ -13746,21 +14056,16 @@ readcmd(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
                }
        }
 
+       params.argv = argptr;
+       params.setvar = setvar0;
+       params.ifs = bltinlookup("IFS"); /* can be NULL */
+
        /* "read -s" needs to save/restore termios, can't allow ^C
         * to jump out of it.
         */
  again:
        INT_OFF;
-       r = shell_builtin_read(setvar0,
-               argptr,
-               bltinlookup("IFS"), /* can be NULL */
-               read_flags,
-               opt_n,
-               opt_p,
-               opt_t,
-               opt_u,
-               opt_d
-       );
+       r = shell_builtin_read(&params);
        INT_ON;
 
        if ((uintptr_t)r == 1 && errno == EINTR) {
@@ -13843,6 +14148,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.
  */
@@ -13851,33 +14197,32 @@ exitshell(void)
 {
        struct jmploc loc;
        char *p;
-       int status;
 
 #if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
-       save_history(line_input_state);
+       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 */
 }
 
@@ -13892,11 +14237,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;
@@ -13919,6 +14259,7 @@ init(void)
                        }
                }
 
+               setvareq((char*)defifsvar, VTEXTFIXED);
                setvareq((char*)defoptindvar, VTEXTFIXED);
 
                setvar0("PPID", utoa(getppid()));
@@ -13965,13 +14306,17 @@ procargs(char **argv)
 
        xargv = argv;
        login_sh = xargv[0] && xargv[0][0] == '-';
+#if NUM_SCRIPTS > 0
+       if (minusc)
+               goto setarg0;
+#endif
        arg0 = xargv[0];
        /* if (xargv[0]) - mmm, this is always true! */
                xargv++;
+       argptr = xargv;
        for (i = 0; i < NOPTS; i++)
                optlist[i] = 2;
-       argptr = xargv;
-       if (options(/*cmdline:*/ 1, &login_sh)) {
+       if (options(&login_sh)) {
                /* it already printed err message */
                raise_exception(EXERROR);
        }
@@ -13982,8 +14327,13 @@ procargs(char **argv)
                        ash_msg_and_raise_error(bb_msg_requires_arg, "-c");
                sflag = 1;
        }
-       if (iflag == 2 && sflag == 1 && isatty(0) && isatty(1))
+       if (iflag == 2 /* no explicit -i given */
+        && sflag == 1 /* -s given (or implied) */
+        && !minusc /* bash compat: ash -sc 'echo $-' is not interactive (dash is) */
+        && isatty(0) && isatty(1) /* we are on tty */
+       ) {
                iflag = 1;
+       }
        if (mflag == 2)
                mflag = iflag;
        for (i = 0; i < NOPTS; i++)
@@ -14032,33 +14382,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();
@@ -14072,7 +14395,12 @@ extern int etext();
  * is used to figure out how far we had gotten.
  */
 int ash_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
+#if NUM_SCRIPTS > 0
+int ash_main(int argc, char **argv)
+#else
 int ash_main(int argc UNUSED_PARAM, char **argv)
+#endif
+/* note: 'argc' is used only if embedded scripts are enabled */
 {
        volatile smallint state;
        struct jmploc jmploc;
@@ -14092,21 +14420,21 @@ int ash_main(int argc UNUSED_PARAM, char **argv)
        monitor(4, etext, profile_buf, sizeof(profile_buf), 50);
 #endif
 
-#if ENABLE_FEATURE_EDITING
-       line_input_state = new_line_input_t(FOR_SHELL | WITH_PATH_LOOKUP);
-#endif
        state = 0;
        if (setjmp(jmploc.loc)) {
                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);
                }
@@ -14126,6 +14454,12 @@ int ash_main(int argc UNUSED_PARAM, char **argv)
 
        init();
        setstackmark(&smark);
+
+#if NUM_SCRIPTS > 0
+       if (argc < 0)
+               /* Non-NULL minusc tells procargs that an embedded script is being run */
+               minusc = get_script_content(-argc - 1);
+#endif
        login_sh = procargs(argv);
 #if DEBUG
        TRACE(("Shell args: "));
@@ -14163,10 +14497,17 @@ int ash_main(int argc UNUSED_PARAM, char **argv)
                 * Ensure we don't falsely claim that 0 (stdin)
                 * is one of stacked source fds.
                 * Testcase: ash -c 'exec 1>&0' must not complain. */
+
                // if (!sflag) g_parsefile->pf_fd = -1;
                // ^^ not necessary since now we special-case fd 0
                // in save_fd_on_redirect()
-               evalstring(minusc, sflag ? 0 : EV_EXIT);
+
+               // dash: evalstring(minusc, sflag ? 0 : EV_EXIT);
+               // The above makes
+               //  ash -sc 'echo $-'
+               // continue reading input from stdin after running 'echo'.
+               // bash does not do this: it prints "hBcs" and exits.
+               evalstring(minusc, EV_EXIT);
        }
 
        if (sflag || minusc == NULL) {
@@ -14193,6 +14534,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