ash: expand: Use HOME in tilde expansion when it is empty
[oweals/busybox.git] / shell / ash.c
index 5586015439cf5d58030731d43c838702fba74c2a..138f19abdaeb7681c1aa8449f56208d4d9bab486 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
 
 #define JOBS ENABLE_ASH_JOB_CONTROL
 
-#include <setjmp.h>
 #include <fnmatch.h>
 #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() */
@@ -296,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",
@@ -315,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)
@@ -343,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;
@@ -378,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 */
@@ -424,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   )
@@ -447,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; \
@@ -668,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
@@ -1500,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; \
@@ -2035,6 +2080,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;
@@ -2061,6 +2110,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 },
@@ -2092,30 +2145,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; \
@@ -2250,7 +2303,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.
@@ -2346,6 +2399,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)
@@ -2393,13 +2450,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;
 
@@ -3506,7 +3562,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
@@ -4147,12 +4203,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))
@@ -4170,17 +4225,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
@@ -4201,7 +4256,7 @@ 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);
+               sigprocmask2(SIG_SETMASK, &mask); /* mask is updated */
                while (!got_sigchld && !pending_sig)
                        sigsuspend(&mask);
                sigprocmask(SIG_SETMASK, &mask, NULL);
@@ -4219,6 +4274,9 @@ 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)
@@ -4226,7 +4284,11 @@ dowait(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));
 
@@ -4263,10 +4325,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;
@@ -4325,6 +4387,13 @@ dowait(int block, struct job *job)
  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;
@@ -4507,15 +4576,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;
@@ -4531,13 +4609,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);
+#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
                }
        }
 
@@ -5890,7 +5986,7 @@ 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 */
 /*
  * rmescape() flags
  */
@@ -6224,9 +6320,7 @@ memtodest(const char *p, size_t len, int syntax, int quotes)
                        if (quotes & QUOTES_ESC) {
                                int n = SIT(c, syntax);
                                if (n == CCTL
-                                || (((quotes & EXP_FULL) || syntax != BASESYNTAX)
-                                    && n == CBACK
-                                   )
+                                || (syntax != BASESYNTAX && n == CBACK)
                                ) {
                                        USTPUTC(CTLESC, q);
                                }
@@ -6346,7 +6440,7 @@ 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);
@@ -6842,8 +6936,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;
@@ -6852,6 +6953,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++;
@@ -7155,14 +7257,13 @@ 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;
@@ -7210,13 +7311,27 @@ 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;
@@ -7281,7 +7396,6 @@ evalvar(char *p, int flag)
        char varflags;
        char subtype;
        int quoted;
-       char easy;
        char *var;
        int patloc;
        int startloc;
@@ -7295,12 +7409,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--;
 
@@ -7346,8 +7459,11 @@ evalvar(char *p, int flag)
 
        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;
        }
@@ -7563,9 +7679,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;
@@ -7598,7 +7721,7 @@ expmeta(char *expdir, char *enddir, char *name)
                                }
                        }
                } else {
-                       if (*p == '\\')
+                       if (*p == '\\' && p[1])
                                esc++;
                        if (p[esc] == '/') {
                                if (metaflag)
@@ -7608,15 +7731,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;
        }
@@ -7624,24 +7747,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 {
@@ -7649,6 +7767,7 @@ expmeta(char *expdir, char *enddir, char *name)
                *endname = '\0';
                endname += esc + 1;
        }
+       name_len -= endname - name;
        matchdot = 0;
        p = start;
        if (*p == '\\')
@@ -7663,16 +7782,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 *
@@ -7745,10 +7878,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;
@@ -7758,13 +7892,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;
@@ -7975,6 +8108,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
@@ -8047,15 +8181,15 @@ static void shellexec(char *prog, char **argv, const char *path, int idx)
 
        /* 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",
@@ -8483,7 +8617,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);
                }
@@ -8922,12 +9057,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();
 
@@ -8954,8 +9094,10 @@ dotrap(void)
                if (!p)
                        continue;
                evalstring(p, 0);
+               exitstatus = status;
        }
-       exitstatus = last_status;
+
+       savestatus = last_status;
        TRACE(("dotrap returns\n"));
 }
 
@@ -8979,8 +9121,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;
@@ -9094,6 +9239,7 @@ evaltree(union node *n, int flags)
        if (flags & EV_EXIT)
                raise_exception(EXEXIT);
 
+       popstackmark(&smark);
        TRACE(("leaving evaltree (no interrupts)\n"));
        return exitstatus;
 }
@@ -9154,14 +9300,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) {
@@ -9178,7 +9322,6 @@ evalfor(union node *n, int flags)
                        break;
        }
        loopnest--;
-       popstackmark(&smark);
 
        return status;
 }
@@ -9189,14 +9332,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);
@@ -9215,8 +9356,6 @@ evalcase(union node *n, int flags)
                }
        }
  out:
-       popstackmark(&smark);
-
        return status;
 }
 
@@ -9393,6 +9532,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.
  */
@@ -9407,8 +9551,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;
 
@@ -9422,8 +9566,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
@@ -9435,10 +9587,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
@@ -9562,9 +9716,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;
@@ -9810,7 +9962,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
@@ -9828,7 +9980,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  },
@@ -9843,8 +9995,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 },
@@ -9889,9 +10041,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)
 {
@@ -9914,8 +10075,8 @@ evalcommand(union node *cmd, int flags)
                "\0\0", bltincmd /* why three NULs? */
        };
        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;
@@ -9937,8 +10098,8 @@ 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;
@@ -10197,7 +10358,6 @@ evalcommand(union node *cmd, int flags)
                goto readstatus;
 
        case CMDFUNCTION:
-               poplocalvars(1);
                /* See above for the rationale */
                dowait(DOWAIT_NONBLOCK, NULL);
                if (evalfun(cmdentry.u.func, argc, argv, flags))
@@ -10211,6 +10371,7 @@ evalcommand(union node *cmd, int flags)
        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
@@ -10219,7 +10380,6 @@ evalcommand(union node *cmd, int flags)
                 */
                setvar0("_", lastarg);
        }
-       popstackmark(&smark);
 
        return status;
 }
@@ -10414,13 +10574,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
@@ -10680,6 +10838,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)
 {
@@ -10733,14 +10897,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);
 }
 
 /*
@@ -10953,6 +11123,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 {
@@ -10967,7 +11139,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;
                }
@@ -10975,14 +11147,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++;
@@ -10993,7 +11168,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;
@@ -11006,26 +11181,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);
                        }
@@ -11116,7 +11305,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) {
@@ -11147,6 +11336,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)
@@ -11612,10 +11827,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);
                                }
@@ -11663,7 +11880,8 @@ simplecmd(void)
        *vpp = NULL;
        *rpp = NULL;
        n = stzalloc(sizeof(struct ncmd));
-       n->type = NCMD;
+       if (NCMD != 0)
+               n->type = NCMD;
        n->ncmd.linno = savelinno;
        n->ncmd.args = args;
        n->ncmd.assign = vars;
@@ -11984,11 +12202,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 +12241,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 +12360,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 +12423,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();
                }
 
@@ -12376,7 +12601,7 @@ parsesub: {
                                STPUTC(c, out);
                                c = pgetc_eatbnl();
                        } while (isdigit(c));
-               } else {
+               } else if (c != '}') {
                        /* $[{[#]]<specialchar>[}] */
                        int cc = c;
 
@@ -12402,7 +12627,8 @@ parsesub: {
                        }
 
                        USTPUTC(cc, out);
-               }
+               } else
+                       goto badsub;
 
                if (c != '}' && subtype == VSLENGTH) {
                        /* ${#VAR didn't end with } */
@@ -12878,9 +13104,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 +13126,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
@@ -13183,8 +13425,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 */
 }
@@ -13530,7 +13774,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 +13952,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 +13988,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) {
@@ -13851,18 +14088,15 @@ 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) {
@@ -13877,7 +14111,7 @@ exitshell(void)
         */
        setjobctl(0);
        flush_stdout_stderr();
-       _exit(status);
+       _exit(savestatus);
        /* NOTREACHED */
 }
 
@@ -13919,6 +14153,7 @@ init(void)
                        }
                }
 
+               setvareq((char*)defifsvar, VTEXTFIXED);
                setvareq((char*)defoptindvar, VTEXTFIXED);
 
                setvar0("PPID", utoa(getppid()));
@@ -13948,7 +14183,7 @@ init(void)
 
 
 //usage:#define ash_trivial_usage
-//usage:       "[-/+OPTIONS] [-/+o OPT]... [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS]]"
+//usage:       "[-/+OPTIONS] [-/+o OPT]... [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS] / -s [ARGS]]"
 //usage:#define ash_full_usage "\n\n"
 //usage:       "Unix shell interpreter"
 
@@ -13965,13 +14200,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 +14221,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++)
@@ -14043,6 +14287,10 @@ reset(void)
        /* from eval.c: */
        evalskip = 0;
        loopnest = 0;
+       if (savestatus >= 0) {
+               exitstatus = savestatus;
+               savestatus = -1;
+       }
 
        /* from expand.c: */
        ifsfree();
@@ -14072,7 +14320,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,9 +14345,6 @@ 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;
@@ -14126,6 +14376,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 +14419,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) {