Bump to version 1.22.1
[platform/upstream/busybox.git] / shell / hush.c
index 4d9e5f8..9271934 100644 (file)
@@ -81,7 +81,6 @@
  *              $ "export" i=`echo 'aaa  bbb'`; echo "$i"
  *              aaa
  */
-#include "busybox.h"  /* for APPLET_IS_NOFORK/NOEXEC */
 #if !(defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) \
        || defined(__APPLE__) \
     )
@@ -93,6 +92,8 @@
 # include <fnmatch.h>
 #endif
 
+#include "busybox.h"  /* for APPLET_IS_NOFORK/NOEXEC */
+#include "unicode.h"
 #include "shell_common.h"
 #include "math.h"
 #include "match.h"
 # define PIPE_BUF 4096  /* amount of buffering in a pipe */
 #endif
 
-//applet:IF_HUSH(APPLET(hush, BB_DIR_BIN, BB_SUID_DROP))
-//applet:IF_MSH(APPLET(msh, BB_DIR_BIN, BB_SUID_DROP))
-//applet:IF_FEATURE_SH_IS_HUSH(APPLET_ODDNAME(sh, hush, BB_DIR_BIN, BB_SUID_DROP, sh))
-//applet:IF_FEATURE_BASH_IS_HUSH(APPLET_ODDNAME(bash, hush, BB_DIR_BIN, BB_SUID_DROP, bash))
-
-//kbuild:lib-$(CONFIG_HUSH) += hush.o match.o shell_common.o
-//kbuild:lib-$(CONFIG_HUSH_RANDOM_SUPPORT) += random.o
-
 //config:config HUSH
 //config:      bool "hush"
 //config:      default y
 //config:        msh is deprecated and will be removed, please migrate to hush.
 //config:
 
+//applet:IF_HUSH(APPLET(hush, BB_DIR_BIN, BB_SUID_DROP))
+//applet:IF_MSH(APPLET(msh, BB_DIR_BIN, BB_SUID_DROP))
+//applet:IF_FEATURE_SH_IS_HUSH(APPLET_ODDNAME(sh, hush, BB_DIR_BIN, BB_SUID_DROP, sh))
+//applet:IF_FEATURE_BASH_IS_HUSH(APPLET_ODDNAME(bash, hush, BB_DIR_BIN, BB_SUID_DROP, bash))
+
+//kbuild:lib-$(CONFIG_HUSH) += hush.o match.o shell_common.o
+//kbuild:lib-$(CONFIG_HUSH_RANDOM_SUPPORT) += random.o
+
 /* -i (interactive) and -s (read stdin) are also accepted,
  * but currently do nothing, therefore aren't shown in help.
  * NOMMU-specific options are not meant to be used by users,
  * therefore we don't show them either.
  */
 //usage:#define hush_trivial_usage
-//usage:       "[-nx] [-c SCRIPT]"
-//usage:#define hush_full_usage ""
+//usage:       "[-nxl] [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS]]"
+//usage:#define hush_full_usage "\n\n"
+//usage:       "Unix shell interpreter"
+
 //usage:#define msh_trivial_usage hush_trivial_usage
-//usage:#define msh_full_usage ""
-//usage:#define sh_trivial_usage NOUSAGE_STR
-//usage:#define sh_full_usage ""
-//usage:#define bash_trivial_usage NOUSAGE_STR
-//usage:#define bash_full_usage ""
+//usage:#define msh_full_usage hush_full_usage
+
+//usage:#if ENABLE_FEATURE_SH_IS_HUSH
+//usage:# define sh_trivial_usage hush_trivial_usage
+//usage:# define sh_full_usage    hush_full_usage
+//usage:#endif
+//usage:#if ENABLE_FEATURE_BASH_IS_HUSH
+//usage:# define bash_trivial_usage hush_trivial_usage
+//usage:# define bash_full_usage    hush_full_usage
+//usage:#endif
 
 
 /* Build knobs */
 # define ENABLE_FEATURE_EDITING 0
 # undef ENABLE_FEATURE_EDITING_FANCY_PROMPT
 # define ENABLE_FEATURE_EDITING_FANCY_PROMPT 0
+# undef ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+# define ENABLE_FEATURE_EDITING_SAVE_ON_EXIT 0
 #endif
 
 /* Do we support ANY keywords? */
@@ -437,8 +447,15 @@ enum {
 /* Used for initialization: o_string foo = NULL_O_STRING; */
 #define NULL_O_STRING { NULL }
 
-/* I can almost use ordinary FILE*.  Is open_memstream() universally
- * available?  Where is it documented? */
+#ifndef debug_printf_parse
+static const char *const assignment_flag[] = {
+       "MAYBE_ASSIGNMENT",
+       "DEFINITELY_ASSIGNMENT",
+       "NOT_ASSIGNMENT",
+       "WORD_IS_KEYWORD",
+};
+#endif
+
 typedef struct in_str {
        const char *p;
        /* eof_flag=1: last char in ->p is really an EOF */
@@ -447,6 +464,7 @@ typedef struct in_str {
 #if ENABLE_HUSH_INTERACTIVE
        smallint promptmode; /* 0: PS1, 1: PS2 */
 #endif
+       int last_char;
        FILE *file;
        int (*get) (struct in_str *) FAST_FUNC;
        int (*peek) (struct in_str *) FAST_FUNC;
@@ -504,7 +522,6 @@ typedef enum redir_type {
 struct command {
        pid_t pid;                  /* 0 if exited */
        int assignment_cnt;         /* how many argv[i] are assignments? */
-       smallint is_stopped;        /* is the command currently running? */
        smallint cmd_type;          /* CMD_xxx */
 #define CMD_NORMAL   0
 #define CMD_SUBSHELL 1
@@ -757,7 +774,6 @@ struct globals {
        smalluint last_exitcode;
        /* are global_argv and global_argv[1..n] malloced? (note: not [0]) */
        smalluint global_args_malloced;
-       smalluint inherited_set_is_saved;
        /* how many non-NULL argv's we have. NB: $# + 1 */
        int global_argc;
        char **global_argv;
@@ -785,15 +801,27 @@ struct globals {
        unsigned handled_SIGCHLD;
        smallint we_have_children;
 #endif
-       /* which signals have non-DFL handler (even with no traps set)? */
-       unsigned non_DFL_mask;
+       /* Which signals have non-DFL handler (even with no traps set)?
+        * Set at the start to:
+        * (SIGQUIT + maybe SPECIAL_INTERACTIVE_SIGS + maybe SPECIAL_JOBSTOP_SIGS)
+        * SPECIAL_INTERACTIVE_SIGS are cleared after fork.
+        * The rest is cleared right before execv syscalls.
+        * Other than these two times, never modified.
+        */
+       unsigned special_sig_mask;
+#if ENABLE_HUSH_JOB
+       unsigned fatal_sig_mask;
+# define G_fatal_sig_mask G.fatal_sig_mask
+#else
+# define G_fatal_sig_mask 0
+#endif
        char **traps; /* char *traps[NSIG] */
-       sigset_t blocked_set;
-       sigset_t inherited_set;
+       sigset_t pending_set;
 #if HUSH_DEBUG
        unsigned long memleak_value;
        int debug_indent;
 #endif
+       struct sigaction sa;
        char user_input_buf[ENABLE_FEATURE_EDITING ? CONFIG_FEATURE_EDITING_MAX_LEN : 2];
 };
 #define G (*ptr_to_globals)
@@ -802,6 +830,9 @@ struct globals {
  * is global, thus "G." prefix is a useful hint */
 #define INIT_G() do { \
        SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
+       /* memset(&G.sa, 0, sizeof(G.sa)); */  \
+       sigfillset(&G.sa.sa_mask); \
+       G.sa.sa_flags = SA_RESTART; \
 } while (0)
 
 
@@ -819,6 +850,9 @@ static int builtin_jobs(char **argv) FAST_FUNC;
 #if ENABLE_HUSH_HELP
 static int builtin_help(char **argv) FAST_FUNC;
 #endif
+#if MAX_HISTORY && ENABLE_FEATURE_EDITING
+static int builtin_history(char **argv) FAST_FUNC;
+#endif
 #if ENABLE_HUSH_LOCAL
 static int builtin_local(char **argv) FAST_FUNC;
 #endif
@@ -888,6 +922,9 @@ static const struct built_in_command bltins1[] = {
 #if ENABLE_HUSH_HELP
        BLTIN("help"     , builtin_help    , NULL),
 #endif
+#if MAX_HISTORY && ENABLE_FEATURE_EDITING
+       BLTIN("history"  , builtin_history , "Show command history"),
+#endif
 #if ENABLE_HUSH_JOB
        BLTIN("jobs"     , builtin_jobs    , "List jobs"),
 #endif
@@ -1069,43 +1106,36 @@ static void die_if_script(unsigned lineno, const char *fmt, ...)
                xfunc_die();
 }
 
-static void syntax_error(unsigned lineno, const char *msg)
+static void syntax_error(unsigned lineno UNUSED_PARAM, const char *msg)
 {
        if (msg)
-               die_if_script(lineno, "syntax error: %s", msg);
+               bb_error_msg("syntax error: %s", msg);
        else
-               die_if_script(lineno, "syntax error", NULL);
+               bb_error_msg("syntax error");
 }
 
-static void syntax_error_at(unsigned lineno, const char *msg)
+static void syntax_error_at(unsigned lineno UNUSED_PARAM, const char *msg)
 {
-       die_if_script(lineno, "syntax error at '%s'", msg);
+       bb_error_msg("syntax error at '%s'", msg);
 }
 
-static void syntax_error_unterm_str(unsigned lineno, const char *s)
+static void syntax_error_unterm_str(unsigned lineno UNUSED_PARAM, const char *s)
 {
-       die_if_script(lineno, "syntax error: unterminated %s", s);
+       bb_error_msg("syntax error: unterminated %s", s);
 }
 
-/* It so happens that all such cases are totally fatal
- * even if shell is interactive: EOF while looking for closing
- * delimiter. There is nowhere to read stuff from after that,
- * it's EOF! The only choice is to terminate.
- */
-static void syntax_error_unterm_ch(unsigned lineno, char ch) NORETURN;
 static void syntax_error_unterm_ch(unsigned lineno, char ch)
 {
        char msg[2] = { ch, '\0' };
        syntax_error_unterm_str(lineno, msg);
-       xfunc_die();
 }
 
-static void syntax_error_unexpected_ch(unsigned lineno, int ch)
+static void syntax_error_unexpected_ch(unsigned lineno UNUSED_PARAM, int ch)
 {
        char msg[2];
        msg[0] = ch;
        msg[1] = '\0';
-       die_if_script(lineno, "syntax error: unexpected %s", ch == EOF ? "EOF" : msg);
+       bb_error_msg("syntax error: unexpected %s", ch == EOF ? "EOF" : msg);
 }
 
 #if HUSH_DEBUG < 2
@@ -1280,7 +1310,7 @@ static void restore_G_args(save_arg_t *sv, char **argv)
  * backgrounds (i.e. stops) or kills all members of currently running
  * pipe.
  *
- * Wait builtin in interruptible by signals for which user trap is set
+ * Wait builtin is interruptible by signals for which user trap is set
  * or by SIGINT in interactive shell.
  *
  * Trap handlers will execute even within trap handlers. (right?)
@@ -1319,12 +1349,14 @@ static void restore_G_args(save_arg_t *sv, char **argv)
  *    "echo $$; sleep 5 & wait; ls -l" + "kill -INT <pid>"
  *    Example 3: this does not wait 5 sec, but executes ls:
  *    "sleep 5; ls -l" + press ^C
+ *    Example 4: this does not wait and does not execute ls:
+ *    "sleep 5 & wait; ls -l" + press ^C
  *
  * (What happens to signals which are IGN on shell start?)
  * (What happens with signal mask on shell start?)
  *
- * Implementation in hush
- * ======================
+ * Old implementation
+ * ==================
  * We use in-kernel pending signal mask to determine which signals were sent.
  * We block all signals which we don't want to take action immediately,
  * i.e. we block all signals which need to have special handling as described
@@ -1332,11 +1364,11 @@ static void restore_G_args(save_arg_t *sv, char **argv)
  * After each pipe execution, we extract any pending signals via sigtimedwait()
  * and act on them.
  *
- * unsigned non_DFL_mask: a mask of such "special" signals
+ * unsigned special_sig_mask: a mask of such "special" signals
  * sigset_t blocked_set:  current blocked signal set
  *
  * "trap - SIGxxx":
- *    clear bit in blocked_set unless it is also in non_DFL_mask
+ *    clear bit in blocked_set unless it is also in special_sig_mask
  * "trap 'cmd' SIGxxx":
  *    set bit in blocked_set (even if 'cmd' is '')
  * after [v]fork, if we plan to be a shell:
@@ -1355,6 +1387,49 @@ static void restore_G_args(save_arg_t *sv, char **argv)
  * Standard says "When a subshell is entered, traps that are not being ignored
  * are set to the default actions". bash interprets it so that traps which
  * are set to '' (ignore) are NOT reset to defaults. We do the same.
+ *
+ * Problem: the above approach makes it unwieldy to catch signals while
+ * we are in read builtin, or while we read commands from stdin:
+ * masked signals are not visible!
+ *
+ * New implementation
+ * ==================
+ * We record each signal we are interested in by installing signal handler
+ * for them - a bit like emulating kernel pending signal mask in userspace.
+ * We are interested in: signals which need to have special handling
+ * as described above, and all signals which have traps set.
+ * Signals are recorded in pending_set.
+ * After each pipe execution, we extract any pending signals
+ * and act on them.
+ *
+ * unsigned special_sig_mask: a mask of shell-special signals.
+ * unsigned fatal_sig_mask: a mask of signals on which we restore tty pgrp.
+ * char *traps[sig] if trap for sig is set (even if it's '').
+ * sigset_t pending_set: set of sigs we received.
+ *
+ * "trap - SIGxxx":
+ *    if sig is in special_sig_mask, set handler back to:
+ *        record_pending_signo, or to IGN if it's a tty stop signal
+ *    if sig is in fatal_sig_mask, set handler back to sigexit.
+ *    else: set handler back to SIG_DFL
+ * "trap 'cmd' SIGxxx":
+ *    set handler to record_pending_signo.
+ * "trap '' SIGxxx":
+ *    set handler to SIG_IGN.
+ * after [v]fork, if we plan to be a shell:
+ *    set signals with special interactive handling to SIG_DFL
+ *    (because child shell is not interactive),
+ *    unset all traps except '' (note: regardless of child shell's type - {}, (), etc)
+ * after [v]fork, if we plan to exec:
+ *    POSIX says fork clears pending signal mask in child - no need to clear it.
+ *
+ * To make wait builtin interruptible, we handle SIGCHLD as special signal,
+ * otherwise (if we leave it SIG_DFL) sigsuspend in wait builtin will not wake up on it.
+ *
+ * Note (compat):
+ * Standard says "When a subshell is entered, traps that are not being ignored
+ * are set to the default actions". bash interprets it so that traps which
+ * are set to '' (ignore) are NOT reset to defaults. We do the same.
  */
 enum {
        SPECIAL_INTERACTIVE_SIGS = 0
@@ -1362,21 +1437,43 @@ enum {
                | (1 << SIGINT)
                | (1 << SIGHUP)
                ,
-       SPECIAL_JOB_SIGS = 0
+       SPECIAL_JOBSTOP_SIGS = 0
 #if ENABLE_HUSH_JOB
                | (1 << SIGTTIN)
                | (1 << SIGTTOU)
                | (1 << SIGTSTP)
 #endif
+               ,
 };
 
-#if ENABLE_HUSH_FAST
-static void SIGCHLD_handler(int sig UNUSED_PARAM)
+static void record_pending_signo(int sig)
 {
-       G.count_SIGCHLD++;
+       sigaddset(&G.pending_set, sig);
+#if ENABLE_HUSH_FAST
+       if (sig == SIGCHLD) {
+               G.count_SIGCHLD++;
 //bb_error_msg("[%d] SIGCHLD_handler: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
-}
+       }
 #endif
+}
+
+static sighandler_t install_sighandler(int sig, sighandler_t handler)
+{
+       struct sigaction old_sa;
+
+       /* We could use signal() to install handlers... almost:
+        * except that we need to mask ALL signals while handlers run.
+        * I saw signal nesting in strace, race window isn't small.
+        * SA_RESTART is also needed, but in Linux, signal()
+        * sets SA_RESTART too.
+        */
+       /* memset(&G.sa, 0, sizeof(G.sa)); - already done */
+       /* sigfillset(&G.sa.sa_mask);      - already done */
+       /* G.sa.sa_flags = SA_RESTART;     - already done */
+       G.sa.sa_handler = handler;
+       sigaction(sig, &G.sa, &old_sa);
+       return old_sa.sa_handler;
+}
 
 #if ENABLE_HUSH_JOB
 
@@ -1393,13 +1490,15 @@ static void SIGCHLD_handler(int sig UNUSED_PARAM)
 static void sigexit(int sig) NORETURN;
 static void sigexit(int sig)
 {
-       /* Disable all signals: job control, SIGPIPE, etc. */
-       sigprocmask_allsigs(SIG_BLOCK);
-
        /* Careful: we can end up here after [v]fork. Do not restore
         * tty pgrp then, only top-level shell process does that */
-       if (G_saved_tty_pgrp && getpid() == G.root_pid)
+       if (G_saved_tty_pgrp && getpid() == G.root_pid) {
+               /* Disable all signals: job control, SIGPIPE, etc.
+                * Mostly paranoid measure, to prevent infinite SIGTTOU.
+                */
+               sigprocmask_allsigs(SIG_BLOCK);
                tcsetpgrp(G_interactive_fd, G_saved_tty_pgrp);
+       }
 
        /* Not a signal, just exit */
        if (sig <= 0)
@@ -1414,15 +1513,43 @@ static void sigexit(int sig)
 
 #endif
 
+static sighandler_t pick_sighandler(unsigned sig)
+{
+       sighandler_t handler = SIG_DFL;
+       if (sig < sizeof(unsigned)*8) {
+               unsigned sigmask = (1 << sig);
+
+#if ENABLE_HUSH_JOB
+               /* is sig fatal? */
+               if (G_fatal_sig_mask & sigmask)
+                       handler = sigexit;
+               else
+#endif
+               /* sig has special handling? */
+               if (G.special_sig_mask & sigmask) {
+                       handler = record_pending_signo;
+                       /* TTIN/TTOU/TSTP can't be set to record_pending_signo
+                        * in order to ignore them: they will be raised
+                        * in an endless loop when we try to do some
+                        * terminal ioctls! We do have to _ignore_ these.
+                        */
+                       if (SPECIAL_JOBSTOP_SIGS & sigmask)
+                               handler = SIG_IGN;
+               }
+       }
+       return handler;
+}
+
 /* Restores tty foreground process group, and exits. */
 static void hush_exit(int exitcode) NORETURN;
 static void hush_exit(int exitcode)
 {
+#if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+       save_history(G.line_input_state);
+#endif
+
        fflush_all();
        if (G.exiting <= 0 && G.traps && G.traps[0] && G.traps[0][0]) {
-               /* Prevent recursion:
-                * trap "echo Hi; exit" EXIT; exit
-                */
                char *argv[3];
                /* argv[0] is unused */
                argv[1] = G.traps[0];
@@ -1459,28 +1586,30 @@ static void hush_exit(int exitcode)
 }
 
 
-static int check_and_run_traps(int sig)
+//TODO: return a mask of ALL handled sigs?
+static int check_and_run_traps(void)
 {
-       /* I want it in rodata, not in bss.
-        * gcc 4.2.1 puts it in rodata only if it has { 0, 0 }
-        * initializer. But other compilers may still use bss.
-        * TODO: find more portable solution.
-        */
-       static const struct timespec zero_timespec = { 0, 0 };
-       smalluint save_rcode;
        int last_sig = 0;
 
-       if (sig)
-               goto jump_in;
        while (1) {
-               sig = sigtimedwait(&G.blocked_set, NULL, &zero_timespec);
-               if (sig <= 0)
+               int sig;
+
+               if (sigisemptyset(&G.pending_set))
                        break;
- jump_in:
-               last_sig = sig;
+               sig = 0;
+               do {
+                       sig++;
+                       if (sigismember(&G.pending_set, sig)) {
+                               sigdelset(&G.pending_set, sig);
+                               goto got_sig;
+                       }
+               } while (sig < NSIG);
+               break;
+ got_sig:
                if (G.traps && G.traps[sig]) {
                        if (G.traps[sig][0]) {
                                /* We have user-defined handler */
+                               smalluint save_rcode;
                                char *argv[3];
                                /* argv[0] is unused */
                                argv[1] = G.traps[sig];
@@ -1488,21 +1617,17 @@ static int check_and_run_traps(int sig)
                                save_rcode = G.last_exitcode;
                                builtin_eval(argv);
                                G.last_exitcode = save_rcode;
+                               last_sig = sig;
                        } /* else: "" trap, ignoring signal */
                        continue;
                }
                /* not a trap: special action */
                switch (sig) {
-#if ENABLE_HUSH_FAST
-               case SIGCHLD:
-                       G.count_SIGCHLD++;
-//bb_error_msg("[%d] check_and_run_traps: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
-                       break;
-#endif
                case SIGINT:
                        /* Builtin was ^C'ed, make it look prettier: */
                        bb_putchar('\n');
                        G.flag_SIGINT = 1;
+                       last_sig = sig;
                        break;
 #if ENABLE_HUSH_JOB
                case SIGHUP: {
@@ -1519,8 +1644,23 @@ static int check_and_run_traps(int sig)
                        sigexit(SIGHUP);
                }
 #endif
+#if ENABLE_HUSH_FAST
+               case SIGCHLD:
+                       G.count_SIGCHLD++;
+//bb_error_msg("[%d] check_and_run_traps: G.count_SIGCHLD:%d G.handled_SIGCHLD:%d", getpid(), G.count_SIGCHLD, G.handled_SIGCHLD);
+                       /* Note:
+                        * We dont do 'last_sig = sig' here -> NOT returning this sig.
+                        * This simplifies wait builtin a bit.
+                        */
+                       break;
+#endif
                default: /* ignored: */
                        /* SIGTERM, SIGQUIT, SIGTTIN, SIGTTOU, SIGTSTP */
+                       /* Note:
+                        * We dont do 'last_sig = sig' here -> NOT returning this sig.
+                        * Example: wait is not interrupted by TERM
+                        * in interactive shell, because TERM is ignored.
+                        */
                        break;
                }
        }
@@ -1842,6 +1982,7 @@ static int FAST_FUNC static_get(struct in_str *i)
        int ch = *i->p;
        if (ch != '\0') {
                i->p++;
+               i->last_char = ch;
                return ch;
        }
        return EOF;
@@ -1899,12 +2040,21 @@ static void get_user_input(struct in_str *i)
        /* Enable command line editing only while a command line
         * is actually being read */
        do {
+               /* Unicode support should be activated even if LANG is set
+                * _during_ shell execution, not only if it was set when
+                * shell was started. Therefore, re-check LANG every time:
+                */
+               const char *s = get_local_var_value("LC_ALL");
+               if (!s) s = get_local_var_value("LC_CTYPE");
+               if (!s) s = get_local_var_value("LANG");
+               reinit_unicode(s);
+
                G.flag_SIGINT = 0;
                /* buglet: SIGINT will not make new prompt to appear _at once_,
                 * only after <Enter>. (^C will work) */
                r = read_line_input(G.line_input_state, prompt_str, G.user_input_buf, CONFIG_FEATURE_EDITING_MAX_LEN-1, /*timeout*/ -1);
                /* catch *SIGINT* etc (^C is handled by read_line_input) */
-               check_and_run_traps(0);
+               check_and_run_traps();
        } while (r == 0 || G.flag_SIGINT); /* repeat if ^C or SIGINT */
        i->eof_flag = (r < 0);
        if (i->eof_flag) { /* EOF/error detected */
@@ -1914,11 +2064,18 @@ static void get_user_input(struct in_str *i)
 # else
        do {
                G.flag_SIGINT = 0;
-               fputs(prompt_str, stdout);
+               if (i->last_char == '\0' || i->last_char == '\n') {
+                       /* Why check_and_run_traps here? Try this interactively:
+                        * $ trap 'echo INT' INT; (sleep 2; kill -INT $$) &
+                        * $ <[enter], repeatedly...>
+                        * Without check_and_run_traps, handler never runs.
+                        */
+                       check_and_run_traps();
+                       fputs(prompt_str, stdout);
+               }
                fflush_all();
                G.user_input_buf[0] = r = fgetc(i->file);
                /*G.user_input_buf[1] = '\0'; - already is and never changed */
-//do we need check_and_run_traps(0)? (maybe only if stdin)
        } while (G.flag_SIGINT);
        i->eof_flag = (r == EOF);
 # endif
@@ -1957,6 +2114,7 @@ static int FAST_FUNC file_get(struct in_str *i)
                do ch = fgetc(i->file); while (ch == '\0');
        }
        debug_printf("file_get: got '%c' %d\n", ch, ch);
+       i->last_char = ch;
        return ch;
 }
 
@@ -2133,7 +2291,7 @@ static void o_addqblock(o_string *o, const char *str, int len)
                        ordinary_cnt = len;
                o_addblock(o, str, ordinary_cnt);
                if (ordinary_cnt == len)
-                       return;
+                       return; /* NUL is already added by o_addblock */
                str += ordinary_cnt;
                len -= ordinary_cnt + 1; /* we are processing + 1 char below */
 
@@ -2147,8 +2305,8 @@ static void o_addqblock(o_string *o, const char *str, int len)
                o_grow_by(o, sz);
                o->data[o->length] = ch;
                o->length++;
-               o->data[o->length] = '\0';
        }
+       o->data[o->length] = '\0';
 }
 
 static void o_addQblock(o_string *o, const char *str, int len)
@@ -2237,6 +2395,7 @@ static int o_save_ptr_helper(o_string *o, int n)
                                n, string_len, string_start);
                o->has_empty_slot = 0;
        }
+       o->has_quoted_part = 0;
        list[n] = (char*)(uintptr_t)string_len;
        return n + 1;
 }
@@ -2894,24 +3053,24 @@ static const struct reserved_combo* match_reserved_word(o_string *word)
         */
        static const struct reserved_combo reserved_list[] = {
 # if ENABLE_HUSH_IF
-               { "!",     RES_NONE,  NOT_ASSIGNMENT , 0 },
-               { "if",    RES_IF,    WORD_IS_KEYWORD, FLAG_THEN | FLAG_START },
-               { "then",  RES_THEN,  WORD_IS_KEYWORD, FLAG_ELIF | FLAG_ELSE | FLAG_FI },
-               { "elif",  RES_ELIF,  WORD_IS_KEYWORD, FLAG_THEN },
-               { "else",  RES_ELSE,  WORD_IS_KEYWORD, FLAG_FI   },
-               { "fi",    RES_FI,    NOT_ASSIGNMENT , FLAG_END  },
+               { "!",     RES_NONE,  NOT_ASSIGNMENT  , 0 },
+               { "if",    RES_IF,    MAYBE_ASSIGNMENT, FLAG_THEN | FLAG_START },
+               { "then",  RES_THEN,  MAYBE_ASSIGNMENT, FLAG_ELIF | FLAG_ELSE | FLAG_FI },
+               { "elif",  RES_ELIF,  MAYBE_ASSIGNMENT, FLAG_THEN },
+               { "else",  RES_ELSE,  MAYBE_ASSIGNMENT, FLAG_FI   },
+               { "fi",    RES_FI,    NOT_ASSIGNMENT  , FLAG_END  },
 # endif
 # if ENABLE_HUSH_LOOPS
-               { "for",   RES_FOR,   NOT_ASSIGNMENT , FLAG_IN | FLAG_DO | FLAG_START },
-               { "while", RES_WHILE, WORD_IS_KEYWORD, FLAG_DO | FLAG_START },
-               { "until", RES_UNTIL, WORD_IS_KEYWORD, FLAG_DO | FLAG_START },
-               { "in",    RES_IN,    NOT_ASSIGNMENT , FLAG_DO   },
-               { "do",    RES_DO,    WORD_IS_KEYWORD, FLAG_DONE },
-               { "done",  RES_DONE,  NOT_ASSIGNMENT , FLAG_END  },
+               { "for",   RES_FOR,   NOT_ASSIGNMENT  , FLAG_IN | FLAG_DO | FLAG_START },
+               { "while", RES_WHILE, MAYBE_ASSIGNMENT, FLAG_DO | FLAG_START },
+               { "until", RES_UNTIL, MAYBE_ASSIGNMENT, FLAG_DO | FLAG_START },
+               { "in",    RES_IN,    NOT_ASSIGNMENT  , FLAG_DO   },
+               { "do",    RES_DO,    MAYBE_ASSIGNMENT, FLAG_DONE },
+               { "done",  RES_DONE,  NOT_ASSIGNMENT  , FLAG_END  },
 # endif
 # if ENABLE_HUSH_CASE
-               { "case",  RES_CASE,  NOT_ASSIGNMENT , FLAG_MATCH | FLAG_START },
-               { "esac",  RES_ESAC,  NOT_ASSIGNMENT , FLAG_END  },
+               { "case",  RES_CASE,  NOT_ASSIGNMENT  , FLAG_MATCH | FLAG_START },
+               { "esac",  RES_ESAC,  NOT_ASSIGNMENT  , FLAG_END  },
 # endif
        };
        const struct reserved_combo *r;
@@ -2977,6 +3136,7 @@ static int reserved_word(o_string *word, struct parse_context *ctx)
        ctx->ctx_res_w = r->res;
        ctx->old_flag = r->flag;
        word->o_assignment = r->assignment_flag;
+       debug_printf_parse("word->o_assignment='%s'\n", assignment_flag[word->o_assignment]);
 
        if (ctx->old_flag & FLAG_END) {
                struct parse_context *old;
@@ -3043,18 +3203,6 @@ static int done_word(o_string *word, struct parse_context *ctx)
                debug_printf_parse("word stored in rd_filename: '%s'\n", word->data);
                ctx->pending_redirect = NULL;
        } else {
-               /* If this word wasn't an assignment, next ones definitely
-                * can't be assignments. Even if they look like ones. */
-               if (word->o_assignment != DEFINITELY_ASSIGNMENT
-                && word->o_assignment != WORD_IS_KEYWORD
-               ) {
-                       word->o_assignment = NOT_ASSIGNMENT;
-               } else {
-                       if (word->o_assignment == DEFINITELY_ASSIGNMENT)
-                               command->assignment_cnt++;
-                       word->o_assignment = MAYBE_ASSIGNMENT;
-               }
-
 #if HAS_KEYWORDS
 # if ENABLE_HUSH_CASE
                if (ctx->ctx_dsemicolon
@@ -3074,8 +3222,9 @@ static int done_word(o_string *word, struct parse_context *ctx)
                 && ctx->ctx_res_w != RES_CASE
 # endif
                ) {
-                       debug_printf_parse("checking '%s' for reserved-ness\n", word->data);
-                       if (reserved_word(word, ctx)) {
+                       int reserved = reserved_word(word, ctx);
+                       debug_printf_parse("checking for reserved-ness: %d\n", reserved);
+                       if (reserved) {
                                o_reset_to_empty_unquoted(word);
                                debug_printf_parse("done_word return %d\n",
                                                (ctx->ctx_res_w == RES_SNTX));
@@ -3096,6 +3245,23 @@ static int done_word(o_string *word, struct parse_context *ctx)
                                        "groups and arglists don't mix\n");
                        return 1;
                }
+
+               /* If this word wasn't an assignment, next ones definitely
+                * can't be assignments. Even if they look like ones. */
+               if (word->o_assignment != DEFINITELY_ASSIGNMENT
+                && word->o_assignment != WORD_IS_KEYWORD
+               ) {
+                       word->o_assignment = NOT_ASSIGNMENT;
+               } else {
+                       if (word->o_assignment == DEFINITELY_ASSIGNMENT) {
+                               command->assignment_cnt++;
+                               debug_printf_parse("++assignment_cnt=%d\n", command->assignment_cnt);
+                       }
+                       debug_printf_parse("word->o_assignment was:'%s'\n", assignment_flag[word->o_assignment]);
+                       word->o_assignment = MAYBE_ASSIGNMENT;
+               }
+               debug_printf_parse("word->o_assignment='%s'\n", assignment_flag[word->o_assignment]);
+
                if (word->has_quoted_part
                 /* optimization: and if it's ("" or '') or ($v... or `cmd`...): */
                 && (word->data[0] == '\0' || word->data[0] == SPECIAL_VAR_SYMBOL)
@@ -3109,14 +3275,6 @@ static int done_word(o_string *word, struct parse_context *ctx)
                        ) {
                                p += 3;
                        }
-                       if (p == word->data || p[0] != '\0') {
-                               /* saw no "$@", or not only "$@" but some
-                                * real text is there too */
-                               /* insert "empty variable" reference, this makes
-                                * e.g. "", $empty"" etc to not disappear */
-                               o_addchr(word, SPECIAL_VAR_SYMBOL);
-                               o_addchr(word, SPECIAL_VAR_SYMBOL);
-                       }
                }
                command->argv = add_string_to_strings(command->argv, xstrdup(word->data));
                debug_print_strings("word appended to argv", command->argv);
@@ -3314,6 +3472,7 @@ static char *fetch_till_str(o_string *as_string,
        int ch;
 
        goto jump_in;
+
        while (1) {
                ch = i_getch(input);
                if (ch != EOF)
@@ -3525,39 +3684,40 @@ static int parse_group(o_string *dest, struct parse_context *ctx,
 
 #if ENABLE_HUSH_TICK || ENABLE_SH_MATH_SUPPORT || ENABLE_HUSH_DOLLAR_OPS
 /* Subroutines for copying $(...) and `...` things */
-static void add_till_backquote(o_string *dest, struct in_str *input, int in_dquote);
+static int add_till_backquote(o_string *dest, struct in_str *input, int in_dquote);
 /* '...' */
-static void add_till_single_quote(o_string *dest, struct in_str *input)
+static int add_till_single_quote(o_string *dest, struct in_str *input)
 {
        while (1) {
                int ch = i_getch(input);
                if (ch == EOF) {
                        syntax_error_unterm_ch('\'');
-                       /*xfunc_die(); - redundant */
+                       return 0;
                }
                if (ch == '\'')
-                       return;
+                       return 1;
                o_addchr(dest, ch);
        }
 }
 /* "...\"...`..`...." - do we need to handle "...$(..)..." too? */
-static void add_till_double_quote(o_string *dest, struct in_str *input)
+static int add_till_double_quote(o_string *dest, struct in_str *input)
 {
        while (1) {
                int ch = i_getch(input);
                if (ch == EOF) {
                        syntax_error_unterm_ch('"');
-                       /*xfunc_die(); - redundant */
+                       return 0;
                }
                if (ch == '"')
-                       return;
+                       return 1;
                if (ch == '\\') {  /* \x. Copy both chars. */
                        o_addchr(dest, ch);
                        ch = i_getch(input);
                }
                o_addchr(dest, ch);
                if (ch == '`') {
-                       add_till_backquote(dest, input, /*in_dquote:*/ 1);
+                       if (!add_till_backquote(dest, input, /*in_dquote:*/ 1))
+                               return 0;
                        o_addchr(dest, ch);
                        continue;
                }
@@ -3578,12 +3738,12 @@ static void add_till_double_quote(o_string *dest, struct in_str *input)
  * Example                               Output
  * echo `echo '\'TEST\`echo ZZ\`BEST`    \TESTZZBEST
  */
-static void add_till_backquote(o_string *dest, struct in_str *input, int in_dquote)
+static int add_till_backquote(o_string *dest, struct in_str *input, int in_dquote)
 {
        while (1) {
                int ch = i_getch(input);
                if (ch == '`')
-                       return;
+                       return 1;
                if (ch == '\\') {
                        /* \x. Copy both unless it is \`, \$, \\ and maybe \" */
                        ch = i_getch(input);
@@ -3597,7 +3757,7 @@ static void add_till_backquote(o_string *dest, struct in_str *input, int in_dquo
                }
                if (ch == EOF) {
                        syntax_error_unterm_ch('`');
-                       /*xfunc_die(); - redundant */
+                       return 0;
                }
                o_addchr(dest, ch);
        }
@@ -3633,7 +3793,7 @@ static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsign
                ch = i_getch(input);
                if (ch == EOF) {
                        syntax_error_unterm_ch(end_ch);
-                       /*xfunc_die(); - redundant */
+                       return 0;
                }
                if (ch == end_ch  IF_HUSH_BASH_COMPAT( || ch == end_char2)) {
                        if (!dbl)
@@ -3647,22 +3807,26 @@ static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsign
                o_addchr(dest, ch);
                if (ch == '(' || ch == '{') {
                        ch = (ch == '(' ? ')' : '}');
-                       add_till_closing_bracket(dest, input, ch);
+                       if (!add_till_closing_bracket(dest, input, ch))
+                               return 0;
                        o_addchr(dest, ch);
                        continue;
                }
                if (ch == '\'') {
-                       add_till_single_quote(dest, input);
+                       if (!add_till_single_quote(dest, input))
+                               return 0;
                        o_addchr(dest, ch);
                        continue;
                }
                if (ch == '"') {
-                       add_till_double_quote(dest, input);
+                       if (!add_till_double_quote(dest, input))
+                               return 0;
                        o_addchr(dest, ch);
                        continue;
                }
                if (ch == '`') {
-                       add_till_backquote(dest, input, /*in_dquote:*/ 0);
+                       if (!add_till_backquote(dest, input, /*in_dquote:*/ 0))
+                               return 0;
                        o_addchr(dest, ch);
                        continue;
                }
@@ -3671,7 +3835,7 @@ static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsign
                        ch = i_getch(input);
                        if (ch == EOF) {
                                syntax_error_unterm_ch(')');
-                               /*xfunc_die(); - redundant */
+                               return 0;
                        }
                        o_addchr(dest, ch);
                        continue;
@@ -3742,8 +3906,8 @@ static int parse_dollar(o_string *as_string,
                ) {
  bad_dollar_syntax:
                        syntax_error_unterm_str("${name}");
-                       debug_printf_parse("parse_dollar return 1: unterminated ${name}\n");
-                       return 1;
+                       debug_printf_parse("parse_dollar return 0: unterminated ${name}\n");
+                       return 0;
                }
                nommu_addchr(as_string, ch);
                ch |= quote_mask;
@@ -3799,6 +3963,8 @@ static int parse_dollar(o_string *as_string,
                                        pos = dest->length;
 #if ENABLE_HUSH_DOLLAR_OPS
                                last_ch = add_till_closing_bracket(dest, input, end_ch);
+                               if (last_ch == 0) /* error? */
+                                       return 0;
 #else
 #error Simple code to only allow ${var} is not implemented
 #endif
@@ -3843,7 +4009,8 @@ static int parse_dollar(o_string *as_string,
                        o_addchr(dest, /*quote_mask |*/ '+');
                        if (!BB_MMU)
                                pos = dest->length;
-                       add_till_closing_bracket(dest, input, ')' | DOUBLE_CLOSE_CHAR_FLAG);
+                       if (!add_till_closing_bracket(dest, input, ')' | DOUBLE_CLOSE_CHAR_FLAG))
+                               return 0; /* error */
                        if (as_string) {
                                o_addstr(as_string, dest->data + pos);
                                o_addchr(as_string, ')');
@@ -3858,7 +4025,8 @@ static int parse_dollar(o_string *as_string,
                o_addchr(dest, quote_mask | '`');
                if (!BB_MMU)
                        pos = dest->length;
-               add_till_closing_bracket(dest, input, ')');
+               if (!add_till_closing_bracket(dest, input, ')'))
+                       return 0; /* error */
                if (as_string) {
                        o_addstr(as_string, dest->data + pos);
                        o_addchr(as_string, ')');
@@ -3885,8 +4053,8 @@ static int parse_dollar(o_string *as_string,
        default:
                o_addQchr(dest, '$');
        }
-       debug_printf_parse("parse_dollar return 0\n");
-       return 0;
+       debug_printf_parse("parse_dollar return 1 (ok)\n");
+       return 1;
 #undef as_string
 }
 
@@ -3927,13 +4095,13 @@ static int encode_string(o_string *as_string,
        if (ch != EOF)
                nommu_addchr(as_string, ch);
        if (ch == dquote_end) { /* may be only '"' or EOF */
-               debug_printf_parse("encode_string return 0\n");
-               return 0;
+               debug_printf_parse("encode_string return 1 (ok)\n");
+               return 1;
        }
        /* note: can't move it above ch == dquote_end check! */
        if (ch == EOF) {
                syntax_error_unterm_ch('"');
-               /*xfunc_die(); - redundant */
+               return 0; /* error */
        }
        next = '\0';
        if (ch != '\n') {
@@ -3964,10 +4132,10 @@ static int encode_string(o_string *as_string,
                goto again;
        }
        if (ch == '$') {
-               if (parse_dollar(as_string, dest, input, /*quote_mask:*/ 0x80) != 0) {
-                       debug_printf_parse("encode_string return 1: "
-                                       "parse_dollar returned non-0\n");
-                       return 1;
+               if (!parse_dollar(as_string, dest, input, /*quote_mask:*/ 0x80)) {
+                       debug_printf_parse("encode_string return 0: "
+                                       "parse_dollar returned 0 (error)\n");
+                       return 0;
                }
                goto again;
        }
@@ -3976,7 +4144,8 @@ static int encode_string(o_string *as_string,
                //unsigned pos = dest->length;
                o_addchr(dest, SPECIAL_VAR_SYMBOL);
                o_addchr(dest, 0x80 | '`');
-               add_till_backquote(dest, input, /*in_dquote:*/ dquote_end == '"');
+               if (!add_till_backquote(dest, input, /*in_dquote:*/ dquote_end == '"'))
+                       return 0; /* error */
                o_addchr(dest, SPECIAL_VAR_SYMBOL);
                //debug_printf_subst("SUBST RES3 '%s'\n", dest->data + pos);
                goto again;
@@ -3991,7 +4160,7 @@ static int encode_string(o_string *as_string,
  * Scan input until EOF or end_trigger char.
  * Return a list of pipes to execute, or NULL on EOF
  * or if end_trigger character is met.
- * On syntax error, exit is shell is not interactive,
+ * On syntax error, exit if shell is not interactive,
  * reset parsing machinery and start parsing anew,
  * or return ERR_PTR.
  */
@@ -4020,8 +4189,6 @@ static struct pipe *parse_stream(char **pstring,
         * here we should use blank chars as separators, not $IFS
         */
 
- reset: /* we come back here only on syntax errors in interactive shell */
-
        if (MAYBE_ASSIGNMENT != 0)
                dest.o_assignment = MAYBE_ASSIGNMENT;
        initialize_context(&ctx);
@@ -4047,8 +4214,8 @@ static struct pipe *parse_stream(char **pstring,
                        /* end_trigger == '}' case errors out earlier,
                         * checking only ')' */
                        if (end_trigger == ')') {
-                               syntax_error_unterm_ch('('); /* exits */
-                               /* goto parse_error; */
+                               syntax_error_unterm_ch('(');
+                               goto parse_error;
                        }
 
                        if (done_word(&dest, &ctx)) {
@@ -4061,7 +4228,7 @@ static struct pipe *parse_stream(char **pstring,
                        /* (this makes bare "&" cmd a no-op.
                         * bash says: "syntax error near unexpected token '&'") */
                        if (pi->num_cmds == 0
-                           IF_HAS_KEYWORDS( && pi->res_word == RES_NONE)
+                       IF_HAS_KEYWORDS(&& pi->res_word == RES_NONE)
                        ) {
                                free_pipe_list(pi);
                                pi = NULL;
@@ -4111,6 +4278,7 @@ static struct pipe *parse_stream(char **pstring,
                         && is_well_formed_var_name(dest.data, '=')
                        ) {
                                dest.o_assignment = DEFINITELY_ASSIGNMENT;
+                               debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
                        }
                        continue;
                }
@@ -4160,6 +4328,7 @@ static struct pipe *parse_stream(char **pstring,
                                        heredoc_cnt = 0;
                                }
                                dest.o_assignment = MAYBE_ASSIGNMENT;
+                               debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
                                ch = ';';
                                /* note: if (is_blank) continue;
                                 * will still trigger for us */
@@ -4209,9 +4378,10 @@ static struct pipe *parse_stream(char **pstring,
                        }
                        done_pipe(&ctx, PIPE_SEQ);
                        dest.o_assignment = MAYBE_ASSIGNMENT;
+                       debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
                        /* Do we sit outside of any if's, loops or case's? */
                        if (!HAS_KEYWORDS
-                        IF_HAS_KEYWORDS(|| (ctx.ctx_res_w == RES_NONE && ctx.old_flag == 0))
+                       IF_HAS_KEYWORDS(|| (ctx.ctx_res_w == RES_NONE && ctx.old_flag == 0))
                        ) {
                                o_free(&dest);
 #if !BB_MMU
@@ -4315,6 +4485,7 @@ static struct pipe *parse_stream(char **pstring,
                        /* ch is a special char and thus this word
                         * cannot be an assignment */
                        dest.o_assignment = NOT_ASSIGNMENT;
+                       debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
                }
 
                /* Note: nommu_addchr(&ctx.as_string, ch) is already done */
@@ -4339,42 +4510,53 @@ static struct pipe *parse_stream(char **pstring,
                        dest.has_quoted_part = 1;
                        break;
                case '$':
-                       if (parse_dollar(&ctx.as_string, &dest, input, /*quote_mask:*/ 0) != 0) {
+                       if (!parse_dollar(&ctx.as_string, &dest, input, /*quote_mask:*/ 0)) {
                                debug_printf_parse("parse_stream parse error: "
-                                       "parse_dollar returned non-0\n");
+                                       "parse_dollar returned 0 (error)\n");
                                goto parse_error;
                        }
                        break;
                case '\'':
                        dest.has_quoted_part = 1;
-                       while (1) {
-                               ch = i_getch(input);
-                               if (ch == EOF) {
-                                       syntax_error_unterm_ch('\'');
-                                       /*xfunc_die(); - redundant */
+                       if (next == '\'' && !ctx.pending_redirect) {
+ insert_empty_quoted_str_marker:
+                               nommu_addchr(&ctx.as_string, next);
+                               i_getch(input); /* eat second ' */
+                               o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+                               o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+                       } else {
+                               while (1) {
+                                       ch = i_getch(input);
+                                       if (ch == EOF) {
+                                               syntax_error_unterm_ch('\'');
+                                               goto parse_error;
+                                       }
+                                       nommu_addchr(&ctx.as_string, ch);
+                                       if (ch == '\'')
+                                               break;
+                                       o_addqchr(&dest, ch);
                                }
-                               nommu_addchr(&ctx.as_string, ch);
-                               if (ch == '\'')
-                                       break;
-                               o_addqchr(&dest, ch);
                        }
                        break;
                case '"':
                        dest.has_quoted_part = 1;
+                       if (next == '"' && !ctx.pending_redirect)
+                               goto insert_empty_quoted_str_marker;
                        if (dest.o_assignment == NOT_ASSIGNMENT)
                                dest.o_expflags |= EXP_FLAG_ESC_GLOB_CHARS;
-                       if (encode_string(&ctx.as_string, &dest, input, '"', /*process_bkslash:*/ 1))
+                       if (!encode_string(&ctx.as_string, &dest, input, '"', /*process_bkslash:*/ 1))
                                goto parse_error;
                        dest.o_expflags &= ~EXP_FLAG_ESC_GLOB_CHARS;
                        break;
 #if ENABLE_HUSH_TICK
                case '`': {
-                       unsigned pos;
+                       USE_FOR_NOMMU(unsigned pos;)
 
                        o_addchr(&dest, SPECIAL_VAR_SYMBOL);
                        o_addchr(&dest, '`');
-                       pos = dest.length;
-                       add_till_backquote(&dest, input, /*in_dquote:*/ 0);
+                       USE_FOR_NOMMU(pos = dest.length;)
+                       if (!add_till_backquote(&dest, input, /*in_dquote:*/ 0))
+                               goto parse_error;
 # if !BB_MMU
                        o_addstr(&ctx.as_string, dest.data + pos);
                        o_addchr(&ctx.as_string, '`');
@@ -4412,6 +4594,7 @@ static struct pipe *parse_stream(char **pstring,
                        /* We just finished a cmd. New one may start
                         * with an assignment */
                        dest.o_assignment = MAYBE_ASSIGNMENT;
+                       debug_printf_parse("dest.o_assignment='%s'\n", assignment_flag[dest.o_assignment]);
                        break;
                case '&':
                        if (done_word(&dest, &ctx)) {
@@ -4512,22 +4695,15 @@ static struct pipe *parse_stream(char **pstring,
                        }
                        IF_HAS_KEYWORDS(pctx = p2;)
                } while (HAS_KEYWORDS && pctx);
-               /* Free text, clear all dest fields */
+
                o_free(&dest);
-               /* If we are not in top-level parse, we return,
-                * our caller will propagate error.
-                */
-               if (end_trigger != ';') {
+               G.last_exitcode = 1;
 #if !BB_MMU
-                       if (pstring)
-                               *pstring = NULL;
+               if (pstring)
+                       *pstring = NULL;
 #endif
-                       debug_leave();
-                       return ERR_PTR;
-               }
-               /* Discard cached input, force prompt */
-               input->p = NULL;
-               goto reset;
+               debug_leave();
+               return ERR_PTR;
        }
 }
 
@@ -4587,12 +4763,22 @@ static void o_addblock_duplicate_backslash(o_string *o, const char *str, int len
 
 /* Store given string, finalizing the word and starting new one whenever
  * we encounter IFS char(s). This is used for expanding variable values.
- * End-of-string does NOT finalize word: think about 'echo -$VAR-' */
-static int expand_on_ifs(o_string *output, int n, const char *str)
+ * End-of-string does NOT finalize word: think about 'echo -$VAR-'.
+ * Return in *ended_with_ifs:
+ * 1 - ended with IFS char, else 0 (this includes case of empty str).
+ */
+static int expand_on_ifs(int *ended_with_ifs, o_string *output, int n, const char *str)
 {
+       int last_is_ifs = 0;
+
        while (1) {
-               int word_len = strcspn(str, G.ifs);
+               int word_len;
+
+               if (!*str)  /* EOL - do not finalize word */
+                       break;
+               word_len = strcspn(str, G.ifs);
                if (word_len) {
+                       /* We have WORD_LEN leading non-IFS chars */
                        if (!(output->o_expflags & EXP_FLAG_GLOB)) {
                                o_addblock(output, str, word_len);
                        } else {
@@ -4605,15 +4791,36 @@ static int expand_on_ifs(o_string *output, int n, const char *str)
                                /*o_addblock(output, str, word_len); - WRONG: "v='\*'; echo Z$v" prints "Z*" instead of "Z\*" */
                                /*o_addqblock(output, str, word_len); - WRONG: "v='*'; echo Z$v" prints "Z*" instead of Z* files */
                        }
+                       last_is_ifs = 0;
                        str += word_len;
+                       if (!*str)  /* EOL - do not finalize word */
+                               break;
                }
+
+               /* We know str here points to at least one IFS char */
+               last_is_ifs = 1;
+               str += strspn(str, G.ifs); /* skip IFS chars */
                if (!*str)  /* EOL - do not finalize word */
                        break;
-               o_addchr(output, '\0');
-               debug_print_list("expand_on_ifs", output, n);
-               n = o_save_ptr(output, n);
-               str += strspn(str, G.ifs); /* skip ifs chars */
+
+               /* Start new word... but not always! */
+               /* Case "v=' a'; echo ''$v": we do need to finalize empty word: */
+               if (output->has_quoted_part
+               /* Case "v=' a'; echo $v":
+                * here nothing precedes the space in $v expansion,
+                * therefore we should not finish the word
+                * (IOW: if there *is* word to finalize, only then do it):
+                */
+                || (n > 0 && output->data[output->length - 1])
+               ) {
+                       o_addchr(output, '\0');
+                       debug_print_list("expand_on_ifs", output, n);
+                       n = o_save_ptr(output, n);
+               }
        }
+
+       if (ended_with_ifs)
+               *ended_with_ifs = last_is_ifs;
        debug_print_list("expand_on_ifs[1]", output, n);
        return n;
 }
@@ -4650,6 +4857,7 @@ static char *encode_then_expand_string(const char *str, int process_bkslash, int
         */
        setup_string_in_str(&input, str);
        encode_string(NULL, &dest, &input, EOF, process_bkslash);
+//TODO: error check (encode_string returns 0 on error)?
        //bb_error_msg("'%s' -> '%s'", str, dest.data);
        exp_str = expand_string_to_string(dest.data, /*unbackslash:*/ do_unbackslash);
        //bb_error_msg("'%s' -> '%s'", dest.data, exp_str);
@@ -5027,6 +5235,7 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg)
         * expansion of right-hand side of assignment == 1-element expand.
         */
        char cant_be_null = 0; /* only bit 0x80 matters */
+       int ended_in_ifs = 0;  /* did last unquoted expansion end with IFS chars? */
        char *p;
 
        debug_printf_expand("expand_vars_to_list: arg:'%s' singleword:%x\n", arg,
@@ -5045,6 +5254,13 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg)
 #if ENABLE_SH_MATH_SUPPORT
                char arith_buf[sizeof(arith_t)*3 + 2];
 #endif
+
+               if (ended_in_ifs) {
+                       o_addchr(output, '\0');
+                       n = o_save_ptr(output, n);
+                       ended_in_ifs = 0;
+               }
+
                o_addblock(output, arg, p - arg);
                debug_print_list("expand_vars_to_list[1]", output, n);
                arg = ++p;
@@ -5073,7 +5289,7 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg)
                        cant_be_null |= first_ch; /* do it for "$@" _now_, when we know it's not empty */
                        if (!(first_ch & 0x80)) { /* unquoted $* or $@ */
                                while (G.global_argv[i]) {
-                                       n = expand_on_ifs(output, n, G.global_argv[i]);
+                                       n = expand_on_ifs(NULL, output, n, G.global_argv[i]);
                                        debug_printf_expand("expand_vars_to_list: argv %d (last %d)\n", i, G.global_argc - 1);
                                        if (G.global_argv[i++][0] && G.global_argv[i]) {
                                                /* this argv[] is not empty and not last:
@@ -5106,11 +5322,13 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg)
                                        if (G.ifs[0])
                                                o_addchr(output, G.ifs[0]);
                                }
+                               output->has_quoted_part = 1;
                        }
                        break;
                }
                case SPECIAL_VAR_SYMBOL: /* <SPECIAL_VAR_SYMBOL><SPECIAL_VAR_SYMBOL> */
                        /* "Empty variable", used to make "" etc to not disappear */
+                       output->has_quoted_part = 1;
                        arg++;
                        cant_be_null = 0x80;
                        break;
@@ -5148,10 +5366,11 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg)
                                debug_printf_expand("unquoted '%s', output->o_escape:%d\n", val,
                                                !!(output->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
                                if (val && val[0]) {
-                                       n = expand_on_ifs(output, n, val);
+                                       n = expand_on_ifs(&ended_in_ifs, output, n, val);
                                        val = NULL;
                                }
                        } else { /* quoted $VAR, val will be appended below */
+                               output->has_quoted_part = 1;
                                debug_printf_expand("quoted '%s', output->o_escape:%d\n", val,
                                                !!(output->o_expflags & EXP_FLAG_ESC_GLOB_CHARS));
                        }
@@ -5176,6 +5395,10 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg)
        } /* end of "while (SPECIAL_VAR_SYMBOL is found) ..." */
 
        if (arg[0]) {
+               if (ended_in_ifs) {
+                       o_addchr(output, '\0');
+                       n = o_save_ptr(output, n);
+               }
                debug_print_list("expand_vars_to_list[a]", output, n);
                /* this part is literal, and it was already pre-quoted
                 * if needed (much earlier), do not use o_addQstr here! */
@@ -5306,6 +5529,25 @@ static char **expand_assignments(char **argv, int count)
 }
 
 
+static void switch_off_special_sigs(unsigned mask)
+{
+       unsigned sig = 0;
+       while ((mask >>= 1) != 0) {
+               sig++;
+               if (!(mask & 1))
+                       continue;
+               if (G.traps) {
+                       if (G.traps[sig] && !G.traps[sig][0])
+                               /* trap is '', has to remain SIG_IGN */
+                               continue;
+                       free(G.traps[sig]);
+                       G.traps[sig] = NULL;
+               }
+               /* We are here only if no trap or trap was not '' */
+               install_sighandler(sig, SIG_DFL);
+       }
+}
+
 #if BB_MMU
 /* never called */
 void re_execute_shell(char ***to_free, const char *s,
@@ -5325,47 +5567,36 @@ static void reset_traps_to_defaults(void)
         * Testcase: (while :; do :; done) + ^Z should background.
         * Same goes for SIGTERM, SIGHUP, SIGINT.
         */
-       if (!G.traps && !(G.non_DFL_mask & SPECIAL_INTERACTIVE_SIGS))
-               return; /* already no traps and no SPECIAL_INTERACTIVE_SIGS */
-
-       /* Switching off SPECIAL_INTERACTIVE_SIGS.
-        * Stupid. It can be done with *single* &= op, but we can't use
-        * the fact that G.blocked_set is implemented as a bitmask
-        * in libc... */
-       mask = (SPECIAL_INTERACTIVE_SIGS >> 1);
-       sig = 1;
-       while (1) {
-               if (mask & 1) {
-                       /* Careful. Only if no trap or trap is not "" */
-                       if (!G.traps || !G.traps[sig] || G.traps[sig][0])
-                               sigdelset(&G.blocked_set, sig);
-               }
-               mask >>= 1;
-               if (!mask)
-                       break;
-               sig++;
-       }
-       /* Our homegrown sig mask is saner to work with :) */
-       G.non_DFL_mask &= ~SPECIAL_INTERACTIVE_SIGS;
+       mask = (G.special_sig_mask & SPECIAL_INTERACTIVE_SIGS) | G_fatal_sig_mask;
+       if (!G.traps && !mask)
+               return; /* already no traps and no special sigs */
 
-       /* Resetting all traps to default except empty ones */
-       mask = G.non_DFL_mask;
-       if (G.traps) for (sig = 0; sig < NSIG; sig++, mask >>= 1) {
-               if (!G.traps[sig] || !G.traps[sig][0])
-                       continue;
+       /* Switch off special sigs */
+       switch_off_special_sigs(mask);
+#if ENABLE_HUSH_JOB
+       G_fatal_sig_mask = 0;
+#endif
+       G.special_sig_mask &= ~SPECIAL_INTERACTIVE_SIGS;
+       /* SIGQUIT,SIGCHLD and maybe SPECIAL_JOBSTOP_SIGS
+        * remain set in G.special_sig_mask */
+
+       if (!G.traps)
+               return;
+
+       /* Reset all sigs to default except ones with empty traps */
+       for (sig = 0; sig < NSIG; sig++) {
+               if (!G.traps[sig])
+                       continue; /* no trap: nothing to do */
+               if (!G.traps[sig][0])
+                       continue; /* empty trap: has to remain SIG_IGN */
+               /* sig has non-empty trap, reset it: */
                free(G.traps[sig]);
                G.traps[sig] = NULL;
-               /* There is no signal for 0 (EXIT) */
+               /* There is no signal for trap 0 (EXIT) */
                if (sig == 0)
                        continue;
-               /* There was a trap handler, we just removed it.
-                * But if sig still has non-DFL handling,
-                * we should not unblock the sig. */
-               if (mask & 1)
-                       continue;
-               sigdelset(&G.blocked_set, sig);
+               install_sighandler(sig, pick_sighandler(sig));
        }
-       sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
 }
 
 #else /* !BB_MMU */
@@ -5476,9 +5707,6 @@ static void re_execute_shell(char ***to_free, const char *s,
         * _inside_ group (just before echo 1), it works.
         *
         * I conclude it means we don't need to pass active traps here.
-        * Even if we would use signal handlers instead of signal masking
-        * in order to implement trap handling,
-        * exec syscall below resets signals to SIG_DFL for us.
         */
        *pp++ = (char *) "-c";
        *pp++ = (char *) s;
@@ -5495,7 +5723,9 @@ static void re_execute_shell(char ***to_free, const char *s,
 
  do_exec:
        debug_printf_exec("re_execute_shell pid:%d cmd:'%s'\n", getpid(), s);
-       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
+       /* Don't propagate SIG_IGN to the child */
+       if (SPECIAL_JOBSTOP_SIGS != 0)
+               switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
        execve(bb_busybox_exec_path, argv, pp);
        /* Fallback. Useful for init=/bin/hush usage etc */
        if (argv[0][0] == '/')
@@ -5531,8 +5761,24 @@ static void parse_and_run_stream(struct in_str *inp, int end_trigger)
                        inp->promptmode = 0; /* PS1 */
 #endif
                pipe_list = parse_stream(NULL, inp, end_trigger);
-               if (!pipe_list) { /* EOF */
-                       if (empty)
+               if (!pipe_list || pipe_list == ERR_PTR) { /* EOF/error */
+                       /* If we are in "big" script
+                        * (not in `cmd` or something similar)...
+                        */
+                       if (pipe_list == ERR_PTR && end_trigger == ';') {
+                               /* Discard cached input (rest of line) */
+                               int ch = inp->last_char;
+                               while (ch != EOF && ch != '\n') {
+                                       //bb_error_msg("Discarded:'%c'", ch);
+                                       ch = i_getch(inp);
+                               }
+                               /* Force prompt */
+                               inp->p = NULL;
+                               /* This stream isn't empty */
+                               empty = 0;
+                               continue;
+                       }
+                       if (!pipe_list && empty)
                                G.last_exitcode = 0;
                        break;
                }
@@ -5540,6 +5786,10 @@ static void parse_and_run_stream(struct in_str *inp, int end_trigger)
                debug_printf_exec("parse_and_run_stream: run_and_free_list\n");
                run_and_free_list(pipe_list);
                empty = 0;
+#if ENABLE_HUSH_FUNCTIONS
+               if (G.flag_return_in_progress == 1)
+                       break;
+#endif
        }
 }
 
@@ -6129,7 +6379,9 @@ static void execvp_or_die(char **argv) NORETURN;
 static void execvp_or_die(char **argv)
 {
        debug_printf_exec("execing '%s'\n", argv[0]);
-       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
+       /* Don't propagate SIG_IGN to the child */
+       if (SPECIAL_JOBSTOP_SIGS != 0)
+               switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
        execvp(argv[0], argv);
        bb_perror_msg("can't execute '%s'", argv[0]);
        _exit(127); /* bash compat */
@@ -6261,7 +6513,9 @@ static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
 # endif
                        /* Re-exec ourselves */
                        debug_printf_exec("re-execing applet '%s'\n", argv[0]);
-                       sigprocmask(SIG_SETMASK, &G.inherited_set, NULL);
+                       /* Don't propagate SIG_IGN to the child */
+                       if (SPECIAL_JOBSTOP_SIGS != 0)
+                               switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
                        execv(bb_busybox_exec_path, argv);
                        /* If they called chroot or otherwise made the binary no longer
                         * executable, fall through */
@@ -6519,7 +6773,6 @@ static int checkjobs(struct pipe *fg_pipe)
                                        }
                                        fg_pipe->cmds[i].cmd_exitcode = ex;
                                } else {
-                                       fg_pipe->cmds[i].is_stopped = 1;
                                        fg_pipe->stopped_cmds++;
                                }
                                debug_printf_jobs("fg_pipe: alive_cmds %d stopped_cmds %d\n",
@@ -6580,7 +6833,6 @@ static int checkjobs(struct pipe *fg_pipe)
                        }
                } else {
                        /* child stopped */
-                       pi->cmds[i].is_stopped = 1;
                        pi->stopped_cmds++;
                }
 #endif
@@ -6960,9 +7212,6 @@ static NOINLINE int run_pipe(struct pipe *pi)
                        if (setup_redirects(command, NULL))
                                _exit(1);
 
-                       /* Restore default handlers just prior to exec */
-                       /*signal(SIGCHLD, SIG_DFL); - so far we don't have any handlers */
-
                        /* Stores to nommu_save list of env vars putenv'ed
                         * (NOMMU, on MMU we don't need that) */
                        /* cast away volatility... */
@@ -7114,7 +7363,7 @@ static int run_list(struct pipe *pi)
                                 * and we should not execute CMD */
                                debug_printf_exec("skipped cmd because of || or &&\n");
                                last_followup = pi->followup;
-                               continue;
+                               goto dont_check_jobs_but_continue;
                        }
                }
                last_followup = pi->followup;
@@ -7240,7 +7489,7 @@ static int run_list(struct pipe *pi)
                                 * and we don't need to wait for anything. */
                                G.last_exitcode = rcode;
                                debug_printf_exec(": builtin/func exitcode %d\n", rcode);
-                               check_and_run_traps(0);
+                               check_and_run_traps();
 #if ENABLE_HUSH_LOOPS
                                /* Was it "break" or "continue"? */
                                if (G.flag_break_continue) {
@@ -7253,8 +7502,10 @@ static int run_list(struct pipe *pi)
                                                        G.flag_break_continue = 0;
                                                /* else: e.g. "continue 2" should *break* once, *then* continue */
                                        } /* else: "while... do... { we are here (innermost list is not a loop!) };...done" */
-                                       if (G.depth_break_continue != 0 || fbc == BC_BREAK)
-                                               goto check_jobs_and_break;
+                                       if (G.depth_break_continue != 0 || fbc == BC_BREAK) {
+                                               checkjobs(NULL);
+                                               break;
+                                       }
                                        /* "continue": simulate end of loop */
                                        rword = RES_DONE;
                                        continue;
@@ -7262,7 +7513,6 @@ static int run_list(struct pipe *pi)
 #endif
 #if ENABLE_HUSH_FUNCTIONS
                                if (G.flag_return_in_progress == 1) {
-                                       /* same as "goto check_jobs_and_break" */
                                        checkjobs(NULL);
                                        break;
                                }
@@ -7272,7 +7522,7 @@ static int run_list(struct pipe *pi)
                                /* even bash 3.2 doesn't do that well with nested bg:
                                 * try "{ { sleep 10; echo DEEP; } & echo HERE; } &".
                                 * I'm NOT treating inner &'s as jobs */
-                               check_and_run_traps(0);
+                               check_and_run_traps();
 #if ENABLE_HUSH_JOB
                                if (G.run_list_level == 1)
                                        insert_bg_job(pi);
@@ -7287,13 +7537,13 @@ static int run_list(struct pipe *pi)
                                        /* Waits for completion, then fg's main shell */
                                        rcode = checkjobs_and_fg_shell(pi);
                                        debug_printf_exec(": checkjobs_and_fg_shell exitcode %d\n", rcode);
-                                       check_and_run_traps(0);
+                                       check_and_run_traps();
                                } else
 #endif
                                { /* This one just waits for completion */
                                        rcode = checkjobs(pi);
                                        debug_printf_exec(": checkjobs exitcode %d\n", rcode);
-                                       check_and_run_traps(0);
+                                       check_and_run_traps();
                                }
                                G.last_exitcode = rcode;
                        }
@@ -7304,30 +7554,31 @@ static int run_list(struct pipe *pi)
                if (rword == RES_IF || rword == RES_ELIF)
                        cond_code = rcode;
 #endif
+ check_jobs_and_continue:
+               checkjobs(NULL);
+ dont_check_jobs_but_continue: ;
 #if ENABLE_HUSH_LOOPS
                /* Beware of "while false; true; do ..."! */
-               if (pi->next && pi->next->res_word == RES_DO) {
+               if (pi->next
+                && (pi->next->res_word == RES_DO || pi->next->res_word == RES_DONE)
+                /* check for RES_DONE is needed for "while ...; do \n done" case */
+               ) {
                        if (rword == RES_WHILE) {
                                if (rcode) {
                                        /* "while false; do...done" - exitcode 0 */
                                        G.last_exitcode = rcode = EXIT_SUCCESS;
                                        debug_printf_exec(": while expr is false: breaking (exitcode:EXIT_SUCCESS)\n");
-                                       goto check_jobs_and_break;
+                                       break;
                                }
                        }
                        if (rword == RES_UNTIL) {
                                if (!rcode) {
                                        debug_printf_exec(": until expr is true: breaking\n");
- check_jobs_and_break:
-                                       checkjobs(NULL);
                                        break;
                                }
                        }
                }
 #endif
-
- check_jobs_and_continue:
-               checkjobs(NULL);
        } /* for (pi) */
 
 #if ENABLE_HUSH_JOB
@@ -7364,88 +7615,81 @@ static int run_and_free_list(struct pipe *pi)
 }
 
 
+static void install_sighandlers(unsigned mask)
+{
+       sighandler_t old_handler;
+       unsigned sig = 0;
+       while ((mask >>= 1) != 0) {
+               sig++;
+               if (!(mask & 1))
+                       continue;
+               old_handler = install_sighandler(sig, pick_sighandler(sig));
+               /* POSIX allows shell to re-enable SIGCHLD
+                * even if it was SIG_IGN on entry.
+                * Therefore we skip IGN check for it:
+                */
+               if (sig == SIGCHLD)
+                       continue;
+               if (old_handler == SIG_IGN) {
+                       /* oops... restore back to IGN, and record this fact */
+                       install_sighandler(sig, old_handler);
+                       if (!G.traps)
+                               G.traps = xzalloc(sizeof(G.traps[0]) * NSIG);
+                       free(G.traps[sig]);
+                       G.traps[sig] = xzalloc(1); /* == xstrdup(""); */
+               }
+       }
+}
+
 /* Called a few times only (or even once if "sh -c") */
-static void init_sigmasks(void)
+static void install_special_sighandlers(void)
 {
-       unsigned sig;
        unsigned mask;
-       sigset_t old_blocked_set;
-
-       if (!G.inherited_set_is_saved) {
-               sigprocmask(SIG_SETMASK, NULL, &G.blocked_set);
-               G.inherited_set = G.blocked_set;
-       }
-       old_blocked_set = G.blocked_set;
 
-       mask = (1 << SIGQUIT);
+       /* Which signals are shell-special? */
+       mask = (1 << SIGQUIT) | (1 << SIGCHLD);
        if (G_interactive_fd) {
-               mask = (1 << SIGQUIT) | SPECIAL_INTERACTIVE_SIGS;
+               mask |= SPECIAL_INTERACTIVE_SIGS;
                if (G_saved_tty_pgrp) /* we have ctty, job control sigs work */
-                       mask |= SPECIAL_JOB_SIGS;
+                       mask |= SPECIAL_JOBSTOP_SIGS;
        }
-       G.non_DFL_mask = mask;
-
-       sig = 0;
-       while (mask) {
-               if (mask & 1)
-                       sigaddset(&G.blocked_set, sig);
-               mask >>= 1;
-               sig++;
+       /* Careful, do not re-install handlers we already installed */
+       if (G.special_sig_mask != mask) {
+               unsigned diff = mask & ~G.special_sig_mask;
+               G.special_sig_mask = mask;
+               install_sighandlers(diff);
        }
-       sigdelset(&G.blocked_set, SIGCHLD);
-
-       if (memcmp(&old_blocked_set, &G.blocked_set, sizeof(old_blocked_set)) != 0)
-               sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
-
-       /* POSIX allows shell to re-enable SIGCHLD
-        * even if it was SIG_IGN on entry */
-#if ENABLE_HUSH_FAST
-       G.count_SIGCHLD++; /* ensure it is != G.handled_SIGCHLD */
-       if (!G.inherited_set_is_saved)
-               signal(SIGCHLD, SIGCHLD_handler);
-#else
-       if (!G.inherited_set_is_saved)
-               signal(SIGCHLD, SIG_DFL);
-#endif
-
-       G.inherited_set_is_saved = 1;
 }
 
 #if ENABLE_HUSH_JOB
 /* helper */
-static void maybe_set_to_sigexit(int sig)
-{
-       void (*handler)(int);
-       /* non_DFL_mask'ed signals are, well, masked,
-        * no need to set handler for them.
-        */
-       if (!((G.non_DFL_mask >> sig) & 1)) {
-               handler = signal(sig, sigexit);
-               if (handler == SIG_IGN) /* oops... restore back to IGN! */
-                       signal(sig, handler);
-       }
-}
 /* Set handlers to restore tty pgrp and exit */
-static void set_fatal_handlers(void)
-{
-       /* We _must_ restore tty pgrp on fatal signals */
-       if (HUSH_DEBUG) {
-               maybe_set_to_sigexit(SIGILL );
-               maybe_set_to_sigexit(SIGFPE );
-               maybe_set_to_sigexit(SIGBUS );
-               maybe_set_to_sigexit(SIGSEGV);
-               maybe_set_to_sigexit(SIGTRAP);
-       } /* else: hush is perfect. what SEGV? */
-       maybe_set_to_sigexit(SIGABRT);
+static void install_fatal_sighandlers(void)
+{
+       unsigned mask;
+
+       /* We will restore tty pgrp on these signals */
+       mask = 0
+               + (1 << SIGILL ) * HUSH_DEBUG
+               + (1 << SIGFPE ) * HUSH_DEBUG
+               + (1 << SIGBUS ) * HUSH_DEBUG
+               + (1 << SIGSEGV) * HUSH_DEBUG
+               + (1 << SIGTRAP) * HUSH_DEBUG
+               + (1 << SIGABRT)
        /* bash 3.2 seems to handle these just like 'fatal' ones */
-       maybe_set_to_sigexit(SIGPIPE);
-       maybe_set_to_sigexit(SIGALRM);
-       /* if we are interactive, SIGHUP, SIGTERM and SIGINT are masked.
+               + (1 << SIGPIPE)
+               + (1 << SIGALRM)
+       /* if we are interactive, SIGHUP, SIGTERM and SIGINT are special sigs.
         * if we aren't interactive... but in this case
-        * we never want to restore pgrp on exit, and this fn is not called */
-       /*maybe_set_to_sigexit(SIGHUP );*/
-       /*maybe_set_to_sigexit(SIGTERM);*/
-       /*maybe_set_to_sigexit(SIGINT );*/
+        * we never want to restore pgrp on exit, and this fn is not called
+        */
+               /*+ (1 << SIGHUP )*/
+               /*+ (1 << SIGTERM)*/
+               /*+ (1 << SIGINT )*/
+       ;
+       G_fatal_sig_mask = mask;
+
+       install_sighandlers(mask);
 }
 #endif
 
@@ -7491,6 +7735,10 @@ static int set_mode(int state, char mode, const char *o_opt)
 int hush_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int hush_main(int argc, char **argv)
 {
+       enum {
+               OPT_login = (1 << 0),
+       };
+       unsigned flags;
        int opt;
        unsigned builtin_argc;
        char **e;
@@ -7498,8 +7746,11 @@ int hush_main(int argc, char **argv)
        struct variable *shell_ver;
 
        INIT_G();
-       if (EXIT_SUCCESS) /* if EXIT_SUCCESS == 0, it is already done */
+       if (EXIT_SUCCESS != 0) /* if EXIT_SUCCESS == 0, it is already done */
                G.last_exitcode = EXIT_SUCCESS;
+#if ENABLE_HUSH_FAST
+       G.count_SIGCHLD++; /* ensure it is != G.handled_SIGCHLD */
+#endif
 #if !BB_MMU
        G.argv0_for_re_execing = argv[0];
 #endif
@@ -7573,22 +7824,8 @@ int hush_main(int argc, char **argv)
 
 #if ENABLE_FEATURE_EDITING
        G.line_input_state = new_line_input_t(FOR_SHELL);
-# if defined MAX_HISTORY && MAX_HISTORY > 0 && ENABLE_HUSH_SAVEHISTORY
-       {
-               const char *hp = get_local_var_value("HISTFILE");
-               if (!hp) {
-                       hp = get_local_var_value("HOME");
-                       if (hp) {
-                               G.line_input_state->hist_file = concat_path_file(hp, ".hush_history");
-                               //set_local_var(xasprintf("HISTFILE=%s", ...));
-                       }
-               }
-       }
-# endif
 #endif
 
-       G.global_argc = argc;
-       G.global_argv = argv;
        /* Initialize some more globals to non-zero values */
        cmdedit_update_prompt();
 
@@ -7600,17 +7837,18 @@ int hush_main(int argc, char **argv)
        }
 
        /* Shell is non-interactive at first. We need to call
-        * init_sigmasks() if we are going to execute "sh <script>",
+        * install_special_sighandlers() if we are going to execute "sh <script>",
         * "sh -c <cmds>" or login shell's /etc/profile and friends.
-        * If we later decide that we are interactive, we run init_sigmasks()
+        * If we later decide that we are interactive, we run install_special_sighandlers()
         * in order to intercept (more) signals.
         */
 
        /* Parse options */
        /* http://www.opengroup.org/onlinepubs/9699919799/utilities/sh.html */
+       flags = (argv[0] && argv[0][0] == '-') ? OPT_login : 0;
        builtin_argc = 0;
        while (1) {
-               opt = getopt(argc, argv, "+c:xins"
+               opt = getopt(argc, argv, "+c:xinsl"
 #if !BB_MMU
                                "<:$:R:V:"
 # if ENABLE_HUSH_FUNCTIONS
@@ -7642,7 +7880,7 @@ int hush_main(int argc, char **argv)
                                /* -c 'builtin' [BARGV...] "" ARG0 [ARG1...] */
                                const struct built_in_command *x;
 
-                               init_sigmasks();
+                               install_special_sighandlers();
                                x = find_builtin(optarg);
                                if (x) { /* paranoia */
                                        G.global_argc -= builtin_argc; /* skip [BARGV...] "" */
@@ -7659,7 +7897,7 @@ int hush_main(int argc, char **argv)
                                G.global_argv[0] = argv[0];
                                G.global_argc++;
                        } /* else -c 'script' ARG0 [ARG1...]: $0 is ARG0 */
-                       init_sigmasks();
+                       install_special_sighandlers();
                        parse_and_run_string(optarg);
                        goto final_return;
                case 'i':
@@ -7671,6 +7909,9 @@ int hush_main(int argc, char **argv)
                        /* "-s" means "read from stdin", but this is how we always
                         * operate, so simply do nothing here. */
                        break;
+               case 'l':
+                       flags |= OPT_login;
+                       break;
 #if !BB_MMU
                case '<': /* "big heredoc" support */
                        full_write1_str(optarg);
@@ -7691,15 +7932,14 @@ int hush_main(int argc, char **argv)
                        empty_trap_mask = bb_strtoull(optarg, &optarg, 16);
                        if (empty_trap_mask != 0) {
                                int sig;
-                               init_sigmasks();
+                               install_special_sighandlers();
                                G.traps = xzalloc(sizeof(G.traps[0]) * NSIG);
                                for (sig = 1; sig < NSIG; sig++) {
                                        if (empty_trap_mask & (1LL << sig)) {
                                                G.traps[sig] = xzalloc(1); /* == xstrdup(""); */
-                                               sigaddset(&G.blocked_set, sig);
+                                               install_sighandler(sig, SIG_IGN);
                                        }
                                }
-                               sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
                        }
 # if ENABLE_HUSH_LOOPS
                        optarg++;
@@ -7737,19 +7977,24 @@ int hush_main(int argc, char **argv)
                }
        } /* option parsing loop */
 
+       /* Skip options. Try "hush -l": $1 should not be "-l"! */
+       G.global_argc = argc - (optind - 1);
+       G.global_argv = argv + (optind - 1);
+       G.global_argv[0] = argv[0];
+
        if (!G.root_pid) {
                G.root_pid = getpid();
                G.root_ppid = getppid();
        }
 
        /* If we are login shell... */
-       if (argv[0] && argv[0][0] == '-') {
+       if (flags & OPT_login) {
                FILE *input;
                debug_printf("sourcing /etc/profile\n");
                input = fopen_for_read("/etc/profile");
                if (input != NULL) {
                        close_on_exec_on(fileno(input));
-                       init_sigmasks();
+                       install_special_sighandlers();
                        parse_and_run_file(input);
                        fclose(input);
                }
@@ -7762,19 +8007,19 @@ int hush_main(int argc, char **argv)
                 */
        }
 
-       if (argv[optind]) {
+       if (G.global_argv[1]) {
                FILE *input;
                /*
                 * "bash <script>" (which is never interactive (unless -i?))
                 * sources $BASH_ENV here (without scanning $PATH).
                 * If called as sh, does the same but with $ENV.
                 */
-               debug_printf("running script '%s'\n", argv[optind]);
-               G.global_argv = argv + optind;
-               G.global_argc = argc - optind;
-               input = xfopen_for_read(argv[optind]);
+               G.global_argc--;
+               G.global_argv++;
+               debug_printf("running script '%s'\n", G.global_argv[0]);
+               input = xfopen_for_read(G.global_argv[0]);
                close_on_exec_on(fileno(input));
-               init_sigmasks();
+               install_special_sighandlers();
                parse_and_run_file(input);
 #if ENABLE_FEATURE_CLEAN_UP
                fclose(input);
@@ -7783,7 +8028,7 @@ int hush_main(int argc, char **argv)
        }
 
        /* Up to here, shell was non-interactive. Now it may become one.
-        * NB: don't forget to (re)run init_sigmasks() as needed.
+        * NB: don't forget to (re)run install_special_sighandlers() as needed.
         */
 
        /* A shell is interactive if the '-i' flag was given,
@@ -7835,12 +8080,12 @@ int hush_main(int argc, char **argv)
                        }
                }
 
-               /* Block some signals */
-               init_sigmasks();
+               /* Install more signal handlers */
+               install_special_sighandlers();
 
                if (G_saved_tty_pgrp) {
                        /* Set other signals to restore saved_tty_pgrp */
-                       set_fatal_handlers();
+                       install_fatal_sighandlers();
                        /* Put ourselves in our own process group
                         * (bash, too, does this only if ctty is available) */
                        bb_setpgrp(); /* is the same as setpgid(our_pid, our_pid); */
@@ -7850,8 +8095,29 @@ int hush_main(int argc, char **argv)
                /* -1 is special - makes xfuncs longjmp, not exit
                 * (we reset die_sleep = 0 whereever we [v]fork) */
                enable_restore_tty_pgrp_on_exit(); /* sets die_sleep = -1 */
+
+# if ENABLE_HUSH_SAVEHISTORY && MAX_HISTORY > 0
+               {
+                       const char *hp = get_local_var_value("HISTFILE");
+                       if (!hp) {
+                               hp = get_local_var_value("HOME");
+                               if (hp)
+                                       hp = concat_path_file(hp, ".hush_history");
+                       } else {
+                               hp = xstrdup(hp);
+                       }
+                       if (hp) {
+                               G.line_input_state->hist_file = hp;
+                               //set_local_var(xasprintf("HISTFILE=%s", ...));
+                       }
+#  if ENABLE_FEATURE_SH_HISTFILESIZE
+                       hp = get_local_var_value("HISTFILESIZE");
+                       G.line_input_state->max_history = size_from_HISTFILESIZE(hp);
+#  endif
+               }
+# endif
        } else {
-               init_sigmasks();
+               install_special_sighandlers();
        }
 #elif ENABLE_HUSH_INTERACTIVE
        /* No job control compiled in, only prompt/line editing */
@@ -7868,10 +8134,10 @@ int hush_main(int argc, char **argv)
        if (G_interactive_fd) {
                close_on_exec_on(G_interactive_fd);
        }
-       init_sigmasks();
+       install_special_sighandlers();
 #else
        /* We have interactiveness code disabled */
-       init_sigmasks();
+       install_special_sighandlers();
 #endif
        /* bash:
         * if interactive but not a login shell, sources ~/.bashrc
@@ -8005,7 +8271,7 @@ static int FAST_FUNC builtin_exec(char **argv)
                tcsetpgrp(G_interactive_fd, G_saved_tty_pgrp);
 
        /* TODO: if exec fails, bash does NOT exit! We do.
-        * We'll need to undo sigprocmask (it's inside execvp_or_die)
+        * We'll need to undo trap cleanup (it's inside execvp_or_die)
         * and tcsetpgrp, and this is inherently racy.
         */
        execvp_or_die(argv);
@@ -8023,7 +8289,7 @@ static int FAST_FUNC builtin_exit(char **argv)
         * (if there are _stopped_ jobs, running ones don't count)
         * # exit
         * exit
-        # EEE (then bash exits)
+        * EEE (then bash exits)
         *
         * TODO: we can use G.exiting = -1 as indicator "last cmd was exit"
         */
@@ -8202,6 +8468,8 @@ static int FAST_FUNC builtin_trap(char **argv)
  process_sig_list:
                ret = EXIT_SUCCESS;
                while (*argv) {
+                       sighandler_t handler;
+
                        sig = get_signum(*argv++);
                        if (sig < 0 || sig >= NSIG) {
                                ret = EXIT_FAILURE;
@@ -8220,18 +8488,13 @@ static int FAST_FUNC builtin_trap(char **argv)
                        if (sig == 0)
                                continue;
 
-                       if (new_cmd) {
-                               sigaddset(&G.blocked_set, sig);
-                       } else {
-                               /* There was a trap handler, we are removing it
-                                * (if sig has non-DFL handling,
-                                * we don't need to do anything) */
-                               if (sig < 32 && (G.non_DFL_mask & (1 << sig)))
-                                       continue;
-                               sigdelset(&G.blocked_set, sig);
-                       }
+                       if (new_cmd)
+                               handler = (new_cmd[0] ? record_pending_signo : SIG_IGN);
+                       else
+                               /* We are removing trap handler */
+                               handler = pick_sighandler(sig);
+                       install_sighandler(sig, handler);
                }
-               sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
                return ret;
        }
 
@@ -8336,7 +8599,6 @@ static int FAST_FUNC builtin_fg_bg(char **argv)
        debug_printf_jobs("reviving %d procs, pgrp %d\n", pi->num_cmds, pi->pgrp);
        for (i = 0; i < pi->num_cmds; i++) {
                debug_printf_jobs("reviving pid %d\n", pi->cmds[i].pid);
-               pi->cmds[i].is_stopped = 0;
        }
        pi->stopped_cmds = 0;
 
@@ -8374,6 +8636,14 @@ static int FAST_FUNC builtin_help(char **argv UNUSED_PARAM)
 }
 #endif
 
+#if MAX_HISTORY && ENABLE_FEATURE_EDITING
+static int FAST_FUNC builtin_history(char **argv UNUSED_PARAM)
+{
+       show_history(G.line_input_state);
+       return EXIT_SUCCESS;
+}
+#endif
+
 #if ENABLE_HUSH_JOB
 static int FAST_FUNC builtin_jobs(char **argv UNUSED_PARAM)
 {
@@ -8432,6 +8702,27 @@ static int FAST_FUNC builtin_pwd(char **argv UNUSED_PARAM)
        return EXIT_SUCCESS;
 }
 
+/* Interruptibility of read builtin in bash
+ * (tested on bash-4.2.8 by sending signals (not by ^C)):
+ *
+ * Empty trap makes read ignore corresponding signal, for any signal.
+ *
+ * SIGINT:
+ * - terminates non-interactive shell;
+ * - interrupts read in interactive shell;
+ * if it has non-empty trap:
+ * - executes trap and returns to command prompt in interactive shell;
+ * - executes trap and returns to read in non-interactive shell;
+ * SIGTERM:
+ * - is ignored (does not interrupt) read in interactive shell;
+ * - terminates non-interactive shell;
+ * if it has non-empty trap:
+ * - executes trap and returns to read;
+ * SIGHUP:
+ * - terminates shell (regardless of interactivity);
+ * if it has non-empty trap:
+ * - executes trap and returns to read;
+ */
 static int FAST_FUNC builtin_read(char **argv)
 {
        const char *r;
@@ -8439,6 +8730,7 @@ static int FAST_FUNC builtin_read(char **argv)
        char *opt_p = NULL;
        char *opt_t = NULL;
        char *opt_u = NULL;
+       const char *ifs;
        int read_flags;
 
        /* "!": do not abort on errors.
@@ -8448,10 +8740,12 @@ static int FAST_FUNC builtin_read(char **argv)
        if (read_flags == (uint32_t)-1)
                return EXIT_FAILURE;
        argv += optind;
+       ifs = get_local_var_value("IFS"); /* can be NULL */
 
+ again:
        r = shell_builtin_read(set_local_var_from_halves,
                argv,
-               get_local_var_value("IFS"), /* can be NULL */
+               ifs,
                read_flags,
                opt_n,
                opt_p,
@@ -8459,6 +8753,12 @@ static int FAST_FUNC builtin_read(char **argv)
                opt_u
        );
 
+       if ((uintptr_t)r == 1 && errno == EINTR) {
+               unsigned sig = check_and_run_traps();
+               if (sig && sig != SIGINT)
+                       goto again;
+       }
+
        if ((uintptr_t)r > 1) {
                bb_error_msg("%s", r);
                r = (char*)(uintptr_t)1;
@@ -8596,6 +8896,9 @@ static int FAST_FUNC builtin_source(char **argv)
        free(arg_path);
        if (!input) {
                /* bb_perror_msg("%s", *argv); - done by fopen_or_warn */
+               /* POSIX: non-interactive shell should abort here,
+                * not merely fail. So far no one complained :)
+                */
                return EXIT_FAILURE;
        }
        close_on_exec_on(fileno(input));
@@ -8605,12 +8908,14 @@ static int FAST_FUNC builtin_source(char **argv)
        /* "we are inside sourced file, ok to use return" */
        G.flag_return_in_progress = -1;
 #endif
-       save_and_replace_G_args(&sv, argv);
+       if (argv[1])
+               save_and_replace_G_args(&sv, argv);
 
        parse_and_run_file(input);
        fclose(input);
 
-       restore_G_args(&sv, argv);
+       if (argv[1])
+               restore_G_args(&sv, argv);
 #if ENABLE_HUSH_FUNCTIONS
        G.flag_return_in_progress = sv_flg;
 #endif
@@ -8691,7 +8996,7 @@ static int FAST_FUNC builtin_unset(char **argv)
 static int FAST_FUNC builtin_wait(char **argv)
 {
        int ret = EXIT_SUCCESS;
-       int status, sig;
+       int status;
 
        argv = skip_dash_dash(argv);
        if (argv[0] == NULL) {
@@ -8711,25 +9016,53 @@ static int FAST_FUNC builtin_wait(char **argv)
                 * ^C <-- after ~4 sec from keyboard
                 * $
                 */
-               sigaddset(&G.blocked_set, SIGCHLD);
-               sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
                while (1) {
-                       checkjobs(NULL);
-                       if (errno == ECHILD)
+                       int sig;
+                       sigset_t oldset, allsigs;
+
+                       /* waitpid is not interruptible by SA_RESTARTed
+                        * signals which we use. Thus, this ugly dance:
+                        */
+
+                       /* Make sure possible SIGCHLD is stored in kernel's
+                        * pending signal mask before we call waitpid.
+                        * Or else we may race with SIGCHLD, lose it,
+                        * and get stuck in sigwaitinfo...
+                        */
+                       sigfillset(&allsigs);
+                       sigprocmask(SIG_SETMASK, &allsigs, &oldset);
+
+                       if (!sigisemptyset(&G.pending_set)) {
+                               /* Crap! we raced with some signal! */
+                       //      sig = 0;
+                               goto restore;
+                       }
+
+                       checkjobs(NULL); /* waitpid(WNOHANG) inside */
+                       if (errno == ECHILD) {
+                               sigprocmask(SIG_SETMASK, &oldset, NULL);
+                               break;
+                       }
+
+                       /* Wait for SIGCHLD or any other signal */
+                       //sig = sigwaitinfo(&allsigs, NULL);
+                       /* It is vitally important for sigsuspend that SIGCHLD has non-DFL handler! */
+                       /* Note: sigsuspend invokes signal handler */
+                       sigsuspend(&oldset);
+ restore:
+                       sigprocmask(SIG_SETMASK, &oldset, NULL);
+
+                       /* So, did we get a signal? */
+                       //if (sig > 0)
+                       //      raise(sig); /* run handler */
+                       sig = check_and_run_traps();
+                       if (sig /*&& sig != SIGCHLD - always true */) {
+                               /* see note 2 */
+                               ret = 128 + sig;
                                break;
-                       /* Wait for SIGCHLD or any other signal of interest */
-                       /* sigtimedwait with infinite timeout: */
-                       sig = sigwaitinfo(&G.blocked_set, NULL);
-                       if (sig > 0) {
-                               sig = check_and_run_traps(sig);
-                               if (sig && sig != SIGCHLD) { /* see note 2 */
-                                       ret = 128 + sig;
-                                       break;
-                               }
                        }
+                       /* SIGCHLD, or no signal, or ignored one, such as SIGQUIT. Repeat */
                }
-               sigdelset(&G.blocked_set, SIGCHLD);
-               sigprocmask(SIG_SETMASK, &G.blocked_set, NULL);
                return ret;
        }