unicode: check $LC_CTYPE too to detect Unicode mode
[platform/upstream/busybox.git] / shell / hush.c
index 89a13fe..fc9b89b 100644 (file)
 # define PIPE_BUF 4096  /* amount of buffering in a pipe */
 #endif
 
-/* Not every libc has sighandler_t. Fix it */
-typedef void (*hush_sighandler_t)(int);
-#define sighandler_t hush_sighandler_t
-
 //config:config HUSH
 //config:      bool "hush"
 //config:      default y
@@ -324,6 +320,8 @@ typedef void (*hush_sighandler_t)(int);
 # 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? */
@@ -449,6 +447,15 @@ enum {
 /* Used for initialization: o_string foo = NULL_O_STRING; */
 #define NULL_O_STRING { NULL }
 
+#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 */
@@ -515,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
@@ -805,9 +811,9 @@ struct globals {
        unsigned special_sig_mask;
 #if ENABLE_HUSH_JOB
        unsigned fatal_sig_mask;
-#define G_fatal_sig_mask G.fatal_sig_mask
+# define G_fatal_sig_mask G.fatal_sig_mask
 #else
-#define G_fatal_sig_mask 0
+# define G_fatal_sig_mask 0
 #endif
        char **traps; /* char *traps[NSIG] */
        sigset_t pending_set;
@@ -815,6 +821,7 @@ struct globals {
        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)
@@ -823,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)
 
 
@@ -840,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
@@ -909,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
@@ -1441,6 +1457,24 @@ static void record_pending_signo(int sig)
 #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
 
 /* After [v]fork, in child: do not restore tty pgrp on xfunc death */
@@ -1456,13 +1490,15 @@ static void record_pending_signo(int sig)
 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)
@@ -1484,12 +1520,13 @@ static sighandler_t pick_sighandler(unsigned sig)
                unsigned sigmask = (1 << sig);
 
 #if ENABLE_HUSH_JOB
-               /* sig is fatal? */
+               /* is sig fatal? */
                if (G_fatal_sig_mask & sigmask)
                        handler = sigexit;
+               else
 #endif
                /* sig has special handling? */
-               else if (G.special_sig_mask & sigmask) {
+               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
@@ -1507,6 +1544,10 @@ static sighandler_t pick_sighandler(unsigned sig)
 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]) {
                char *argv[3];
@@ -2003,7 +2044,10 @@ static void get_user_input(struct in_str *i)
                 * _during_ shell execution, not only if it was set when
                 * shell was started. Therefore, re-check LANG every time:
                 */
-               reinit_unicode(get_local_var_value("LANG"));
+               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_,
@@ -2247,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 */
 
@@ -2261,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)
@@ -2351,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;
 }
@@ -3008,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;
@@ -3091,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;
@@ -3157,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
@@ -3188,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));
@@ -3210,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)
@@ -3223,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);
@@ -4184,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;
@@ -4234,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;
                }
@@ -4283,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 */
@@ -4332,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
@@ -4438,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 */
@@ -4470,20 +4518,30 @@ static struct pipe *parse_stream(char **pstring,
                        break;
                case '\'':
                        dest.has_quoted_part = 1;
-                       while (1) {
-                               ch = i_getch(input);
-                               if (ch == EOF) {
-                                       syntax_error_unterm_ch('\'');
-                                       goto parse_error;
+                       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))
@@ -4492,11 +4550,11 @@ static struct pipe *parse_stream(char **pstring,
                        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;
+                       USE_FOR_NOMMU(pos = dest.length;)
                        if (!add_till_backquote(&dest, input, /*in_dquote:*/ 0))
                                goto parse_error;
 # if !BB_MMU
@@ -4536,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)) {
@@ -4704,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 {
@@ -4722,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;
 }
@@ -5145,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,
@@ -5163,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;
@@ -5191,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:
@@ -5224,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;
@@ -5266,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));
                        }
@@ -5294,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! */
@@ -5424,12 +5529,6 @@ static char **expand_assignments(char **argv, int count)
 }
 
 
-#if BB_MMU
-/* never called */
-void re_execute_shell(char ***to_free, const char *s,
-               char *g_argv0, char **g_argv,
-               char **builtin_argv) NORETURN;
-
 static void switch_off_special_sigs(unsigned mask)
 {
        unsigned sig = 0;
@@ -5445,10 +5544,16 @@ static void switch_off_special_sigs(unsigned mask)
                        G.traps[sig] = NULL;
                }
                /* We are here only if no trap or trap was not '' */
-               signal(sig, SIG_DFL);
+               install_sighandler(sig, SIG_DFL);
        }
 }
 
+#if BB_MMU
+/* never called */
+void re_execute_shell(char ***to_free, const char *s,
+               char *g_argv0, char **g_argv,
+               char **builtin_argv) NORETURN;
+
 static void reset_traps_to_defaults(void)
 {
        /* This function is always called in a child shell
@@ -5490,7 +5595,7 @@ static void reset_traps_to_defaults(void)
                /* There is no signal for trap 0 (EXIT) */
                if (sig == 0)
                        continue;
-               signal(sig, pick_sighandler(sig));
+               install_sighandler(sig, pick_sighandler(sig));
        }
 }
 
@@ -5602,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;
@@ -5621,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);
-       switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
+       /* 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] == '/')
@@ -6275,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]);
-       switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
+       /* 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 */
@@ -6407,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]);
-                       switch_off_special_sigs(G.special_sig_mask & SPECIAL_JOBSTOP_SIGS);
+                       /* 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 */
@@ -6665,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",
@@ -6726,7 +6833,6 @@ static int checkjobs(struct pipe *fg_pipe)
                        }
                } else {
                        /* child stopped */
-                       pi->cmds[i].is_stopped = 1;
                        pi->stopped_cmds++;
                }
 #endif
@@ -7257,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;
@@ -7396,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;
@@ -7405,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;
                                }
@@ -7447,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
@@ -7515,7 +7623,7 @@ static void install_sighandlers(unsigned mask)
                sig++;
                if (!(mask & 1))
                        continue;
-               old_handler = signal(sig, pick_sighandler(sig));
+               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:
@@ -7524,7 +7632,7 @@ static void install_sighandlers(unsigned mask)
                        continue;
                if (old_handler == SIG_IGN) {
                        /* oops... restore back to IGN, and record this fact */
-                       signal(sig, old_handler);
+                       install_sighandler(sig, old_handler);
                        if (!G.traps)
                                G.traps = xzalloc(sizeof(G.traps[0]) * NSIG);
                        free(G.traps[sig]);
@@ -7716,22 +7824,6 @@ 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", ...));
-                       }
-               }
-# if ENABLE_FEATURE_SH_HISTFILESIZE
-               hp = get_local_var_value("HISTFILESIZE");
-               G.line_input_state->max_history = size_from_HISTFILESIZE(hp);
-# endif
-       }
-# endif
 #endif
 
        /* Initialize some more globals to non-zero values */
@@ -7845,7 +7937,7 @@ int hush_main(int argc, char **argv)
                                for (sig = 1; sig < NSIG; sig++) {
                                        if (empty_trap_mask & (1LL << sig)) {
                                                G.traps[sig] = xzalloc(1); /* == xstrdup(""); */
-                                               signal(sig, SIG_IGN);
+                                               install_sighandler(sig, SIG_IGN);
                                        }
                                }
                        }
@@ -8003,6 +8095,27 @@ 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 {
                install_special_sighandlers();
        }
@@ -8176,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"
         */
@@ -8380,7 +8493,7 @@ static int FAST_FUNC builtin_trap(char **argv)
                        else
                                /* We are removing trap handler */
                                handler = pick_sighandler(sig);
-                       signal(sig, handler);
+                       install_sighandler(sig, handler);
                }
                return ret;
        }
@@ -8486,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;
 
@@ -8524,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)
 {
@@ -8776,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));
@@ -8785,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