od_bloaty: fix debug code
[oweals/busybox.git] / shell / hush.c
index c0325cf5e416901c684ea9d3547b57d3846e938d..7b83c736c1ec8e77632be08d8226f9c58496ebd6 100644 (file)
  *
  * TODOs:
  *      grep for "TODO" and fix (some of them are easy)
+ *      make complex ${var%...} constructs support optional
+ *      make here documents optional
  *      special variables (done: PWD, PPID, RANDOM)
+ *      follow IFS rules more precisely, including update semantics
  *      tilde expansion
  *      aliases
- *      follow IFS rules more precisely, including update semantics
+ *      "command" missing features:
+ *          command -p CMD: run CMD using default $PATH
+ *              (can use this to override standalone shell as well?)
+ *          command BLTIN: disables special-ness (e.g. errors do not abort)
+ *          command -V CMD1 CMD2 CMD3 (multiple args) (not in standard)
  *      builtins mandated by standards we don't support:
- *          [un]alias, command, fc, getopts, newgrp, readonly, times
- *      make complex ${var%...} constructs support optional
- *      make here documents optional
+ *          [un]alias, fc:
+ *          fc -l[nr] [BEG] [END]: list range of commands in history
+ *          fc [-e EDITOR] [BEG] [END]: edit/rerun range of commands
+ *          fc -s [PAT=REP] [CMD]: rerun CMD, replacing PAT with REP
  *
  * Bash compat TODO:
  *      redirection of stdout+stderr: &> and >&
  *          The EXPR is evaluated according to ARITHMETIC EVALUATION.
  *          This is exactly equivalent to let "EXPR".
  *      $[EXPR]: synonym for $((EXPR))
+ *      indirect expansion: ${!VAR}
+ *      substring op on @: ${@:n:m}
  *
  * Won't do:
+ *      Some builtins mandated by standards:
+ *          newgrp [GRP]: not a builtin in bash but a suid binary
+ *              which spawns a new shell with new group ID
  *      In bash, export builtin is special, its arguments are assignments
  *          and therefore expansion of them should be "one-word" expansion:
  *              $ export i=`echo 'a  b'` # export has one arg: "i=a  b"
  *              aaa
  */
 //config:config HUSH
-//config:      bool "hush"
+//config:      bool "hush (64 kb)"
 //config:      default y
 //config:      help
-//config:        hush is a small shell (25k). It handles the normal flow control
-//config:        constructs such as if/then/elif/else/fi, for/in/do/done, while loops,
-//config:        case/esac. Redirections, here documents, $((arithmetic))
-//config:        and functions are supported.
+//config:      hush is a small shell. It handles the normal flow control
+//config:      constructs such as if/then/elif/else/fi, for/in/do/done, while loops,
+//config:      case/esac. Redirections, here documents, $((arithmetic))
+//config:      and functions are supported.
 //config:
-//config:        It will compile and work on no-mmu systems.
+//config:      It will compile and work on no-mmu systems.
 //config:
-//config:        It does not handle select, aliases, tilde expansion,
-//config:        &>file and >&file redirection of stdout+stderr.
+//config:      It does not handle select, aliases, tilde expansion,
+//config:      &>file and >&file redirection of stdout+stderr.
 //config:
 //config:config HUSH_BASH_COMPAT
 //config:      bool "bash-compatible extensions"
 //config:      default y
 //config:      depends on HUSH_BASH_COMPAT
 //config:      help
-//config:        Enable {abc,def} extension.
+//config:      Enable {abc,def} extension.
 //config:
 //config:config HUSH_INTERACTIVE
 //config:      bool "Interactive mode"
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:      help
-//config:        Enable interactive mode (prompt and command editing).
-//config:        Without this, hush simply reads and executes commands
-//config:        from stdin just like a shell script from a file.
-//config:        No prompt, no PS1/PS2 magic shell variables.
+//config:      Enable interactive mode (prompt and command editing).
+//config:      Without this, hush simply reads and executes commands
+//config:      from stdin just like a shell script from a file.
+//config:      No prompt, no PS1/PS2 magic shell variables.
 //config:
 //config:config HUSH_SAVEHISTORY
 //config:      bool "Save command history to .hush_history"
 //config:      default y
 //config:      depends on HUSH_INTERACTIVE
 //config:      help
-//config:        Enable job control: Ctrl-Z backgrounds, Ctrl-C interrupts current
-//config:        command (not entire shell), fg/bg builtins work. Without this option,
-//config:        "cmd &" still works by simply spawning a process and immediately
-//config:        prompting for next command (or executing next command in a script),
-//config:        but no separate process group is formed.
+//config:      Enable job control: Ctrl-Z backgrounds, Ctrl-C interrupts current
+//config:      command (not entire shell), fg/bg builtins work. Without this option,
+//config:      "cmd &" still works by simply spawning a process and immediately
+//config:      prompting for next command (or executing next command in a script),
+//config:      but no separate process group is formed.
 //config:
 //config:config HUSH_TICK
 //config:      bool "Support process substitution"
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:      help
-//config:        Enable `command` and $(command).
+//config:      Enable `command` and $(command).
 //config:
 //config:config HUSH_IF
 //config:      bool "Support if/then/elif/else/fi"
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:      help
-//config:        Enable case ... esac statement. +400 bytes.
+//config:      Enable case ... esac statement. +400 bytes.
 //config:
 //config:config HUSH_FUNCTIONS
 //config:      bool "Support funcname() { commands; } syntax"
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:      help
-//config:        Enable support for shell functions. +800 bytes.
+//config:      Enable support for shell functions. +800 bytes.
 //config:
 //config:config HUSH_LOCAL
 //config:      bool "local builtin"
 //config:      default y
 //config:      depends on HUSH_FUNCTIONS
 //config:      help
-//config:        Enable support for local variables in functions.
+//config:      Enable support for local variables in functions.
 //config:
 //config:config HUSH_RANDOM_SUPPORT
 //config:      bool "Pseudorandom generator and $RANDOM variable"
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:      help
-//config:        Enable pseudorandom generator and dynamic variable "$RANDOM".
-//config:        Each read of "$RANDOM" will generate a new pseudorandom value.
+//config:      Enable pseudorandom generator and dynamic variable "$RANDOM".
+//config:      Each read of "$RANDOM" will generate a new pseudorandom value.
 //config:
 //config:config HUSH_MODE_X
 //config:      bool "Support 'hush -x' option and 'set -x' command"
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:      help
-//config:        This instructs hush to print commands before execution.
-//config:        Adds ~300 bytes.
+//config:      This instructs hush to print commands before execution.
+//config:      Adds ~300 bytes.
 //config:
 //config:config HUSH_ECHO
 //config:      bool "echo builtin"
 //config:      default y
 //config:      depends on HUSH_EXPORT
 //config:      help
-//config:        export -n unexports variables. It is a bash extension.
+//config:      export -n unexports variables. It is a bash extension.
+//config:
+//config:config HUSH_READONLY
+//config:      bool "readonly builtin"
+//config:      default y
+//config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
+//config:      help
+//config:      Enable support for read-only variables.
 //config:
 //config:config HUSH_KILL
 //config:      bool "kill builtin (supports kill %jobspec)"
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:
+//config:config HUSH_COMMAND
+//config:      bool "command builtin"
+//config:      default y
+//config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
+//config:
 //config:config HUSH_TRAP
 //config:      bool "trap builtin"
 //config:      default y
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:
+//config:config HUSH_TIMES
+//config:      bool "times builtin"
+//config:      default y
+//config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
+//config:
 //config:config HUSH_READ
 //config:      bool "read builtin"
 //config:      default y
 //config:      default y
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
 //config:
+//config:config HUSH_GETOPTS
+//config:      bool "getopts builtin"
+//config:      default y
+//config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
+//config:
 //config:config HUSH_MEMLEAK
 //config:      bool "memleak builtin (debugging)"
 //config:      default n
 //config:      depends on HUSH || SH_IS_HUSH || BASH_IS_HUSH
-//config:
-//config:config MSH
-//config:      bool "msh (deprecated: aliased to hush)"
-//config:      default n
-//config:      select HUSH
-//config:      help
-//config:        msh is deprecated and will be removed, please migrate to hush.
 
 //applet:IF_HUSH(APPLET(hush, BB_DIR_BIN, BB_SUID_DROP))
-//applet:IF_MSH(APPLET_ODDNAME(msh, hush, BB_DIR_BIN, BB_SUID_DROP, hush))
-//applet:IF_SH_IS_HUSH(APPLET_ODDNAME(sh, hush, BB_DIR_BIN, BB_SUID_DROP, hush))
+//                       APPLET_ODDNAME:name  main  location    suid_type     help
+//applet:IF_SH_IS_HUSH(  APPLET_ODDNAME(sh,   hush, BB_DIR_BIN, BB_SUID_DROP, hush))
 //applet:IF_BASH_IS_HUSH(APPLET_ODDNAME(bash, hush, BB_DIR_BIN, BB_SUID_DROP, hush))
 
 //kbuild:lib-$(CONFIG_HUSH) += hush.o match.o shell_common.o
  * therefore we don't show them either.
  */
 //usage:#define hush_trivial_usage
-//usage:       "[-nxl] [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS]]"
+//usage:       "[-enxl] [-c 'SCRIPT' [ARG0 [ARGS]] / FILE [ARGS]]"
 //usage:#define hush_full_usage "\n\n"
 //usage:       "Unix shell interpreter"
 
 #if ENABLE_HUSH_CASE
 # include <fnmatch.h>
 #endif
+#include <sys/times.h>
 #include <sys/utsname.h> /* for setting $HOSTNAME */
 
 #include "busybox.h"  /* for APPLET_IS_NOFORK/NOEXEC */
 #endif
 
 
+/* So far, all bash compat is controlled by one config option */
+/* Separate defines document which part of code implements what */
+#define BASH_PATTERN_SUBST ENABLE_HUSH_BASH_COMPAT
+#define BASH_SUBSTR        ENABLE_HUSH_BASH_COMPAT
+#define BASH_SOURCE        ENABLE_HUSH_BASH_COMPAT
+#define BASH_HOSTNAME_VAR  ENABLE_HUSH_BASH_COMPAT
+#define BASH_LINENO_VAR    ENABLE_HUSH_BASH_COMPAT
+#define BASH_TEST2         (ENABLE_HUSH_BASH_COMPAT && ENABLE_HUSH_TEST)
+#define BASH_READ_D        ENABLE_HUSH_BASH_COMPAT
+
+
 /* Build knobs */
 #define LEAK_HUNTING 0
 #define BUILD_AS_NOMMU 0
 #define debug_printf_expand(...) do {} while (0)
 #define debug_printf_varexp(...) do {} while (0)
 #define debug_printf_glob(...)   do {} while (0)
+#define debug_printf_redir(...)  do {} while (0)
 #define debug_printf_list(...)   do {} while (0)
 #define debug_printf_subst(...)  do {} while (0)
 #define debug_printf_clean(...)  do {} while (0)
 #define _SPECIAL_VARS_STR     "_*@$!?#"
 #define SPECIAL_VARS_STR     ("_*@$!?#" + 1)
 #define NUMERIC_SPECVARS_STR ("_*@$!?#" + 3)
-#if ENABLE_HUSH_BASH_COMPAT
+#if BASH_PATTERN_SUBST
 /* Support / and // replace ops */
 /* Note that // is stored as \ in "encoded" string representation */
 # define VAR_ENCODED_SUBST_OPS      "\\/%#:-=+?"
 # define MINUS_PLUS_EQUAL_QUESTION ("%#:-=+?" + 3)
 #endif
 
-#define SPECIAL_VAR_SYMBOL   3
+#define SPECIAL_VAR_SYMBOL_STR "\3"
+#define SPECIAL_VAR_SYMBOL       3
+/* The "variable" with name "\1" emits string "\3". Testcase: "echo ^C" */
+#define SPECIAL_VAR_QUOTED_SVS   1
 
 struct variable;
 
@@ -569,10 +613,13 @@ typedef enum redir_type {
 struct command {
        pid_t pid;                  /* 0 if exited */
        int assignment_cnt;         /* how many argv[i] are assignments? */
+#if BASH_LINENO_VAR
+       unsigned lineno;
+#endif
        smallint cmd_type;          /* CMD_xxx */
 #define CMD_NORMAL   0
 #define CMD_SUBSHELL 1
-#if ENABLE_HUSH_BASH_COMPAT
+#if BASH_TEST2
 /* used for "[[ EXPR ]]" */
 # define CMD_SINGLEWORD_NOGLOB 2
 #endif
@@ -743,6 +790,7 @@ struct function {
 static const char o_opt_strings[] ALIGN1 =
        "pipefail\0"
        "noexec\0"
+       "errexit\0"
 #if ENABLE_HUSH_MODE_X
        "xtrace\0"
 #endif
@@ -750,6 +798,7 @@ static const char o_opt_strings[] ALIGN1 =
 enum {
        OPT_O_PIPEFAIL,
        OPT_O_NOEXEC,
+       OPT_O_ERREXIT,
 #if ENABLE_HUSH_MODE_X
        OPT_O_XTRACE,
 #endif
@@ -806,6 +855,25 @@ struct globals {
 #else
 # define G_saved_tty_pgrp 0
 #endif
+       /* How deeply are we in context where "set -e" is ignored */
+       int errexit_depth;
+       /* "set -e" rules (do we follow them correctly?):
+        * Exit if pipe, list, or compound command exits with a non-zero status.
+        * Shell does not exit if failed command is part of condition in
+        * if/while, part of && or || list except the last command, any command
+        * in a pipe but the last, or if the command's return value is being
+        * inverted with !. If a compound command other than a subshell returns a
+        * non-zero status because a command failed while -e was being ignored, the
+        * shell does not exit. A trap on ERR, if set, is executed before the shell
+        * exits [ERR is a bashism].
+        *
+        * If a compound command or function executes in a context where -e is
+        * ignored, none of the commands executed within are affected by the -e
+        * setting. If a compound command or function sets -e while executing in a
+        * context where -e is ignored, that setting does not have any effect until
+        * the compound command or the command containing the function call completes.
+        */
+
        char o_opt[NUM_OPT_O];
 #if ENABLE_HUSH_MODE_X
 # define G_x_mode (G.o_opt[OPT_O_XTRACE])
@@ -829,6 +897,7 @@ struct globals {
        smallint exiting; /* used to prevent EXIT trap recursion */
        /* These four support $?, $#, and $1 */
        smalluint last_exitcode;
+       smalluint last_bg_pid_exitcode;
 #if ENABLE_HUSH_SET
        /* are global_argv and global_argv[1..n] malloced? (note: not [0]) */
        smalluint global_args_malloced;
@@ -845,6 +914,9 @@ struct globals {
 #if ENABLE_HUSH_LOOPS
        unsigned depth_break_continue;
        unsigned depth_of_loop;
+#endif
+#if ENABLE_HUSH_GETOPTS
+       unsigned getopt_count;
 #endif
        const char *ifs;
        const char *cwd;
@@ -862,6 +934,10 @@ struct globals {
        unsigned count_SIGCHLD;
        unsigned handled_SIGCHLD;
        smallint we_have_children;
+#endif
+#if BASH_LINENO_VAR
+       unsigned lineno;
+       char *lineno_var;
 #endif
        struct FILE_list *FILE_list;
        /* Which signals have non-DFL handler (even with no traps set)?
@@ -919,10 +995,16 @@ static int builtin_exit(char **argv) FAST_FUNC;
 #if ENABLE_HUSH_EXPORT
 static int builtin_export(char **argv) FAST_FUNC;
 #endif
+#if ENABLE_HUSH_READONLY
+static int builtin_readonly(char **argv) FAST_FUNC;
+#endif
 #if ENABLE_HUSH_JOB
 static int builtin_fg_bg(char **argv) FAST_FUNC;
 static int builtin_jobs(char **argv) FAST_FUNC;
 #endif
+#if ENABLE_HUSH_GETOPTS
+static int builtin_getopts(char **argv) FAST_FUNC;
+#endif
 #if ENABLE_HUSH_HELP
 static int builtin_help(char **argv) FAST_FUNC;
 #endif
@@ -947,7 +1029,7 @@ static int builtin_set(char **argv) FAST_FUNC;
 #endif
 static int builtin_shift(char **argv) FAST_FUNC;
 static int builtin_source(char **argv) FAST_FUNC;
-#if ENABLE_HUSH_TEST
+#if ENABLE_HUSH_TEST || BASH_TEST2
 static int builtin_test(char **argv) FAST_FUNC;
 #endif
 #if ENABLE_HUSH_TRAP
@@ -956,6 +1038,9 @@ static int builtin_trap(char **argv) FAST_FUNC;
 #if ENABLE_HUSH_TYPE
 static int builtin_type(char **argv) FAST_FUNC;
 #endif
+#if ENABLE_HUSH_TIMES
+static int builtin_times(char **argv) FAST_FUNC;
+#endif
 static int builtin_true(char **argv) FAST_FUNC;
 #if ENABLE_HUSH_UMASK
 static int builtin_umask(char **argv) FAST_FUNC;
@@ -995,13 +1080,13 @@ struct built_in_command {
 };
 
 static const struct built_in_command bltins1[] = {
-       BLTIN("."        , builtin_source  , "Run commands in file"),
+       BLTIN("."        , builtin_source  , "Run commands in file"),
        BLTIN(":"        , builtin_true    , NULL),
 #if ENABLE_HUSH_JOB
-       BLTIN("bg"       , builtin_fg_bg   , "Resume a job in the background"),
+       BLTIN("bg"       , builtin_fg_bg   , "Resume job in background"),
 #endif
 #if ENABLE_HUSH_LOOPS
-       BLTIN("break"    , builtin_break   , "Exit from a loop"),
+       BLTIN("break"    , builtin_break   , "Exit loop"),
 #endif
        BLTIN("cd"       , builtin_cd      , "Change directory"),
 #if ENABLE_HUSH_LOOPS
@@ -1009,18 +1094,21 @@ static const struct built_in_command bltins1[] = {
 #endif
        BLTIN("eval"     , builtin_eval    , "Construct and run shell command"),
        BLTIN("exec"     , builtin_exec    , "Execute command, don't return to shell"),
-       BLTIN("exit"     , builtin_exit    , "Exit"),
+       BLTIN("exit"     , builtin_exit    , NULL),
 #if ENABLE_HUSH_EXPORT
        BLTIN("export"   , builtin_export  , "Set environment variables"),
 #endif
 #if ENABLE_HUSH_JOB
-       BLTIN("fg"       , builtin_fg_bg   , "Bring job into the foreground"),
+       BLTIN("fg"       , builtin_fg_bg   , "Bring job to foreground"),
+#endif
+#if ENABLE_HUSH_GETOPTS
+       BLTIN("getopts"  , builtin_getopts , NULL),
 #endif
 #if ENABLE_HUSH_HELP
        BLTIN("help"     , builtin_help    , NULL),
 #endif
 #if MAX_HISTORY && ENABLE_FEATURE_EDITING
-       BLTIN("history"  , builtin_history , "Show command history"),
+       BLTIN("history"  , builtin_history , "Show history"),
 #endif
 #if ENABLE_HUSH_JOB
        BLTIN("jobs"     , builtin_jobs    , "List jobs"),
@@ -1037,15 +1125,21 @@ static const struct built_in_command bltins1[] = {
 #if ENABLE_HUSH_READ
        BLTIN("read"     , builtin_read    , "Input into variable"),
 #endif
+#if ENABLE_HUSH_READONLY
+       BLTIN("readonly" , builtin_readonly, "Make variables read-only"),
+#endif
 #if ENABLE_HUSH_FUNCTIONS
-       BLTIN("return"   , builtin_return  , "Return from function"),
+       BLTIN("return"   , builtin_return  , "Return from function"),
 #endif
 #if ENABLE_HUSH_SET
-       BLTIN("set"      , builtin_set     , "Set/unset positional parameters"),
+       BLTIN("set"      , builtin_set     , "Set positional parameters"),
 #endif
        BLTIN("shift"    , builtin_shift   , "Shift positional parameters"),
-#if ENABLE_HUSH_BASH_COMPAT
-       BLTIN("source"   , builtin_source  , "Run commands in a file"),
+#if BASH_SOURCE
+       BLTIN("source"   , builtin_source  , NULL),
+#endif
+#if ENABLE_HUSH_TIMES
+       BLTIN("times"    , builtin_times   , NULL),
 #endif
 #if ENABLE_HUSH_TRAP
        BLTIN("trap"     , builtin_trap    , "Trap signals"),
@@ -1064,13 +1158,19 @@ static const struct built_in_command bltins1[] = {
        BLTIN("unset"    , builtin_unset   , "Unset variables"),
 #endif
 #if ENABLE_HUSH_WAIT
-       BLTIN("wait"     , builtin_wait    , "Wait for process"),
+       BLTIN("wait"     , builtin_wait    , "Wait for process to finish"),
 #endif
 };
+/* These builtins won't be used if we are on NOMMU and need to re-exec
+ * (it's cheaper to run an external program in this case):
+ */
 static const struct built_in_command bltins2[] = {
 #if ENABLE_HUSH_TEST
        BLTIN("["        , builtin_test    , NULL),
 #endif
+#if BASH_TEST2
+       BLTIN("[["       , builtin_test    , NULL),
+#endif
 #if ENABLE_HUSH_ECHO
        BLTIN("echo"     , builtin_echo    , NULL),
 #endif
@@ -1138,6 +1238,10 @@ static const struct built_in_command bltins2[] = {
 # define DEBUG_GLOB 0
 #endif
 
+#ifndef debug_printf_redir
+# define debug_printf_redir(...) (indent(), fdprintf(2, __VA_ARGS__))
+#endif
+
 #ifndef debug_printf_list
 # define debug_printf_list(...) (indent(), fdprintf(2, __VA_ARGS__))
 #endif
@@ -1205,7 +1309,7 @@ static void xxfree(void *ptr)
  * HUSH_DEBUG >= 2 prints line number in this file where it was detected.
  */
 #if HUSH_DEBUG < 2
-# define die_if_script(lineno, ...)             die_if_script(__VA_ARGS__)
+# define msg_and_die_if_script(lineno, ...)     msg_and_die_if_script(__VA_ARGS__)
 # define syntax_error(lineno, msg)              syntax_error(msg)
 # define syntax_error_at(lineno, msg)           syntax_error_at(msg)
 # define syntax_error_unterm_ch(lineno, ch)     syntax_error_unterm_ch(ch)
@@ -1213,7 +1317,16 @@ static void xxfree(void *ptr)
 # define syntax_error_unexpected_ch(lineno, ch) syntax_error_unexpected_ch(ch)
 #endif
 
-static void die_if_script(unsigned lineno, const char *fmt, ...)
+static void die_if_script(void)
+{
+       if (!G_interactive_fd) {
+               if (G.last_exitcode) /* sometines it's 2, not 1 (bash compat) */
+                       xfunc_error_retval = G.last_exitcode;
+               xfunc_die();
+       }
+}
+
+static void msg_and_die_if_script(unsigned lineno, const char *fmt, ...)
 {
        va_list p;
 
@@ -1223,8 +1336,7 @@ static void die_if_script(unsigned lineno, const char *fmt, ...)
        va_start(p, fmt);
        bb_verror_msg(fmt, p, NULL);
        va_end(p);
-       if (!G_interactive_fd)
-               xfunc_die();
+       die_if_script();
 }
 
 static void syntax_error(unsigned lineno UNUSED_PARAM, const char *msg)
@@ -1233,16 +1345,20 @@ static void syntax_error(unsigned lineno UNUSED_PARAM, const char *msg)
                bb_error_msg("syntax error: %s", msg);
        else
                bb_error_msg("syntax error");
+       die_if_script();
 }
 
 static void syntax_error_at(unsigned lineno UNUSED_PARAM, const char *msg)
 {
        bb_error_msg("syntax error at '%s'", msg);
+       die_if_script();
 }
 
 static void syntax_error_unterm_str(unsigned lineno UNUSED_PARAM, const char *s)
 {
        bb_error_msg("syntax error: unterminated %s", s);
+//? source4.tests fails: in bash, echo ${^} in script does not terminate the script
+//     die_if_script();
 }
 
 static void syntax_error_unterm_ch(unsigned lineno, char ch)
@@ -1260,17 +1376,18 @@ static void syntax_error_unexpected_ch(unsigned lineno UNUSED_PARAM, int ch)
        bb_error_msg("hush.c:%u", lineno);
 #endif
        bb_error_msg("syntax error: unexpected %s", ch == EOF ? "EOF" : msg);
+       die_if_script();
 }
 
 #if HUSH_DEBUG < 2
-# undef die_if_script
+# undef msg_and_die_if_script
 # undef syntax_error
 # undef syntax_error_at
 # undef syntax_error_unterm_ch
 # undef syntax_error_unterm_str
 # undef syntax_error_unexpected_ch
 #else
-# define die_if_script(...)             die_if_script(__LINE__, __VA_ARGS__)
+# define msg_and_die_if_script(...)     msg_and_die_if_script(__LINE__, __VA_ARGS__)
 # define syntax_error(msg)              syntax_error(__LINE__, msg)
 # define syntax_error_at(msg)           syntax_error_at(__LINE__, msg)
 # define syntax_error_unterm_ch(ch)     syntax_error_unterm_ch(__LINE__, ch)
@@ -1373,17 +1490,37 @@ static void free_strings(char **strings)
        free(strings);
 }
 
+static int fcntl_F_DUPFD(int fd, int avoid_fd)
+{
+       int newfd;
+ repeat:
+       newfd = fcntl(fd, F_DUPFD, avoid_fd + 1);
+       if (newfd < 0) {
+               if (errno == EBUSY)
+                       goto repeat;
+               if (errno == EINTR)
+                       goto repeat;
+       }
+       return newfd;
+}
 
-static int xdup_and_close(int fd, int F_DUPFD_maybe_CLOEXEC)
+static int xdup_CLOEXEC_and_close(int fd, int avoid_fd)
 {
-       /* We avoid taking stdio fds. Mimicking ash: use fds above 9 */
-       int newfd = fcntl(fd, F_DUPFD_maybe_CLOEXEC, 10);
+       int newfd;
+ repeat:
+       newfd = fcntl(fd, F_DUPFD_CLOEXEC, avoid_fd + 1);
        if (newfd < 0) {
+               if (errno == EBUSY)
+                       goto repeat;
+               if (errno == EINTR)
+                       goto repeat;
                /* fd was not open? */
                if (errno == EBADF)
                        return fd;
                xfunc_die();
        }
+       if (F_DUPFD_CLOEXEC == F_DUPFD) /* if old libc (w/o F_DUPFD_CLOEXEC) */
+               fcntl(newfd, F_SETFD, FD_CLOEXEC);
        close(fd);
        return newfd;
 }
@@ -1416,13 +1553,14 @@ static void fclose_and_forget(FILE *fp)
        }
        fclose(fp);
 }
-static int save_FILEs_on_redirect(int fd)
+static int save_FILEs_on_redirect(int fd, int avoid_fd)
 {
        struct FILE_list *fl = G.FILE_list;
        while (fl) {
                if (fd == fl->fd) {
                        /* We use it only on script files, they are all CLOEXEC */
-                       fl->fd = xdup_and_close(fd, F_DUPFD_CLOEXEC);
+                       fl->fd = xdup_CLOEXEC_and_close(fd, avoid_fd);
+                       debug_printf_redir("redirect_fd %d: matches a script fd, moving it to %d\n", fd, fl->fd);
                        return 1;
                }
                fl = fl->next;
@@ -1435,13 +1573,14 @@ static void restore_redirected_FILEs(void)
        while (fl) {
                int should_be = fileno(fl->fp);
                if (fl->fd != should_be) {
+                       debug_printf_redir("restoring script fd from %d to %d\n", fl->fd, should_be);
                        xmove_fd(fl->fd, should_be);
                        fl->fd = should_be;
                }
                fl = fl->next;
        }
 }
-#if ENABLE_FEATURE_SH_STANDALONE
+#if ENABLE_FEATURE_SH_STANDALONE && BB_MMU
 static void close_all_FILE_list(void)
 {
        struct FILE_list *fl = G.FILE_list;
@@ -1457,6 +1596,16 @@ static void close_all_FILE_list(void)
        }
 }
 #endif
+static int fd_in_FILEs(int fd)
+{
+       struct FILE_list *fl = G.FILE_list;
+       while (fl) {
+               if (fl->fd == fd)
+                       return 1;
+               fl = fl->next;
+       }
+       return 0;
+}
 
 
 /* Helpers for setting new $n and restoring them back
@@ -1470,8 +1619,6 @@ typedef struct save_arg_t {
 
 static void save_and_replace_G_args(save_arg_t *sv, char **argv)
 {
-       int n;
-
        sv->sv_argv0 = argv[0];
        sv->sv_g_argv = G.global_argv;
        sv->sv_g_argc = G.global_argc;
@@ -1481,10 +1628,7 @@ static void save_and_replace_G_args(save_arg_t *sv, char **argv)
        G.global_argv = argv;
        IF_HUSH_SET(G.global_args_malloced = 0;)
 
-       n = 1;
-       while (*++argv)
-               n++;
-       G.global_argc = n;
+       G.global_argc = 1 + string_array_len(argv + 1);
 }
 
 static void restore_G_args(save_arg_t *sv, char **argv)
@@ -1706,7 +1850,7 @@ static void restore_ttypgrp_and__exit(void)
  *     echo END_OF_SCRIPT
  * lseeks fd in input FILE object from EOF to "e" in "echo END_OF_SCRIPT".
  * This makes "echo END_OF_SCRIPT" executed twice.
- * Similar problems can be seen with die_if_script() -> xfunc_die()
+ * Similar problems can be seen with msg_and_die_if_script() -> xfunc_die()
  * and in `cmd` handling.
  * If set as die_func(), this makes xfunc_die() exit via _exit(), not exit():
  */
@@ -1792,7 +1936,7 @@ static void hush_exit(int exitcode)
        if (G.exiting <= 0 && G_traps && G_traps[0] && G_traps[0][0]) {
                char *argv[3];
                /* argv[0] is unused */
-               argv[1] = G_traps[0];
+               argv[1] = xstrdup(G_traps[0]); /* copy, since EXIT trap handler may modify G_traps[0] */
                argv[2] = NULL;
                G.exiting = 1; /* prevent EXIT trap recursion */
                /* Note: G_traps[0] is not cleared!
@@ -1872,6 +2016,9 @@ static int check_and_run_traps(void)
                        break;
 #if ENABLE_HUSH_JOB
                case SIGHUP: {
+//TODO: why are we doing this? ash and dash don't do this,
+//they have no handler for SIGHUP at all,
+//they rely on kernel to send SIGHUP+SIGCONT to orphaned process groups
                        struct pipe *job;
                        debug_printf_exec("%s: sig:%d default SIGHUP handler\n", __func__, sig);
                        /* bash is observed to signal whole process groups,
@@ -1892,7 +2039,7 @@ static int check_and_run_traps(void)
                        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.
+                        * We don't do 'last_sig = sig' here -> NOT returning this sig.
                         * This simplifies wait builtin a bit.
                         */
                        break;
@@ -1901,7 +2048,7 @@ static int check_and_run_traps(void)
                        debug_printf_exec("%s: sig:%d default handling is to ignore\n", __func__, sig);
                        /* SIGTERM, SIGQUIT, SIGTTIN, SIGTTOU, SIGTSTP */
                        /* Note:
-                        * We dont do 'last_sig = sig' here -> NOT returning this sig.
+                        * We don't do 'last_sig = sig' here -> NOT returning this sig.
                         * Example: wait is not interrupted by TERM
                         * in interactive shell, because TERM is ignored.
                         */
@@ -1975,32 +2122,19 @@ static const char* FAST_FUNC get_local_var_value(const char *name)
 
 /* str holds "NAME=VAL" and is expected to be malloced.
  * We take ownership of it.
- * flg_export:
- *  0: do not change export flag
- *     (if creating new variable, flag will be 0)
- *  1: set export flag and putenv the variable
- * -1: clear export flag and unsetenv the variable
- * flg_read_only is set only when we handle -R var=val
  */
-#if !BB_MMU && ENABLE_HUSH_LOCAL
-/* all params are used */
-#elif BB_MMU && ENABLE_HUSH_LOCAL
-#define set_local_var(str, flg_export, local_lvl, flg_read_only) \
-       set_local_var(str, flg_export, local_lvl)
-#elif BB_MMU && !ENABLE_HUSH_LOCAL
-#define set_local_var(str, flg_export, local_lvl, flg_read_only) \
-       set_local_var(str, flg_export)
-#elif !BB_MMU && !ENABLE_HUSH_LOCAL
-#define set_local_var(str, flg_export, local_lvl, flg_read_only) \
-       set_local_var(str, flg_export, flg_read_only)
-#endif
-static int set_local_var(char *str, int flg_export, int local_lvl, int flg_read_only)
+#define SETFLAG_EXPORT   (1 << 0)
+#define SETFLAG_UNEXPORT (1 << 1)
+#define SETFLAG_MAKE_RO  (1 << 2)
+#define SETFLAG_LOCAL_SHIFT    3
+static int set_local_var(char *str, unsigned flags)
 {
        struct variable **var_pp;
        struct variable *cur;
        char *free_me = NULL;
        char *eq_sign;
        int name_len;
+       IF_HUSH_LOCAL(unsigned local_lvl = (flags >> SETFLAG_LOCAL_SHIFT);)
 
        eq_sign = strchr(str, '=');
        if (!eq_sign) { /* not expected to ever happen? */
@@ -2009,6 +2143,13 @@ static int set_local_var(char *str, int flg_export, int local_lvl, int flg_read_
        }
 
        name_len = eq_sign - str + 1; /* including '=' */
+#if BASH_LINENO_VAR
+       if (G.lineno_var) {
+               if (name_len == 7 && strncmp("LINENO", str, 6) == 0)
+                       G.lineno_var = NULL;
+       }
+#endif
+
        var_pp = &G.top_var;
        while ((cur = *var_pp) != NULL) {
                if (strncmp(cur->varstr, str, name_len) != 0) {
@@ -2018,14 +2159,13 @@ static int set_local_var(char *str, int flg_export, int local_lvl, int flg_read_
 
                /* We found an existing var with this name */
                if (cur->flg_read_only) {
-#if !BB_MMU
-                       if (!flg_read_only)
-#endif
-                               bb_error_msg("%s: readonly variable", str);
+                       bb_error_msg("%s: readonly variable", str);
                        free(str);
+//NOTE: in bash, assignment in "export READONLY_VAR=Z" fails, and sets $?=1,
+//but export per se succeeds (does put the var in env). We don't mimic that.
                        return -1;
                }
-               if (flg_export == -1) { // "&& cur->flg_export" ?
+               if (flags & SETFLAG_UNEXPORT) { // && cur->flg_export ?
                        debug_printf_env("%s: unsetenv '%s'\n", __func__, str);
                        *eq_sign = '\0';
                        unsetenv(str);
@@ -2049,7 +2189,7 @@ static int set_local_var(char *str, int flg_export, int local_lvl, int flg_read_
                         * z=z
                         */
                        if (cur->flg_export)
-                               flg_export = 1;
+                               flags |= SETFLAG_EXPORT;
                        break;
                }
 #endif
@@ -2080,24 +2220,29 @@ static int set_local_var(char *str, int flg_export, int local_lvl, int flg_read_
 
        /* Not found - create new variable struct */
        cur = xzalloc(sizeof(*cur));
-#if ENABLE_HUSH_LOCAL
-       cur->func_nest_level = local_lvl;
-#endif
+       IF_HUSH_LOCAL(cur->func_nest_level = local_lvl;)
        cur->next = *var_pp;
        *var_pp = cur;
 
  set_str_and_exp:
        cur->varstr = str;
-#if !BB_MMU
-       cur->flg_read_only = flg_read_only;
-#endif
  exp:
-       if (flg_export == 1)
+#if !BB_MMU || ENABLE_HUSH_READONLY
+       if (flags & SETFLAG_MAKE_RO) {
+               cur->flg_read_only = 1;
+       }
+#endif
+       if (flags & SETFLAG_EXPORT)
                cur->flg_export = 1;
        if (name_len == 4 && cur->varstr[0] == 'P' && cur->varstr[1] == 'S')
                cmdedit_update_prompt();
+#if ENABLE_HUSH_GETOPTS
+       /* defoptindvar is a "OPTIND=..." constant string */
+       if (strncmp(cur->varstr, defoptindvar, 7) == 0)
+               G.getopt_count = 0;
+#endif
        if (cur->flg_export) {
-               if (flg_export == -1) {
+               if (flags & SETFLAG_UNEXPORT) {
                        cur->flg_export = 0;
                        /* unsetenv was already done */
                } else {
@@ -2114,10 +2259,9 @@ static int set_local_var(char *str, int flg_export, int local_lvl, int flg_read_
 }
 
 /* Used at startup and after each cd */
-static void set_pwd_var(int exp)
+static void set_pwd_var(unsigned flag)
 {
-       set_local_var(xasprintf("PWD=%s", get_cwd(/*force:*/ 1)),
-               /*exp:*/ exp, /*lvl:*/ 0, /*ro:*/ 0);
+       set_local_var(xasprintf("PWD=%s", get_cwd(/*force:*/ 1)), flag);
 }
 
 static int unset_local_var_len(const char *name, int name_len)
@@ -2127,6 +2271,16 @@ static int unset_local_var_len(const char *name, int name_len)
 
        if (!name)
                return EXIT_SUCCESS;
+
+#if ENABLE_HUSH_GETOPTS
+       if (name_len == 6 && strncmp(name, "OPTIND", 6) == 0)
+               G.getopt_count = 0;
+#endif
+#if BASH_LINENO_VAR
+       if (name_len == 6 && G.lineno_var && strncmp(name, "LINENO", 6) == 0)
+               G.lineno_var = NULL;
+#endif
+
        var_pp = &G.top_var;
        while ((cur = *var_pp) != NULL) {
                if (strncmp(cur->varstr, name, name_len) == 0 && cur->varstr[name_len] == '=') {
@@ -2149,7 +2303,7 @@ static int unset_local_var_len(const char *name, int name_len)
        return EXIT_SUCCESS;
 }
 
-#if ENABLE_HUSH_UNSET
+#if ENABLE_HUSH_UNSET || ENABLE_HUSH_GETOPTS
 static int unset_local_var(const char *name)
 {
        return unset_local_var_len(name, strlen(name));
@@ -2171,11 +2325,11 @@ static void unset_vars(char **strings)
        free(strings);
 }
 
-#if ENABLE_FEATURE_SH_MATH || ENABLE_HUSH_BASH_COMPAT || ENABLE_HUSH_READ
+#if BASH_HOSTNAME_VAR || ENABLE_FEATURE_SH_MATH || ENABLE_HUSH_READ || ENABLE_HUSH_GETOPTS
 static void FAST_FUNC set_local_var_from_halves(const char *name, const char *val)
 {
        char *var = xasprintf("%s=%s", name, val);
-       set_local_var(var, /*flags:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+       set_local_var(var, /*flag:*/ 0);
 }
 #endif
 
@@ -2218,16 +2372,32 @@ static struct variable *set_vars_and_save_old(char **strings)
                if (eq) {
                        var_pp = get_ptr_to_local_var(*s, eq - *s);
                        if (var_pp) {
-                               /* Remove variable from global linked list */
                                var_p = *var_pp;
+                               if (var_p->flg_read_only) {
+                                       char **p;
+                                       bb_error_msg("%s: readonly variable", *s);
+                                       /*
+                                        * "VAR=V BLTIN" unsets VARs after BLTIN completes.
+                                        * If VAR is readonly, leaving it in the list
+                                        * after asssignment error (msg above)
+                                        * causes doubled error message later, on unset.
+                                        */
+                                       debug_printf_env("removing/freeing '%s' element\n", *s);
+                                       free(*s);
+                                       p = s;
+                                       do { *p = p[1]; p++; } while (*p);
+                                       goto next;
+                               }
+                               /* Remove variable from global linked list */
                                debug_printf_env("%s: removing '%s'\n", __func__, var_p->varstr);
                                *var_pp = var_p->next;
                                /* Add it to returned list */
                                var_p->next = old;
                                old = var_p;
                        }
-                       set_local_var(*s, /*exp:*/ 1, /*lvl:*/ 0, /*ro:*/ 0);
+                       set_local_var(*s, SETFLAG_EXPORT);
                }
+ next:
                s++;
        }
        return old;
@@ -2264,7 +2434,7 @@ static void reinit_unicode_for_hush(void)
  *     AT\
  *     H\
  *     \
- * It excercises a lot of corner cases.
+ * It exercises a lot of corner cases.
  */
 static void cmdedit_update_prompt(void)
 {
@@ -2316,18 +2486,17 @@ static int get_user_input(struct in_str *i)
                /* buglet: SIGINT will not make new prompt to appear _at once_,
                 * only after <Enter>. (^C works immediately) */
                r = read_line_input(G.line_input_state, prompt_str,
-                               G.user_input_buf, CONFIG_FEATURE_EDITING_MAX_LEN-1,
-                               /*timeout*/ -1
+                               G.user_input_buf, CONFIG_FEATURE_EDITING_MAX_LEN-1
                );
                /* read_line_input intercepts ^C, "convert" it to SIGINT */
-               if (r == 0) {
-                       write(STDOUT_FILENO, "^C", 2);
+               if (r == 0)
                        raise(SIGINT);
-               }
                check_and_run_traps();
                if (r != 0 && !G.flag_SIGINT)
                        break;
                /* ^C or SIGINT: repeat */
+               /* bash prints ^C even on real SIGINT (non-kbd generated) */
+               write(STDOUT_FILENO, "^C", 2);
                G.last_exitcode = 128 + SIGINT;
        }
        if (r < 0) {
@@ -2430,6 +2599,10 @@ static int i_getch(struct in_str *i)
  out:
        debug_printf("file_get: got '%c' %d\n", ch, ch);
        i->last_char = ch;
+#if BASH_LINENO_VAR
+       if (ch == '\n')
+               G.lineno++;
+#endif
        return ch;
 }
 
@@ -2579,9 +2752,8 @@ static void o_delchr(o_string *o)
 static void o_addblock(o_string *o, const char *str, int len)
 {
        o_grow_by(o, len);
-       memcpy(&o->data[o->length], str, len);
+       ((char*)mempcpy(&o->data[o->length], str, len))[0] = '\0';
        o->length += len;
-       o->data[o->length] = '\0';
 }
 
 static void o_addstr(o_string *o, const char *str)
@@ -3290,7 +3462,7 @@ static int done_command(struct parse_context *ctx)
 #if 0  /* Instead we emit error message at run time */
        if (ctx->pending_redirect) {
                /* For example, "cmd >" (no filename to redirect to) */
-               die_if_script("syntax error: %s", "invalid redirect");
+               syntax_error("invalid redirect");
                ctx->pending_redirect = NULL;
        }
 #endif
@@ -3313,6 +3485,9 @@ static int done_command(struct parse_context *ctx)
        ctx->command = command = &pi->cmds[pi->num_cmds];
  clear_and_ret:
        memset(command, 0, sizeof(*command));
+#if BASH_LINENO_VAR
+       command->lineno = G.lineno;
+#endif
        return pi->num_cmds; /* used only for 0/nonzero check */
 }
 
@@ -3323,12 +3498,49 @@ static void done_pipe(struct parse_context *ctx, pipe_style type)
        debug_printf_parse("done_pipe entered, followup %d\n", type);
        /* Close previous command */
        not_null = done_command(ctx);
-       ctx->pipe->followup = type;
 #if HAS_KEYWORDS
        ctx->pipe->pi_inverted = ctx->ctx_inverted;
        ctx->ctx_inverted = 0;
        ctx->pipe->res_word = ctx->ctx_res_w;
 #endif
+       if (type == PIPE_BG && ctx->list_head != ctx->pipe) {
+               /* Necessary since && and || have precedence over &:
+                * "cmd1 && cmd2 &" must spawn both cmds, not only cmd2,
+                * in a backgrounded subshell.
+                */
+               struct pipe *pi;
+               struct command *command;
+
+               /* Is this actually this construct, all pipes end with && or ||? */
+               pi = ctx->list_head;
+               while (pi != ctx->pipe) {
+                       if (pi->followup != PIPE_AND && pi->followup != PIPE_OR)
+                               goto no_conv;
+                       pi = pi->next;
+               }
+
+               debug_printf_parse("BG with more than one pipe, converting to { p1 &&...pN; } &\n");
+               pi->followup = PIPE_SEQ; /* close pN _not_ with "&"! */
+               pi = xzalloc(sizeof(*pi));
+               pi->followup = PIPE_BG;
+               pi->num_cmds = 1;
+               pi->cmds = xzalloc(sizeof(pi->cmds[0]));
+               command = &pi->cmds[0];
+               if (CMD_NORMAL != 0) /* "if xzalloc didn't do that already" */
+                       command->cmd_type = CMD_NORMAL;
+               command->group = ctx->list_head;
+#if !BB_MMU
+               command->group_as_string = xstrndup(
+                           ctx->as_string.data,
+                           ctx->as_string.length - 1 /* do not copy last char, "&" */
+               );
+#endif
+               /* Replace all pipes in ctx with one newly created */
+               ctx->list_head = ctx->pipe = pi;
+       } else {
+ no_conv:
+               ctx->pipe->followup = type;
+       }
 
        /* Without this check, even just <enter> on command line generates
         * tree of three NOPs (!). Which is harmless but annoying.
@@ -3498,9 +3710,8 @@ static int reserved_word(o_string *word, struct parse_context *ctx)
        if (r->flag & FLAG_START) {
                struct parse_context *old;
 
-               old = xmalloc(sizeof(*old));
+               old = xmemdup(ctx, sizeof(*ctx));
                debug_printf_parse("push stack %p\n", old);
-               *old = *ctx;   /* physical copy */
                initialize_context(ctx);
                ctx->stack = old;
        } else if (/*ctx->ctx_res_w == RES_NONE ||*/ !(ctx->old_flag & (1 << r->res))) {
@@ -3630,7 +3841,7 @@ static int done_word(o_string *word, struct parse_context *ctx)
                                                (ctx->ctx_res_w == RES_SNTX));
                                return (ctx->ctx_res_w == RES_SNTX);
                        }
-# if ENABLE_HUSH_BASH_COMPAT
+# if BASH_TEST2
                        if (strcmp(word->data, "[[") == 0) {
                                command->cmd_type = CMD_SINGLEWORD_NOGLOB;
                        }
@@ -3661,21 +3872,6 @@ static int done_word(o_string *word, struct parse_context *ctx)
                        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)
-                /* (otherwise it's known to be not empty and is already safe) */
-               ) {
-                       /* exclude "$@" - it can expand to no word despite "" */
-                       char *p = word->data;
-                       while (p[0] == SPECIAL_VAR_SYMBOL
-                           && (p[1] & 0x7f) == '@'
-                           && p[2] == SPECIAL_VAR_SYMBOL
-                       ) {
-                               p += 3;
-                       }
-               }
                command->argv = add_string_to_strings(command->argv, xstrdup(word->data));
                debug_print_strings("word appended to argv", command->argv);
        }
@@ -3820,7 +4016,7 @@ static int parse_redirect(struct parse_context *ctx,
 #if 0          /* Instead we emit error message at run time */
                if (ctx->pending_redirect) {
                        /* For example, "cmd > <file" */
-                       die_if_script("syntax error: %s", "invalid redirect");
+                       syntax_error("invalid redirect");
                }
 #endif
                /* Set ctx->pending_redirect, so we know what to do at the
@@ -3883,24 +4079,34 @@ static char *fetch_till_str(o_string *as_string,
                ch = i_getch(input);
                if (ch != EOF)
                        nommu_addchr(as_string, ch);
-               if ((ch == '\n' || ch == EOF)
-                && ((heredoc_flags & HEREDOC_QUOTED) || prev != '\\')
-               ) {
-                       if (strcmp(heredoc.data + past_EOL, word) == 0) {
-                               heredoc.data[past_EOL] = '\0';
-                               debug_printf_parse("parsed heredoc '%s'\n", heredoc.data);
-                               return heredoc.data;
-                       }
-                       while (ch == '\n') {
-                               o_addchr(&heredoc, ch);
-                               prev = ch;
+               if (ch == '\n' || ch == EOF) {
+ check_heredoc_end:
+                       if ((heredoc_flags & HEREDOC_QUOTED) || prev != '\\') {
+                               if (strcmp(heredoc.data + past_EOL, word) == 0) {
+                                       heredoc.data[past_EOL] = '\0';
+                                       debug_printf_parse("parsed heredoc '%s'\n", heredoc.data);
+                                       return heredoc.data;
+                               }
+                               if (ch == '\n') {
+                                       /* This is a new line.
+                                        * Remember position and backslash-escaping status.
+                                        */
+                                       o_addchr(&heredoc, ch);
+                                       prev = ch;
  jump_in:
-                               past_EOL = heredoc.length;
-                               do {
-                                       ch = i_getch(input);
-                                       if (ch != EOF)
-                                               nommu_addchr(as_string, ch);
-                               } while ((heredoc_flags & HEREDOC_SKIPTABS) && ch == '\t');
+                                       past_EOL = heredoc.length;
+                                       /* Get 1st char of next line, possibly skipping leading tabs */
+                                       do {
+                                               ch = i_getch(input);
+                                               if (ch != EOF)
+                                                       nommu_addchr(as_string, ch);
+                                       } while ((heredoc_flags & HEREDOC_SKIPTABS) && ch == '\t');
+                                       /* If this immediately ended the line,
+                                        * go back to end-of-line checks.
+                                        */
+                                       if (ch == '\n')
+                                               goto check_heredoc_end;
+                               }
                        }
                }
                if (ch == EOF) {
@@ -4228,7 +4434,7 @@ static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsign
 {
        int ch;
        char dbl = end_ch & DOUBLE_CLOSE_CHAR_FLAG;
-# if ENABLE_HUSH_BASH_COMPAT
+# if BASH_SUBSTR || BASH_PATTERN_SUBST
        char end_char2 = end_ch >> 8;
 # endif
        end_ch &= (DOUBLE_CLOSE_CHAR_FLAG - 1);
@@ -4239,7 +4445,11 @@ static int add_till_closing_bracket(o_string *dest, struct in_str *input, unsign
                        syntax_error_unterm_ch(end_ch);
                        return 0;
                }
-               if (ch == end_ch  IF_HUSH_BASH_COMPAT( || ch == end_char2)) {
+               if (ch == end_ch
+# if BASH_SUBSTR || BASH_PATTERN_SUBST
+                       || ch == end_char2
+# endif
+               ) {
                        if (!dbl)
                                break;
                        /* we look for closing )) of $((EXPR)) */
@@ -4344,6 +4554,8 @@ static int parse_dollar(o_string *as_string,
        case '@': /* args */
                goto make_one_char_var;
        case '{': {
+               char len_single_ch;
+
                o_addchr(dest, SPECIAL_VAR_SYMBOL);
 
                ch = i_getch(input); /* eat '{' */
@@ -4363,6 +4575,7 @@ static int parse_dollar(o_string *as_string,
                        return 0;
                }
                nommu_addchr(as_string, ch);
+               len_single_ch = ch;
                ch |= quote_mask;
 
                /* It's possible to just call add_till_closing_bracket() at this point.
@@ -4387,19 +4600,28 @@ static int parse_dollar(o_string *as_string,
                                /* handle parameter expansions
                                 * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02
                                 */
-                               if (!strchr(VAR_SUBST_OPS, ch)) /* ${var<bad_char>... */
-                                       goto bad_dollar_syntax;
-
+                               if (!strchr(VAR_SUBST_OPS, ch)) { /* ${var<bad_char>... */
+                                       if (len_single_ch != '#'
+                                       /*|| !strchr(SPECIAL_VARS_STR, ch) - disallow errors like ${#+} ? */
+                                        || i_peek(input) != '}'
+                                       ) {
+                                               goto bad_dollar_syntax;
+                                       }
+                                       /* else: it's "length of C" ${#C} op,
+                                        * where C is a single char
+                                        * special var name, e.g. ${#!}.
+                                        */
+                               }
                                /* Eat everything until closing '}' (or ':') */
                                end_ch = '}';
-                               if (ENABLE_HUSH_BASH_COMPAT
+                               if (BASH_SUBSTR
                                 && ch == ':'
                                 && !strchr(MINUS_PLUS_EQUAL_QUESTION, i_peek(input))
                                ) {
                                        /* It's ${var:N[:M]} thing */
                                        end_ch = '}' * 0x100 + ':';
                                }
-                               if (ENABLE_HUSH_BASH_COMPAT
+                               if (BASH_PATTERN_SUBST
                                 && ch == '/'
                                ) {
                                        /* It's ${var/[/]pattern[/repl]} thing */
@@ -4426,7 +4648,9 @@ static int parse_dollar(o_string *as_string,
                                        o_addchr(as_string, last_ch);
                                }
 
-                               if (ENABLE_HUSH_BASH_COMPAT && (end_ch & 0xff00)) {
+                               if ((BASH_SUBSTR || BASH_PATTERN_SUBST)
+                                        && (end_ch & 0xff00)
+                               ) {
                                        /* close the first block: */
                                        o_addchr(dest, SPECIAL_VAR_SYMBOL);
                                        /* while parsing N from ${var:N[:M]}
@@ -4437,13 +4661,14 @@ static int parse_dollar(o_string *as_string,
                                                goto again;
                                        }
                                        /* got '}' */
-                                       if (end_ch == '}' * 0x100 + ':') {
+                                       if (BASH_SUBSTR && end_ch == '}' * 0x100 + ':') {
                                                /* it's ${var:N} - emulate :999999999 */
                                                o_addstr(dest, "999999999");
                                        } /* else: it's ${var/[/]pattern} */
                                }
                                break;
                        }
+                       len_single_ch = 0; /* it can't be ${#C} op */
                }
                o_addchr(dest, SPECIAL_VAR_SYMBOL);
                break;
@@ -4512,7 +4737,7 @@ static int parse_dollar(o_string *as_string,
 }
 
 #if BB_MMU
-# if ENABLE_HUSH_BASH_COMPAT
+# if BASH_PATTERN_SUBST
 #define encode_string(as_string, dest, input, dquote_end, process_bkslash) \
        encode_string(dest, input, dquote_end, process_bkslash)
 # else
@@ -4524,7 +4749,7 @@ static int parse_dollar(o_string *as_string,
 
 #else /* !MMU */
 
-# if ENABLE_HUSH_BASH_COMPAT
+# if BASH_PATTERN_SUBST
 /* all parameters are needed, no macro tricks */
 # else
 #define encode_string(as_string, dest, input, dquote_end, process_bkslash) \
@@ -4537,7 +4762,7 @@ static int encode_string(o_string *as_string,
                int dquote_end,
                int process_bkslash)
 {
-#if !ENABLE_HUSH_BASH_COMPAT
+#if !BASH_PATTERN_SUBST
        const int process_bkslash = 1;
 #endif
        int ch;
@@ -4706,7 +4931,8 @@ static struct pipe *parse_stream(char **pstring,
                        next = i_peek(input);
 
                is_special = "{}<>;&|()#'" /* special outside of "str" */
-                               "\\$\"" IF_HUSH_TICK("`"); /* always special */
+                               "\\$\"" IF_HUSH_TICK("`") /* always special */
+                               SPECIAL_VAR_SYMBOL_STR;
                /* Are { and } special here? */
                if (ctx.command->argv /* word [word]{... - non-special */
                 || dest.length       /* word{... - non-special */
@@ -4765,7 +4991,9 @@ static struct pipe *parse_stream(char **pstring,
                                         * Really, ask yourself, why
                                         * "cmd && <newline>" doesn't start
                                         * cmd but waits for more input?
-                                        * No reason...)
+                                        * The only reason is that it might be
+                                        * a "cmd1 && <nl> cmd2 &" construct,
+                                        * cmd1 may need to run in BG).
                                         */
                                        struct pipe *pi = ctx.list_head;
                                        if (pi->num_cmds != 0       /* check #1 */
@@ -4861,10 +5089,16 @@ static struct pipe *parse_stream(char **pstring,
                                else
                                        o_free_unsafe(&ctx.as_string);
 #endif
-                               debug_leave();
+                               if (ch != ';' && IS_NULL_PIPE(ctx.list_head)) {
+                                       /* Example: bare "{ }", "()" */
+                                       G.last_exitcode = 2; /* bash compat */
+                                       syntax_error_unexpected_ch(ch);
+                                       goto parse_error2;
+                               }
                                debug_printf_parse("parse_stream return %p: "
                                                "end_trigger char found\n",
                                                ctx.list_head);
+                               debug_leave();
                                return ctx.list_head;
                        }
                }
@@ -4924,14 +5158,23 @@ static struct pipe *parse_stream(char **pstring,
                case '#':
                        if (dest.length == 0 && !dest.has_quoted_part) {
                                /* skip "#comment" */
+                               /* note: we do not add it to &ctx.as_string */
+/* TODO: in bash:
+ * comment inside $() goes to the next \n, even inside quoted string (!):
+ * cmd "$(cmd2 #comment)" - syntax error
+ * cmd "`cmd2 #comment`" - ok
+ * We accept both (comment ends where command subst ends, in both cases).
+ */
                                while (1) {
                                        ch = i_peek(input);
-                                       if (ch == EOF || ch == '\n')
+                                       if (ch == '\n') {
+                                               nommu_addchr(&ctx.as_string, '\n');
+                                               break;
+                                       }
+                                       ch = i_getch(input);
+                                       if (ch == EOF)
                                                break;
-                                       i_getch(input);
-                                       /* note: we do not add it to &ctx.as_string */
                                }
-                               nommu_addchr(&ctx.as_string, '\n');
                                continue; /* back to top of while (1) */
                        }
                        break;
@@ -4961,8 +5204,14 @@ static struct pipe *parse_stream(char **pstring,
                /* Note: nommu_addchr(&ctx.as_string, ch) is already done */
 
                switch (ch) {
-               case '#': /* non-comment #: "echo a#b" etc */
-                       o_addQchr(&dest, ch);
+               case SPECIAL_VAR_SYMBOL:
+                       /* Convert raw ^C to corresponding special variable reference */
+                       o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+                       o_addchr(&dest, SPECIAL_VAR_QUOTED_SVS);
+                       /* fall through */
+               case '#':
+                       /* non-comment #: "echo a#b" etc */
+                       o_addchr(&dest, ch);
                        break;
                case '\\':
                        if (next == EOF) {
@@ -5004,6 +5253,11 @@ static struct pipe *parse_stream(char **pstring,
                                        nommu_addchr(&ctx.as_string, ch);
                                        if (ch == '\'')
                                                break;
+                                       if (ch == SPECIAL_VAR_SYMBOL) {
+                                               /* Convert raw ^C to corresponding special variable reference */
+                                               o_addchr(&dest, SPECIAL_VAR_SYMBOL);
+                                               o_addchr(&dest, SPECIAL_VAR_QUOTED_SVS);
+                                       }
                                        o_addqchr(&dest, ch);
                                }
                        }
@@ -5122,9 +5376,9 @@ static struct pipe *parse_stream(char **pstring,
                        /* proper use of this character is caught by end_trigger:
                         * if we see {, we call parse_group(..., end_trigger='}')
                         * and it will match } earlier (not here). */
-                       syntax_error_unexpected_ch(ch);
                        G.last_exitcode = 2;
-                       goto parse_error1;
+                       syntax_error_unexpected_ch(ch);
+                       goto parse_error2;
                default:
                        if (HUSH_DEBUG)
                                bb_error_msg_and_die("BUG: unexpected %c\n", ch);
@@ -5133,7 +5387,7 @@ static struct pipe *parse_stream(char **pstring,
 
  parse_error:
        G.last_exitcode = 1;
- parse_error1:
+ parse_error2:
        {
                struct parse_context *pctx;
                IF_HAS_KEYWORDS(struct parse_context *p2;)
@@ -5180,7 +5434,7 @@ static struct pipe *parse_stream(char **pstring,
 /*** Execution routines ***/
 
 /* Expansion can recurse, need forward decls: */
-#if !ENABLE_HUSH_BASH_COMPAT
+#if !BASH_PATTERN_SUBST && !ENABLE_HUSH_CASE
 /* only ${var/pattern/repl} (its pattern part) needs additional mode */
 #define expand_string_to_string(str, do_unbackslash) \
        expand_string_to_string(str)
@@ -5213,7 +5467,7 @@ static void o_addblock_duplicate_backslash(o_string *o, const char *str, int len
                        /* And now we want to add { or } and continue:
                         *  o_addchr(o, c);
                         *  continue;
-                        * luckily, just falling throught achieves this.
+                        * luckily, just falling through achieves this.
                         */
                }
 #endif
@@ -5301,13 +5555,16 @@ static int expand_on_ifs(int *ended_with_ifs, o_string *output, int n, const cha
  * Returns malloced string.
  * As an optimization, we return NULL if expansion is not needed.
  */
-#if !ENABLE_HUSH_BASH_COMPAT
+#if !BASH_PATTERN_SUBST
 /* only ${var/pattern/repl} (its pattern part) needs additional mode */
 #define encode_then_expand_string(str, process_bkslash, do_unbackslash) \
        encode_then_expand_string(str)
 #endif
 static char *encode_then_expand_string(const char *str, int process_bkslash, int do_unbackslash)
 {
+#if !BASH_PATTERN_SUBST
+       enum { do_unbackslash = 1 };
+#endif
        char *exp_str;
        struct in_str input;
        o_string dest = NULL_O_STRING;
@@ -5350,12 +5607,12 @@ static arith_t expand_and_evaluate_arith(const char *arg, const char **errmsg_p)
        if (errmsg_p)
                *errmsg_p = math_state.errmsg;
        if (math_state.errmsg)
-               die_if_script(math_state.errmsg);
+               msg_and_die_if_script(math_state.errmsg);
        return res;
 }
 #endif
 
-#if ENABLE_HUSH_BASH_COMPAT
+#if BASH_PATTERN_SUBST
 /* ${var/[/]pattern[/repl]} helpers */
 static char *strstr_pattern(char *val, const char *pattern, int *size)
 {
@@ -5389,17 +5646,15 @@ static char *replace_pattern(char *val, const char *pattern, const char *repl, c
                        break;
 
                result = xrealloc(result, res_len + (s - val) + repl_len + 1);
-               memcpy(result + res_len, val, s - val);
-               res_len += s - val;
-               strcpy(result + res_len, repl);
-               res_len += repl_len;
+               strcpy(mempcpy(result + res_len, val, s - val), repl);
+               res_len += (s - val) + repl_len;
                debug_printf_varexp("val:'%s' s:'%s' result:'%s'\n", val, s, result);
 
                val = s + size;
                if (exp_op == '/')
                        break;
        }
-       if (val[0] && result) {
+       if (*val && result) {
                result = xrealloc(result, res_len + strlen(val) + 1);
                strcpy(result + res_len, val);
                debug_printf_varexp("val:'%s' result:'%s'\n", val, result);
@@ -5407,7 +5662,7 @@ static char *replace_pattern(char *val, const char *pattern, const char *repl, c
        debug_printf_varexp("result:'%s'\n", result);
        return result;
 }
-#endif
+#endif /* BASH_PATTERN_SUBST */
 
 /* Helper:
  * Handles <SPECIAL_VAR_SYMBOL>varname...<SPECIAL_VAR_SYMBOL> construct.
@@ -5432,8 +5687,10 @@ static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, cha
        first_char = arg[0] = arg0 & 0x7f;
        exp_op = 0;
 
-       if (first_char == '#'      /* ${#... */
-        && arg[1] && !exp_saveptr /* not ${#} and not ${#<op_char>...} */
+       if (first_char == '#' && arg[1] /* ${#...} but not ${#} */
+        && (!exp_saveptr               /* and ( not(${#<op_char>...}) */
+           || (arg[2] == '\0' && strchr(SPECIAL_VARS_STR, arg[1])) /* or ${#C} "len of $C" ) */
+           )           /* NB: skipping ^^^specvar check mishandles ${#::2} */
        ) {
                /* It must be length operator: ${#var} */
                var++;
@@ -5455,7 +5712,7 @@ static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, cha
                        if (exp_op == ':') {
                                exp_op = *exp_word++;
 //TODO: try ${var:} and ${var:bogus} in non-bash config
-                               if (ENABLE_HUSH_BASH_COMPAT
+                               if (BASH_SUBSTR
                                 && (!exp_op || !strchr(MINUS_PLUS_EQUAL_QUESTION, exp_op))
                                ) {
                                        /* oops... it's ${var:N[:M]}, not ${var:?xxx} or some such */
@@ -5537,7 +5794,7 @@ static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, cha
                                }
                        }
                }
-#if ENABLE_HUSH_BASH_COMPAT
+#if BASH_PATTERN_SUBST
                else if (exp_op == '/' || exp_op == '\\') {
                        /* It's ${var/[/]pattern[/repl]} thing.
                         * Note that in encoded form it has TWO parts:
@@ -5584,9 +5841,9 @@ static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, cha
                                free(repl);
                        }
                }
-#endif
+#endif /* BASH_PATTERN_SUBST */
                else if (exp_op == ':') {
-#if ENABLE_HUSH_BASH_COMPAT && ENABLE_FEATURE_SH_MATH
+#if BASH_SUBSTR && ENABLE_FEATURE_SH_MATH
                        /* It's ${var:N[:M]} bashism.
                         * Note that in encoded form it has TWO parts:
                         * var:N<SPECIAL_VAR_SYMBOL>M<SPECIAL_VAR_SYMBOL>
@@ -5606,27 +5863,34 @@ static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, cha
                        if (errmsg)
                                goto arith_err;
                        debug_printf_varexp("len:'%s'=%lld\n", exp_word, (long long)len);
-                       if (len >= 0) { /* bash compat: len < 0 is illegal */
-                               if (beg < 0) /* bash compat */
-                                       beg = 0;
-                               debug_printf_varexp("from val:'%s'\n", val);
-                               if (len == 0 || !val || beg >= strlen(val)) {
+                       if (beg < 0) {
+                               /* negative beg counts from the end */
+                               beg = (arith_t)strlen(val) + beg;
+                               if (beg < 0) /* ${v: -999999} is "" */
+                                       beg = len = 0;
+                       }
+                       debug_printf_varexp("from val:'%s'\n", val);
+                       if (len < 0) {
+                               /* in bash, len=-n means strlen()-n */
+                               len = (arith_t)strlen(val) - beg + len;
+                               if (len < 0) /* bash compat */
+                                       msg_and_die_if_script("%s: substring expression < 0", var);
+                       }
+                       if (len <= 0 || !val || beg >= strlen(val)) {
  arith_err:
-                                       val = NULL;
-                               } else {
-                                       /* Paranoia. What if user entered 9999999999999
-                                        * which fits in arith_t but not int? */
-                                       if (len >= INT_MAX)
-                                               len = INT_MAX;
-                                       val = to_be_freed = xstrndup(val + beg, len);
-                               }
-                               debug_printf_varexp("val:'%s'\n", val);
-                       } else
-#endif
-                       {
-                               die_if_script("malformed ${%s:...}", var);
                                val = NULL;
+                       } else {
+                               /* Paranoia. What if user entered 9999999999999
+                                * which fits in arith_t but not int? */
+                               if (len >= INT_MAX)
+                                       len = INT_MAX;
+                               val = to_be_freed = xstrndup(val + beg, len);
                        }
+                       debug_printf_varexp("val:'%s'\n", val);
+#else /* not (HUSH_SUBSTR_EXPANSION && FEATURE_SH_MATH) */
+                       msg_and_die_if_script("malformed ${%s:...}", var);
+                       val = NULL;
+#endif
                } else { /* one of "-=+?" */
                        /* Standard-mandated substitution ops:
                         * ${var?word} - indicate error if unset
@@ -5661,9 +5925,13 @@ static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, cha
                                        exp_word = to_be_freed;
                                if (exp_op == '?') {
                                        /* mimic bash message */
-                                       die_if_script("%s: %s",
+                                       msg_and_die_if_script("%s: %s",
                                                var,
-                                               exp_word[0] ? exp_word : "parameter null or not set"
+                                               exp_word[0]
+                                               ? exp_word
+                                               : "parameter null or not set"
+                                               /* ash has more specific messages, a-la: */
+                                               /*: (exp_save == ':' ? "parameter null or not set" : "parameter not set")*/
                                        );
 //TODO: how interactive bash aborts expansion mid-command?
                                } else {
@@ -5674,11 +5942,11 @@ static NOINLINE const char *expand_one_var(char **to_be_freed_pp, char *arg, cha
                                        /* ${var=[word]} or ${var:=[word]} */
                                        if (isdigit(var[0]) || var[0] == '#') {
                                                /* mimic bash message */
-                                               die_if_script("$%s: cannot assign in this way", var);
+                                               msg_and_die_if_script("$%s: cannot assign in this way", var);
                                                val = NULL;
                                        } else {
                                                char *new_var = xasprintf("%s=%s", var, val);
-                                               set_local_var(new_var, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+                                               set_local_var(new_var, /*flag:*/ 0);
                                        }
                                }
                        }
@@ -5802,13 +6070,18 @@ static NOINLINE int expand_vars_to_list(o_string *output, int n, char *arg)
                        arg++;
                        cant_be_null = 0x80;
                        break;
+               case SPECIAL_VAR_QUOTED_SVS:
+                       /* <SPECIAL_VAR_SYMBOL><SPECIAL_VAR_QUOTED_SVS><SPECIAL_VAR_SYMBOL> */
+                       arg++;
+                       val = SPECIAL_VAR_SYMBOL_STR;
+                       break;
 #if ENABLE_HUSH_TICK
                case '`': /* <SPECIAL_VAR_SYMBOL>`cmd<SPECIAL_VAR_SYMBOL> */
                        *p = '\0'; /* replace trailing <SPECIAL_VAR_SYMBOL> */
                        arg++;
                        /* Can't just stuff it into output o_string,
                         * expanded result may need to be globbed
-                        * and $IFS-splitted */
+                        * and $IFS-split */
                        debug_printf_subst("SUBST '%s' first_ch %x\n", arg, first_ch);
                        G.last_exitcode = process_command_subs(&subst_result, arg);
                        debug_printf_subst("SUBST RES:%d '%s'\n", G.last_exitcode, subst_result.data);
@@ -5912,7 +6185,7 @@ static char **expand_strvec_to_strvec(char **argv)
        return expand_variables(argv, EXP_FLAG_GLOB | EXP_FLAG_ESC_GLOB_CHARS);
 }
 
-#if ENABLE_HUSH_BASH_COMPAT
+#if BASH_TEST2
 static char **expand_strvec_to_strvec_singleword_noglob(char **argv)
 {
        return expand_variables(argv, EXP_FLAG_SINGLEWORD);
@@ -5927,7 +6200,7 @@ static char **expand_strvec_to_strvec_singleword_noglob(char **argv)
  */
 static char *expand_string_to_string(const char *str, int do_unbackslash)
 {
-#if !ENABLE_HUSH_BASH_COMPAT
+#if !BASH_PATTERN_SUBST && !ENABLE_HUSH_CASE
        const int do_unbackslash = 1;
 #endif
        char *argv[2], **list;
@@ -5960,7 +6233,7 @@ static char *expand_string_to_string(const char *str, int do_unbackslash)
        return (char*)list;
 }
 
-/* Used for "eval" builtin */
+#if ENABLE_HUSH_CASE
 static char* expand_strvec_to_string(char **argv)
 {
        char **list;
@@ -5982,6 +6255,7 @@ static char* expand_strvec_to_string(char **argv)
        debug_printf_expand("strvec_to_string='%s'\n", (char*)list);
        return (char*)list;
 }
+#endif
 
 static char **expand_assignments(char **argv, int count)
 {
@@ -6274,8 +6548,17 @@ static void parse_and_run_string(const char *s)
 static void parse_and_run_file(FILE *f)
 {
        struct in_str input;
+#if BASH_LINENO_VAR
+       unsigned sv;
+
+       sv = G.lineno;
+       G.lineno = 1;
+#endif
        setup_file_in_str(&input, f);
        parse_and_run_stream(&input, ';');
+#if BASH_LINENO_VAR
+       G.lineno = sv;
+#endif
 }
 
 #if ENABLE_HUSH_TICK
@@ -6502,85 +6785,175 @@ static void setup_heredoc(struct redir_struct *redir)
        wait(NULL); /* wait till child has died */
 }
 
-/* fd: redirect wants this fd to be used (e.g. 3>file).
- * Move all conflicting internally used fds,
- * and remember them so that we can restore them later.
- */
-static int save_fds_on_redirect(int fd, int squirrel[3])
+struct squirrel {
+       int orig_fd;
+       int moved_to;
+       /* moved_to = n: fd was moved to n; restore back to orig_fd after redir */
+       /* moved_to = -1: fd was opened by redirect; close orig_fd after redir */
+};
+
+static struct squirrel *append_squirrel(struct squirrel *sq, int i, int orig, int moved)
+{
+       sq = xrealloc(sq, (i + 2) * sizeof(sq[0]));
+       sq[i].orig_fd = orig;
+       sq[i].moved_to = moved;
+       sq[i+1].orig_fd = -1; /* end marker */
+       return sq;
+}
+
+static struct squirrel *add_squirrel(struct squirrel *sq, int fd, int avoid_fd)
 {
-       if (squirrel) {
-               /* Handle redirects of fds 0,1,2 */
+       int moved_to;
+       int i;
 
-               /* If we collide with an already moved stdio fd... */
-               if (fd == squirrel[0]) {
-                       squirrel[0] = xdup_and_close(squirrel[0], F_DUPFD);
-                       return 1;
-               }
-               if (fd == squirrel[1]) {
-                       squirrel[1] = xdup_and_close(squirrel[1], F_DUPFD);
-                       return 1;
+       i = 0;
+       if (sq) for (; sq[i].orig_fd >= 0; i++) {
+               /* If we collide with an already moved fd... */
+               if (fd == sq[i].moved_to) {
+                       sq[i].moved_to = fcntl_F_DUPFD(sq[i].moved_to, avoid_fd);
+                       debug_printf_redir("redirect_fd %d: already busy, moving to %d\n", fd, sq[i].moved_to);
+                       if (sq[i].moved_to < 0) /* what? */
+                               xfunc_die();
+                       return sq;
                }
-               if (fd == squirrel[2]) {
-                       squirrel[2] = xdup_and_close(squirrel[2], F_DUPFD);
-                       return 1;
+               if (fd == sq[i].orig_fd) {
+                       /* Example: echo Hello >/dev/null 1>&2 */
+                       debug_printf_redir("redirect_fd %d: already moved\n", fd);
+                       return sq;
                }
-               /* If we are about to redirect stdio fd, and did not yet move it... */
-               if (fd <= 2 && squirrel[fd] < 0) {
-                       /* We avoid taking stdio fds */
-                       squirrel[fd] = fcntl(fd, F_DUPFD, 10);
-                       if (squirrel[fd] < 0 && errno != EBADF)
-                               xfunc_die();
-                       return 0; /* "we did not close fd" */
+       }
+
+       /* If this fd is open, we move and remember it; if it's closed, moved_to = -1 */
+       moved_to = fcntl_F_DUPFD(fd, avoid_fd);
+       debug_printf_redir("redirect_fd %d: previous fd is moved to %d (-1 if it was closed)\n", fd, moved_to);
+       if (moved_to < 0 && errno != EBADF)
+               xfunc_die();
+       return append_squirrel(sq, i, fd, moved_to);
+}
+
+static struct squirrel *add_squirrel_closed(struct squirrel *sq, int fd)
+{
+       int i;
+
+       i = 0;
+       if (sq) for (; sq[i].orig_fd >= 0; i++) {
+               /* If we collide with an already moved fd... */
+               if (fd == sq[i].orig_fd) {
+                       /* Examples:
+                        * "echo 3>FILE 3>&- 3>FILE"
+                        * "echo 3>&- 3>FILE"
+                        * No need for last redirect to insert
+                        * another "need to close 3" indicator.
+                        */
+                       debug_printf_redir("redirect_fd %d: already moved or closed\n", fd);
+                       return sq;
                }
        }
 
+       debug_printf_redir("redirect_fd %d: previous fd was closed\n", fd);
+       return append_squirrel(sq, i, fd, -1);
+}
+
+/* fd: redirect wants this fd to be used (e.g. 3>file).
+ * Move all conflicting internally used fds,
+ * and remember them so that we can restore them later.
+ */
+static int save_fd_on_redirect(int fd, int avoid_fd, struct squirrel **sqp)
+{
+       if (avoid_fd < 9) /* the important case here is that it can be -1 */
+               avoid_fd = 9;
+
 #if ENABLE_HUSH_INTERACTIVE
-       if (fd != 0 && fd == G.interactive_fd) {
-               G.interactive_fd = xdup_and_close(G.interactive_fd, F_DUPFD_CLOEXEC);
-               return 1;
+       if (fd == G.interactive_fd) {
+               /* Testcase: "ls -l /proc/$$/fd 255>&-" should work */
+               G.interactive_fd = xdup_CLOEXEC_and_close(G.interactive_fd, avoid_fd);
+               debug_printf_redir("redirect_fd %d: matches interactive_fd, moving it to %d\n", fd, G.interactive_fd);
+               return 1; /* "we closed fd" */
        }
 #endif
-
        /* Are we called from setup_redirects(squirrel==NULL)? Two cases:
         * (1) Redirect in a forked child. No need to save FILEs' fds,
         * we aren't going to use them anymore, ok to trash.
-        * (2) "exec 3>FILE". Bummer. We can save FILEs' fds,
-        * but how are we doing to use them?
+        * (2) "exec 3>FILE". Bummer. We can save script FILEs' fds,
+        * but how are we doing to restore them?
         * "fileno(fd) = new_fd" can't be done.
         */
-       if (!squirrel)
+       if (!sqp)
                return 0;
 
-       return save_FILEs_on_redirect(fd);
+       /* If this one of script's fds? */
+       if (save_FILEs_on_redirect(fd, avoid_fd))
+               return 1; /* yes. "we closed fd" */
+
+       /* Check whether it collides with any open fds (e.g. stdio), save fds as needed */
+       *sqp = add_squirrel(*sqp, fd, avoid_fd);
+       return 0; /* "we did not close fd" */
 }
 
-static void restore_redirects(int squirrel[3])
+static void restore_redirects(struct squirrel *sq)
 {
-       int i, fd;
-       for (i = 0; i <= 2; i++) {
-               fd = squirrel[i];
-               if (fd != -1) {
-                       /* We simply die on error */
-                       xmove_fd(fd, i);
+       if (sq) {
+               int i;
+               for (i = 0; sq[i].orig_fd >= 0; i++) {
+                       if (sq[i].moved_to >= 0) {
+                               /* We simply die on error */
+                               debug_printf_redir("restoring redirected fd from %d to %d\n", sq[i].moved_to, sq[i].orig_fd);
+                               xmove_fd(sq[i].moved_to, sq[i].orig_fd);
+                       } else {
+                               /* cmd1 9>FILE; cmd2_should_see_fd9_closed */
+                               debug_printf_redir("restoring redirected fd %d: closing it\n", sq[i].orig_fd);
+                               close(sq[i].orig_fd);
+                       }
                }
+               free(sq);
        }
 
-       /* Moved G.interactive_fd stays on new fd, not doing anything for it */
+       /* If moved, G.interactive_fd stays on new fd, not restoring it */
 
        restore_redirected_FILEs();
 }
 
+#if ENABLE_FEATURE_SH_STANDALONE && BB_MMU
+static void close_saved_fds_and_FILE_fds(void)
+{
+       if (G_interactive_fd)
+               close(G_interactive_fd);
+       close_all_FILE_list();
+}
+#endif
+
+static int internally_opened_fd(int fd, struct squirrel *sq)
+{
+       int i;
+
+#if ENABLE_HUSH_INTERACTIVE
+       if (fd == G.interactive_fd)
+               return 1;
+#endif
+       /* If this one of script's fds? */
+       if (fd_in_FILEs(fd))
+               return 1;
+
+       if (sq) for (i = 0; sq[i].orig_fd >= 0; i++) {
+               if (fd == sq[i].moved_to)
+                       return 1;
+       }
+       return 0;
+}
+
 /* squirrel != NULL means we squirrel away copies of stdin, stdout,
  * and stderr if they are redirected. */
-static int setup_redirects(struct command *prog, int squirrel[])
+static int setup_redirects(struct command *prog, struct squirrel **sqp)
 {
-       int openfd, mode;
        struct redir_struct *redir;
 
        for (redir = prog->redirects; redir; redir = redir->next) {
+               int newfd;
+               int closed;
+
                if (redir->rd_type == REDIRECT_HEREDOC2) {
                        /* "rd_fd<<HERE" case */
-                       save_fds_on_redirect(redir->rd_fd, squirrel);
+                       save_fd_on_redirect(redir->rd_fd, /*avoid:*/ 0, sqp);
                        /* for REDIRECT_HEREDOC2, rd_filename holds _contents_
                         * of the heredoc */
                        debug_printf_parse("set heredoc '%s'\n",
@@ -6592,20 +6965,22 @@ static int setup_redirects(struct command *prog, int squirrel[])
                if (redir->rd_dup == REDIRFD_TO_FILE) {
                        /* "rd_fd<*>file" case (<*> is <,>,>>,<>) */
                        char *p;
+                       int mode;
+
                        if (redir->rd_filename == NULL) {
                                /*
                                 * Examples:
                                 * "cmd >" (no filename)
                                 * "cmd > <file" (2nd redirect starts too early)
                                 */
-                               die_if_script("syntax error: %s", "invalid redirect");
+                               syntax_error("invalid redirect");
                                continue;
                        }
                        mode = redir_table[redir->rd_type].mode;
                        p = expand_string_to_string(redir->rd_filename, /*unbackslash:*/ 1);
-                       openfd = open_or_warn(p, mode);
+                       newfd = open_or_warn(p, mode);
                        free(p);
-                       if (openfd < 0) {
+                       if (newfd < 0) {
                                /* Error message from open_or_warn can be lost
                                 * if stderr has been redirected, but bash
                                 * and ash both lose it as well
@@ -6613,27 +6988,52 @@ static int setup_redirects(struct command *prog, int squirrel[])
                                 */
                                return 1;
                        }
+                       if (newfd == redir->rd_fd && sqp) {
+                               /* open() gave us precisely the fd we wanted.
+                                * This means that this fd was not busy
+                                * (not opened to anywhere).
+                                * Remember to close it on restore:
+                                */
+                               *sqp = add_squirrel_closed(*sqp, newfd);
+                               debug_printf_redir("redir to previously closed fd %d\n", newfd);
+                       }
                } else {
-                       /* "rd_fd<*>rd_dup" or "rd_fd<*>-" cases */
-                       openfd = redir->rd_dup;
-               }
-
-               if (openfd != redir->rd_fd) {
-                       int closed = save_fds_on_redirect(redir->rd_fd, squirrel);
-                       if (openfd == REDIRFD_CLOSE) {
-                               /* "rd_fd >&-" means "close me" */
-                               if (!closed) {
-                                       /* ^^^ optimization: saving may already
-                                        * have closed it. If not... */
-                                       close(redir->rd_fd);
-                               }
-                       } else {
-                               xdup2(openfd, redir->rd_fd);
-                               if (redir->rd_dup == REDIRFD_TO_FILE)
-                                       /* "rd_fd > FILE" */
-                                       close(openfd);
-                               /* else: "rd_fd > rd_dup" */
+                       /* "rd_fd>&rd_dup" or "rd_fd>&-" case */
+                       newfd = redir->rd_dup;
+               }
+
+               if (newfd == redir->rd_fd)
+                       continue;
+
+               /* if "N>FILE": move newfd to redir->rd_fd */
+               /* if "N>&M": dup newfd to redir->rd_fd */
+               /* if "N>&-": close redir->rd_fd (newfd is REDIRFD_CLOSE) */
+
+               closed = save_fd_on_redirect(redir->rd_fd, /*avoid:*/ newfd, sqp);
+               if (newfd == REDIRFD_CLOSE) {
+                       /* "N>&-" means "close me" */
+                       if (!closed) {
+                               /* ^^^ optimization: saving may already
+                                * have closed it. If not... */
+                               close(redir->rd_fd);
                        }
+                       /* Sometimes we do another close on restore, getting EBADF.
+                        * Consider "echo 3>FILE 3>&-"
+                        * first redirect remembers "need to close 3",
+                        * and second redirect closes 3! Restore code then closes 3 again.
+                        */
+               } else {
+                       /* if newfd is a script fd or saved fd, simulate EBADF */
+                       if (internally_opened_fd(newfd, sqp ? *sqp : NULL)) {
+                               //errno = EBADF;
+                               //bb_perror_msg_and_die("can't duplicate file descriptor");
+                               newfd = -1; /* same effect as code above */
+                       }
+                       xdup2(newfd, redir->rd_fd);
+                       if (redir->rd_dup == REDIRFD_TO_FILE)
+                               /* "rd_fd > FILE" */
+                               close(newfd);
+                       /* else: "rd_fd > rd_dup" */
                }
        }
        return 0;
@@ -6795,18 +7195,39 @@ static void exec_function(char ***to_free,
                char **argv)
 {
 # if BB_MMU
-       int n = 1;
+       int n;
 
        argv[0] = G.global_argv[0];
        G.global_argv = argv;
-       while (*++argv)
-               n++;
-       G.global_argc = n;
+       G.global_argc = n = 1 + string_array_len(argv + 1);
+
+// Example when we are here: "cmd | func"
+// func will run with saved-redirect fds open.
+// $ f() { echo /proc/self/fd/*; }
+// $ true | f
+// /proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2 /proc/self/fd/255 /proc/self/fd/3
+// stdio^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ G_interactive_fd^ DIR fd for glob
+// Same in script:
+// $ . ./SCRIPT
+// /proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2 /proc/self/fd/255 /proc/self/fd/3 /proc/self/fd/4
+// stdio^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ G_interactive_fd^ opened ./SCRIPT DIR fd for glob
+// They are CLOEXEC so external programs won't see them, but
+// for "more correctness" we might want to close those extra fds here:
+//?    close_saved_fds_and_FILE_fds();
+
+       /* "we are in function, ok to use return" */
+       G_flag_return_in_progress = -1;
+       IF_HUSH_LOCAL(G.func_nest_level++;)
+
        /* On MMU, funcp->body is always non-NULL */
        n = run_list(funcp->body);
        fflush_all();
        _exit(n);
 # else
+//?    close_saved_fds_and_FILE_fds();
+
+//TODO: check whether "true | func_with_return" works
+
        re_execute_shell(to_free,
                        funcp->body_as_string,
                        G.global_argv[0],
@@ -6826,9 +7247,7 @@ static int run_function(const struct function *funcp, char **argv)
        /* "we are in function, ok to use return" */
        sv_flg = G_flag_return_in_progress;
        G_flag_return_in_progress = -1;
-# if ENABLE_HUSH_LOCAL
-       G.func_nest_level++;
-# endif
+       IF_HUSH_LOCAL(G.func_nest_level++;)
 
        /* On MMU, funcp->body is always non-NULL */
 # if !BB_MMU
@@ -6892,6 +7311,7 @@ static void exec_builtin(char ***to_free,
 #if BB_MMU
        int rcode;
        fflush_all();
+//?    close_saved_fds_and_FILE_fds();
        rcode = x->b_function(argv);
        fflush_all();
        _exit(rcode);
@@ -6954,6 +7374,32 @@ static void dump_cmd_in_x_mode(char **argv)
 # define dump_cmd_in_x_mode(argv) ((void)0)
 #endif
 
+#if ENABLE_HUSH_COMMAND
+static void if_command_vV_print_and_exit(char opt_vV, char *cmd, const char *explanation)
+{
+       char *to_free;
+
+       if (!opt_vV)
+               return;
+
+       to_free = NULL;
+       if (!explanation) {
+               char *path = getenv("PATH");
+               explanation = to_free = find_executable(cmd, &path); /* path == NULL is ok */
+               if (!explanation)
+                       _exit(1); /* PROG was not found */
+               if (opt_vV != 'V')
+                       cmd = to_free; /* -v PROG prints "/path/to/PROG" */
+       }
+       printf((opt_vV == 'V') ? "%s is %s\n" : "%s\n", cmd, explanation);
+       free(to_free);
+       fflush_all();
+       _exit(0);
+}
+#else
+# define if_command_vV_print_and_exit(a,b,c) ((void)0)
+#endif
+
 #if BB_MMU
 #define pseudo_exec_argv(nommu_save, argv, assignment_cnt, argv_expanded) \
        pseudo_exec_argv(argv, assignment_cnt, argv_expanded)
@@ -6974,7 +7420,11 @@ static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
                char **argv, int assignment_cnt,
                char **argv_expanded)
 {
+       const struct built_in_command *x;
        char **new_env;
+#if ENABLE_HUSH_COMMAND
+       char opt_vV = 0;
+#endif
 
        new_env = expand_assignments(argv, assignment_cnt);
        dump_cmd_in_x_mode(new_env);
@@ -7013,24 +7463,8 @@ static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
                goto skip;
 #endif
 
-       /* Check if the command matches any of the builtins.
-        * Depending on context, this might be redundant.  But it's
-        * easier to waste a few CPU cycles than it is to figure out
-        * if this is one of those cases.
-        */
-       {
-               /* On NOMMU, it is more expensive to re-execute shell
-                * just in order to run echo or test builtin.
-                * It's better to skip it here and run corresponding
-                * non-builtin later. */
-               const struct built_in_command *x;
-               x = BB_MMU ? find_builtin(argv[0]) : find_builtin1(argv[0]);
-               if (x) {
-                       exec_builtin(&nommu_save->argv_from_re_execing, x, argv);
-               }
-       }
 #if ENABLE_HUSH_FUNCTIONS
-       /* Check if the command matches any functions */
+       /* Check if the command matches any functions (this goes before bltins) */
        {
                const struct function *funcp = find_function(argv[0]);
                if (funcp) {
@@ -7039,17 +7473,78 @@ static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
        }
 #endif
 
-#if ENABLE_FEATURE_SH_STANDALONE
+#if ENABLE_HUSH_COMMAND
+       /* "command BAR": run BAR without looking it up among functions
+        * "command -v BAR": print "BAR" or "/path/to/BAR"; or exit 1
+        * "command -V BAR": print "BAR is {a function,a shell builtin,/path/to/BAR}"
+        */
+       while (strcmp(argv[0], "command") == 0 && argv[1]) {
+               char *p;
+
+               argv++;
+               p = *argv;
+               if (p[0] != '-' || !p[1])
+                       continue; /* bash allows "command command command [-OPT] BAR" */
+
+               for (;;) {
+                       p++;
+                       switch (*p) {
+                       case '\0':
+                               argv++;
+                               p = *argv;
+                               if (p[0] != '-' || !p[1])
+                                       goto after_opts;
+                               continue; /* next arg is also -opts, process it too */
+                       case 'v':
+                       case 'V':
+                               opt_vV = *p;
+                               continue;
+                       default:
+                               bb_error_msg_and_die("%s: %s: invalid option", "command", argv[0]);
+                       }
+               }
+       }
+ after_opts:
+# if ENABLE_HUSH_FUNCTIONS
+       if (opt_vV && find_function(argv[0]))
+               if_command_vV_print_and_exit(opt_vV, argv[0], "a function");
+# endif
+#endif
+
+       /* Check if the command matches any of the builtins.
+        * Depending on context, this might be redundant.  But it's
+        * easier to waste a few CPU cycles than it is to figure out
+        * if this is one of those cases.
+        */
+       /* Why "BB_MMU ? :" difference in logic? -
+        * On NOMMU, it is more expensive to re-execute shell
+        * just in order to run echo or test builtin.
+        * It's better to skip it here and run corresponding
+        * non-builtin later. */
+       x = BB_MMU ? find_builtin(argv[0]) : find_builtin1(argv[0]);
+       if (x) {
+               if_command_vV_print_and_exit(opt_vV, argv[0], "a shell builtin");
+               exec_builtin(&nommu_save->argv_from_re_execing, x, argv);
+       }
+
+#if ENABLE_FEATURE_SH_STANDALONE
        /* Check if the command matches any busybox applets */
        {
                int a = find_applet_by_name(argv[0]);
                if (a >= 0) {
+                       if_command_vV_print_and_exit(opt_vV, argv[0], "an applet");
 # if BB_MMU /* see above why on NOMMU it is not allowed */
                        if (APPLET_IS_NOEXEC(a)) {
-                               /* Do not leak open fds from opened script files etc */
-                               close_all_FILE_list();
+                               /* Do not leak open fds from opened script files etc.
+                                * Testcase: interactive "ls -l /proc/self/fd"
+                                * should not show tty fd open.
+                                */
+                               close_saved_fds_and_FILE_fds();
+//FIXME: should also close saved redir fds
+                               /* Without this, "rm -i FILE" can't be ^C'ed: */
+                               switch_off_special_sigs(G.special_sig_mask);
                                debug_printf_exec("running applet '%s'\n", argv[0]);
-                               run_applet_no_and_exit(a, argv);
+                               run_noexec_applet_and_exit(a, argv[0], argv);
                        }
 # endif
                        /* Re-exec ourselves */
@@ -7067,6 +7562,7 @@ static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
 #if ENABLE_FEATURE_SH_STANDALONE || BB_MMU
  skip:
 #endif
+       if_command_vV_print_and_exit(opt_vV, argv[0], NULL);
        execvp_or_die(argv);
 }
 
@@ -7146,24 +7642,54 @@ static const char *get_cmdtext(struct pipe *pi)
        return pi->cmdtext;
 }
 
-static void insert_bg_job(struct pipe *pi)
+static void remove_job_from_table(struct pipe *pi)
+{
+       struct pipe *prev_pipe;
+
+       if (pi == G.job_list) {
+               G.job_list = pi->next;
+       } else {
+               prev_pipe = G.job_list;
+               while (prev_pipe->next != pi)
+                       prev_pipe = prev_pipe->next;
+               prev_pipe->next = pi->next;
+       }
+       G.last_jobid = 0;
+       if (G.job_list)
+               G.last_jobid = G.job_list->jobid;
+}
+
+static void delete_finished_job(struct pipe *pi)
+{
+       remove_job_from_table(pi);
+       free_pipe(pi);
+}
+
+static void clean_up_last_dead_job(void)
+{
+       if (G.job_list && !G.job_list->alive_cmds)
+               delete_finished_job(G.job_list);
+}
+
+static void insert_job_into_table(struct pipe *pi)
 {
        struct pipe *job, **jobp;
        int i;
 
-       /* Linear search for the ID of the job to use */
-       pi->jobid = 1;
-       for (job = G.job_list; job; job = job->next)
-               if (job->jobid >= pi->jobid)
-                       pi->jobid = job->jobid + 1;
+       clean_up_last_dead_job();
 
-       /* Add job to the list of running jobs */
+       /* Find the end of the list, and find next job ID to use */
+       i = 0;
        jobp = &G.job_list;
-       while ((job = *jobp) != NULL)
+       while ((job = *jobp) != NULL) {
+               if (job->jobid > i)
+                       i = job->jobid;
                jobp = &job->next;
-       job = *jobp = xmalloc(sizeof(*job));
+       }
+       pi->jobid = i + 1;
 
-       *job = *pi; /* physical copy */
+       /* Create a new job struct at the end */
+       job = *jobp = xmemdup(pi, sizeof(*pi));
        job->next = NULL;
        job->cmds = xzalloc(sizeof(pi->cmds[0]) * pi->num_cmds);
        /* Cannot copy entire pi->cmds[] vector! This causes double frees */
@@ -7177,31 +7703,6 @@ static void insert_bg_job(struct pipe *pi)
                printf("[%u] %u %s\n", job->jobid, (unsigned)job->cmds[0].pid, job->cmdtext);
        G.last_jobid = job->jobid;
 }
-
-static void remove_bg_job(struct pipe *pi)
-{
-       struct pipe *prev_pipe;
-
-       if (pi == G.job_list) {
-               G.job_list = pi->next;
-       } else {
-               prev_pipe = G.job_list;
-               while (prev_pipe->next != pi)
-                       prev_pipe = prev_pipe->next;
-               prev_pipe->next = pi->next;
-       }
-       if (G.job_list)
-               G.last_jobid = G.job_list->jobid;
-       else
-               G.last_jobid = 0;
-}
-
-/* Remove a backgrounded job */
-static void delete_finished_bg_job(struct pipe *pi)
-{
-       remove_bg_job(pi);
-       free_pipe(pi);
-}
 #endif /* JOB */
 
 static int job_exited_or_stopped(struct pipe *pi)
@@ -7288,7 +7789,7 @@ static int process_wait_result(struct pipe *fg_pipe, pid_t childpid, int status)
                                if (G_interactive_fd) {
 #if ENABLE_HUSH_JOB
                                        if (fg_pipe->alive_cmds != 0)
-                                               insert_bg_job(fg_pipe);
+                                               insert_job_into_table(fg_pipe);
 #endif
                                        return rcode;
                                }
@@ -7298,7 +7799,7 @@ static int process_wait_result(struct pipe *fg_pipe, pid_t childpid, int status)
                        /* There are still running processes in the fg_pipe */
                        return -1;
                }
-               /* It wasnt in fg_pipe, look for process in bg pipes */
+               /* It wasn't in fg_pipe, look for process in bg pipes */
        }
 
 #if ENABLE_HUSH_JOB
@@ -7317,16 +7818,31 @@ static int process_wait_result(struct pipe *fg_pipe, pid_t childpid, int status)
  found_pi_and_prognum:
        if (dead) {
                /* child exited */
-               pi->cmds[i].pid = 0;
-               pi->cmds[i].cmd_exitcode = WEXITSTATUS(status);
+               int rcode = WEXITSTATUS(status);
                if (WIFSIGNALED(status))
-                       pi->cmds[i].cmd_exitcode = 128 + WTERMSIG(status);
+                       rcode = 128 + WTERMSIG(status);
+               pi->cmds[i].cmd_exitcode = rcode;
+               if (G.last_bg_pid == pi->cmds[i].pid)
+                       G.last_bg_pid_exitcode = rcode;
+               pi->cmds[i].pid = 0;
                pi->alive_cmds--;
                if (!pi->alive_cmds) {
-                       if (G_interactive_fd)
+                       if (G_interactive_fd) {
                                printf(JOB_STATUS_FORMAT, pi->jobid,
                                                "Done", pi->cmdtext);
-                       delete_finished_bg_job(pi);
+                               delete_finished_job(pi);
+                       } else {
+/*
+ * bash deletes finished jobs from job table only in interactive mode,
+ * after "jobs" cmd, or if pid of a new process matches one of the old ones
+ * (see cleanup_dead_jobs(), delete_old_job(), J_NOTIFIED in bash source).
+ * Testcase script: "(exit 3) & sleep 1; wait %1; echo $?" prints 3 in bash.
+ * We only retain one "dead" job, if it's the single job on the list.
+ * This covers most of real-world scenarios where this is useful.
+ */
+                               if (pi != G.job_list)
+                                       delete_finished_job(pi);
+                       }
                }
        } else {
                /* child stopped */
@@ -7483,14 +7999,14 @@ static int checkjobs_and_fg_shell(struct pipe *fg_pipe)
 static int redirect_and_varexp_helper(char ***new_env_p,
                struct variable **old_vars_p,
                struct command *command,
-               int squirrel[3],
+               struct squirrel **sqp,
                char **argv_expanded)
 {
        /* setup_redirects acts on file descriptors, not FILEs.
         * This is perfect for work that comes after exec().
         * Is it really safe for inline use?  Experimentally,
         * things seem to work. */
-       int rcode = setup_redirects(command, squirrel);
+       int rcode = setup_redirects(command, sqp);
        if (rcode == 0) {
                char **new_env = expand_assignments(command->argv, command->assignment_cnt);
                *new_env_p = new_env;
@@ -7510,8 +8026,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
        struct command *command;
        char **argv_expanded;
        char **argv;
-       /* it is not always needed, but we aim to smaller code */
-       int squirrel[] = { -1, -1, -1 };
+       struct squirrel *squirrel = NULL;
        int rcode;
 
        debug_printf_exec("run_pipe start: members:%d\n", pi->num_cmds);
@@ -7568,7 +8083,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
                /* { list } */
                debug_printf("non-subshell group\n");
                rcode = 1; /* exitcode if redir failed */
-               if (setup_redirects(command, squirrel) == 0) {
+               if (setup_redirects(command, &squirrel) == 0) {
                        debug_printf_exec(": run_list\n");
                        rcode = run_list(command->group) & 0xff;
                }
@@ -7590,20 +8105,25 @@ static NOINLINE int run_pipe(struct pipe *pi)
                char **new_env = NULL;
                struct variable *old_vars = NULL;
 
+#if BASH_LINENO_VAR
+               if (G.lineno_var)
+                       strcpy(G.lineno_var + sizeof("LINENO=")-1, utoa(command->lineno));
+#endif
+
                if (argv[command->assignment_cnt] == NULL) {
                        /* Assignments, but no command */
                        /* Ensure redirects take effect (that is, create files).
                         * Try "a=t >file" */
 #if 0 /* A few cases in testsuite fail with this code. FIXME */
-                       rcode = redirect_and_varexp_helper(&new_env, /*old_vars:*/ NULL, command, squirrel, /*argv_expanded:*/ NULL);
+                       rcode = redirect_and_varexp_helper(&new_env, /*old_vars:*/ NULL, command, &squirrel, /*argv_expanded:*/ NULL);
                        /* Set shell variables */
                        if (new_env) {
                                argv = new_env;
                                while (*argv) {
-                                       set_local_var(*argv, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
-                                       /* Do we need to flag set_local_var() errors?
-                                        * "assignment to readonly var" and "putenv error"
-                                        */
+                                       if (set_local_var(*argv, /*flag:*/ 0)) {
+                                               /* assignment to readonly var / putenv error? */
+                                               rcode = 1;
+                                       }
                                        argv++;
                                }
                        }
@@ -7617,7 +8137,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
 
 #else /* Older, bigger, but more correct code */
 
-                       rcode = setup_redirects(command, squirrel);
+                       rcode = setup_redirects(command, &squirrel);
                        restore_redirects(squirrel);
                        /* Set shell variables */
                        if (G_x_mode)
@@ -7628,10 +8148,10 @@ static NOINLINE int run_pipe(struct pipe *pi)
                                        fprintf(stderr, " %s", p);
                                debug_printf_exec("set shell var:'%s'->'%s'\n",
                                                *argv, p);
-                               set_local_var(p, /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
-                               /* Do we need to flag set_local_var() errors?
-                                * "assignment to readonly var" and "putenv error"
-                                */
+                               if (set_local_var(p, /*flag:*/ 0)) {
+                                       /* assignment to readonly var / putenv error? */
+                                       rcode = 1;
+                               }
                                argv++;
                        }
                        if (G_x_mode)
@@ -7649,7 +8169,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
                }
 
                /* Expand the rest into (possibly) many strings each */
-#if ENABLE_HUSH_BASH_COMPAT
+#if BASH_TEST2
                if (command->cmd_type == CMD_SINGLEWORD_NOGLOB) {
                        argv_expanded = expand_strvec_to_strvec_singleword_noglob(argv + command->assignment_cnt);
                } else
@@ -7665,12 +8185,13 @@ static NOINLINE int run_pipe(struct pipe *pi)
                        return G.last_exitcode;
                }
 
-               x = find_builtin(argv_expanded[0]);
 #if ENABLE_HUSH_FUNCTIONS
-               funcp = NULL;
-               if (!x)
-                       funcp = find_function(argv_expanded[0]);
+               /* Check if argv[0] matches any functions (this goes before bltins) */
+               funcp = find_function(argv_expanded[0]);
 #endif
+               x = NULL;
+               if (!funcp)
+                       x = find_builtin(argv_expanded[0]);
                if (x || funcp) {
                        if (!funcp) {
                                if (x->b_function == builtin_exec && argv_expanded[1] == NULL) {
@@ -7680,7 +8201,7 @@ static NOINLINE int run_pipe(struct pipe *pi)
                                        goto clean_up_and_ret1;
                                }
                        }
-                       rcode = redirect_and_varexp_helper(&new_env, &old_vars, command, squirrel, argv_expanded);
+                       rcode = redirect_and_varexp_helper(&new_env, &old_vars, command, &squirrel, argv_expanded);
                        if (rcode == 0) {
                                if (!funcp) {
                                        debug_printf_exec(": builtin '%s' '%s'...\n",
@@ -7710,6 +8231,24 @@ static NOINLINE int run_pipe(struct pipe *pi)
                        add_vars(old_vars);
 /* clean_up_and_ret0: */
                        restore_redirects(squirrel);
+                       /*
+                        * Try "usleep 99999999" + ^C + "echo $?"
+                        * with FEATURE_SH_NOFORK=y.
+                        */
+                       if (!funcp) {
+                               /* It was builtin or nofork.
+                                * if this would be a real fork/execed program,
+                                * it should have died if a fatal sig was received.
+                                * But OTOH, there was no separate process,
+                                * the sig was sent to _shell_, not to non-existing
+                                * child.
+                                * Let's just handle ^C only, this one is obvious:
+                                * we aren't ok with exitcode 0 when ^C was pressed
+                                * during builtin/nofork.
+                                */
+                               if (sigismember(&G.pending_set, SIGINT))
+                                       rcode = 128 + SIGINT;
+                       }
  clean_up_and_ret1:
                        free(argv_expanded);
                        IF_HAS_KEYWORDS(if (pi->pi_inverted) rcode = !rcode;)
@@ -7718,13 +8257,21 @@ static NOINLINE int run_pipe(struct pipe *pi)
                        return rcode;
                }
 
-               if (ENABLE_FEATURE_SH_NOFORK) {
+               if (ENABLE_FEATURE_SH_NOFORK && NUM_APPLETS > 1) {
                        int n = find_applet_by_name(argv_expanded[0]);
                        if (n >= 0 && APPLET_IS_NOFORK(n)) {
-                               rcode = redirect_and_varexp_helper(&new_env, &old_vars, command, squirrel, argv_expanded);
+                               rcode = redirect_and_varexp_helper(&new_env, &old_vars, command, &squirrel, argv_expanded);
                                if (rcode == 0) {
                                        debug_printf_exec(": run_nofork_applet '%s' '%s'...\n",
                                                argv_expanded[0], argv_expanded[1]);
+                                       /*
+                                        * Note: signals (^C) can't interrupt here.
+                                        * We remember them and they will be acted upon
+                                        * after applet returns.
+                                        * This makes applets which can run for a long time
+                                        * and/or wait for user input ineligible for NOFORK:
+                                        * for example, "yes" or "rm" (rm -i waits for input).
+                                        */
                                        rcode = run_nofork_applet(n, argv_expanded);
                                }
                                goto clean_up_and_ret;
@@ -7767,6 +8314,11 @@ static NOINLINE int run_pipe(struct pipe *pi)
                if (cmd_no < pi->num_cmds)
                        xpiped_pair(pipefds);
 
+#if BASH_LINENO_VAR
+               if (G.lineno_var)
+                       strcpy(G.lineno_var + sizeof("LINENO=")-1, utoa(command->lineno));
+#endif
+
                command->pid = BB_MMU ? fork() : vfork();
                if (!command->pid) { /* child */
 #if ENABLE_HUSH_JOB
@@ -7946,6 +8498,7 @@ static int run_list(struct pipe *pi)
        /* Go through list of pipes, (maybe) executing them. */
        for (; pi; pi = IF_HUSH_LOOPS(rword == RES_DONE ? loop_top : ) pi->next) {
                int r;
+               int sv_errexit_depth;
 
                if (G.flag_SIGINT)
                        break;
@@ -7955,6 +8508,16 @@ static int run_list(struct pipe *pi)
                IF_HAS_KEYWORDS(rword = pi->res_word;)
                debug_printf_exec(": rword=%d cond_code=%d last_rword=%d\n",
                                rword, cond_code, last_rword);
+
+               sv_errexit_depth = G.errexit_depth;
+               if (
+#if ENABLE_HUSH_IF
+                   rword == RES_IF || rword == RES_ELIF ||
+#endif
+                   pi->followup != PIPE_SEQ
+               ) {
+                       G.errexit_depth++;
+               }
 #if ENABLE_HUSH_LOOPS
                if ((rword == RES_WHILE || rword == RES_UNTIL || rword == RES_FOR)
                 && loop_top == NULL /* avoid bumping G.depth_of_loop twice */
@@ -8032,7 +8595,7 @@ static int run_list(struct pipe *pi)
                        }
                        /* Insert next value from for_lcur */
                        /* note: *for_lcur already has quotes removed, $var expanded, etc */
-                       set_local_var(xasprintf("%s=%s", pi->cmds[0].argv[0], *for_lcur++), /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ 0);
+                       set_local_var(xasprintf("%s=%s", pi->cmds[0].argv[0], *for_lcur++), /*flag:*/ 0);
                        continue;
                }
                if (rword == RES_IN) {
@@ -8046,6 +8609,7 @@ static int run_list(struct pipe *pi)
                if (rword == RES_CASE) {
                        debug_printf_exec("CASE cond_code:%d\n", cond_code);
                        case_word = expand_strvec_to_string(pi->cmds->argv);
+                       unbackslash(case_word);
                        continue;
                }
                if (rword == RES_MATCH) {
@@ -8057,9 +8621,10 @@ static int run_list(struct pipe *pi)
                        /* all prev words didn't match, does this one match? */
                        argv = pi->cmds->argv;
                        while (*argv) {
-                               char *pattern = expand_string_to_string(*argv, /*unbackslash:*/ 1);
+                               char *pattern = expand_string_to_string(*argv, /*unbackslash:*/ 0);
                                /* TODO: which FNM_xxx flags to use? */
                                cond_code = (fnmatch(pattern, case_word, /*flags:*/ 0) != 0);
+                               debug_printf_exec("fnmatch(pattern:'%s',str:'%s'):%d\n", pattern, case_word, cond_code);
                                free(pattern);
                                if (cond_code == 0) { /* match! we will execute this branch */
                                        free(case_word);
@@ -8140,12 +8705,13 @@ static int run_list(struct pipe *pi)
                         * I'm NOT treating inner &'s as jobs */
 #if ENABLE_HUSH_JOB
                        if (G.run_list_level == 1)
-                               insert_bg_job(pi);
+                               insert_job_into_table(pi);
 #endif
                        /* Last command's pid goes to $! */
                        G.last_bg_pid = pi->cmds[pi->num_cmds - 1].pid;
+                       G.last_bg_pid_exitcode = 0;
                        debug_printf_exec(": cmd&: exitcode EXIT_SUCCESS\n");
-/* Check pi->pi_inverted? "! sleep 1 & echo $?": bash says 1. dash and ash says 0 */
+/* Check pi->pi_inverted? "! sleep 1 & echo $?": bash says 1. dash and ash say 0 */
                        rcode = EXIT_SUCCESS;
                        goto check_traps;
                } else {
@@ -8165,6 +8731,14 @@ static int run_list(struct pipe *pi)
                        check_and_run_traps();
                }
 
+               /* Handle "set -e" */
+               if (rcode != 0 && G.o_opt[OPT_O_ERREXIT]) {
+                       debug_printf_exec("ERREXIT:1 errexit_depth:%d\n", G.errexit_depth);
+                       if (G.errexit_depth == 0)
+                               hush_exit(rcode);
+               }
+               G.errexit_depth = sv_errexit_depth;
+
                /* Analyze how result affects subsequent commands */
 #if ENABLE_HUSH_IF
                if (rword == RES_IF || rword == RES_ELIF)
@@ -8246,6 +8820,10 @@ static void install_sighandlers(unsigned mask)
                 */
                if (sig == SIGCHLD)
                        continue;
+               /* bash re-enables SIGHUP which is SIG_IGNed on entry.
+                * Try: "trap '' HUP; bash; echo RET" and type "kill -HUP $$"
+                */
+               //if (sig == SIGHUP) continue; - TODO?
                if (old_handler == SIG_IGN) {
                        /* oops... restore back to IGN, and record this fact */
                        install_sighandler(sig, old_handler);
@@ -8344,6 +8922,9 @@ static int set_mode(int state, char mode, const char *o_opt)
                        G.o_opt[idx] = state;
                        break;
                }
+       case 'e':
+               G.o_opt[OPT_O_ERREXIT] = state;
+               break;
        default:
                return EXIT_FAILURE;
        }
@@ -8373,17 +8954,19 @@ int hush_main(int argc, char **argv)
 #if !BB_MMU
        G.argv0_for_re_execing = argv[0];
 #endif
+
        /* Deal with HUSH_VERSION */
+       debug_printf_env("unsetenv '%s'\n", "HUSH_VERSION");
+       unsetenv("HUSH_VERSION"); /* in case it exists in initial env */
        shell_ver = xzalloc(sizeof(*shell_ver));
        shell_ver->flg_export = 1;
        shell_ver->flg_read_only = 1;
        /* Code which handles ${var<op>...} needs writable values for all variables,
         * therefore we xstrdup: */
        shell_ver->varstr = xstrdup(hush_version_str);
+
        /* Create shell local variables from the values
         * currently living in the environment */
-       debug_printf_env("unsetenv '%s'\n", "HUSH_VERSION");
-       unsetenv("HUSH_VERSION"); /* in case it exists in initial env */
        G.top_var = shell_ver;
        cur_var = G.top_var;
        e = environ;
@@ -8403,9 +8986,9 @@ int hush_main(int argc, char **argv)
        putenv(shell_ver->varstr);
 
        /* Export PWD */
-       set_pwd_var(/*exp:*/ 1);
+       set_pwd_var(SETFLAG_EXPORT);
 
-#if ENABLE_HUSH_BASH_COMPAT
+#if BASH_HOSTNAME_VAR
        /* Set (but not export) HOSTNAME unless already set */
        if (!get_local_var_value("HOSTNAME")) {
                struct utsname uts;
@@ -8449,6 +9032,14 @@ int hush_main(int argc, char **argv)
         */
 #endif
 
+#if BASH_LINENO_VAR
+       if (BASH_LINENO_VAR) {
+               char *p = xasprintf("LINENO=%*s", (int)(sizeof(int)*3), "");
+               set_local_var(p, /*flags*/ 0);
+               G.lineno_var = p; /* can't assign before set_local_var("LINENO=...") */
+       }
+#endif
+
 #if ENABLE_FEATURE_EDITING
        G.line_input_state = new_line_input_t(FOR_SHELL);
 #endif
@@ -8470,7 +9061,7 @@ int hush_main(int argc, char **argv)
        flags = (argv[0] && argv[0][0] == '-') ? OPT_login : 0;
        builtin_argc = 0;
        while (1) {
-               opt = getopt(argc, argv, "+c:xinsl"
+               opt = getopt(argc, argv, "+c:exinsl"
 #if !BB_MMU
                                "<:$:R:V:"
 # if ENABLE_HUSH_FUNCTIONS
@@ -8553,8 +9144,9 @@ int hush_main(int argc, char **argv)
                        optarg++;
                        empty_trap_mask = bb_strtoull(optarg, &optarg, 16);
                        if (empty_trap_mask != 0) {
-                               int sig;
+                               IF_HUSH_TRAP(int sig;)
                                install_special_sighandlers();
+# if ENABLE_HUSH_TRAP
                                G_traps = xzalloc(sizeof(G_traps[0]) * NSIG);
                                for (sig = 1; sig < NSIG; sig++) {
                                        if (empty_trap_mask & (1LL << sig)) {
@@ -8562,6 +9154,7 @@ int hush_main(int argc, char **argv)
                                                install_sighandler(sig, SIG_IGN);
                                        }
                                }
+# endif
                        }
 # if ENABLE_HUSH_LOOPS
                        optarg++;
@@ -8571,7 +9164,7 @@ int hush_main(int argc, char **argv)
                }
                case 'R':
                case 'V':
-                       set_local_var(xstrdup(optarg), /*exp:*/ 0, /*lvl:*/ 0, /*ro:*/ opt == 'R');
+                       set_local_var(xstrdup(optarg), opt == 'R' ? SETFLAG_MAKE_RO : 0);
                        break;
 # if ENABLE_HUSH_FUNCTIONS
                case 'F': {
@@ -8586,6 +9179,7 @@ int hush_main(int argc, char **argv)
 #endif
                case 'n':
                case 'x':
+               case 'e':
                        if (set_mode(1, opt, NULL) == 0) /* no error */
                                break;
                default:
@@ -8672,7 +9266,7 @@ int hush_main(int argc, char **argv)
                        G_saved_tty_pgrp = 0;
 
                /* try to dup stdin to high fd#, >= 255 */
-               G_interactive_fd = fcntl(STDIN_FILENO, F_DUPFD, 255);
+               G_interactive_fd = fcntl_F_DUPFD(STDIN_FILENO, 254);
                if (G_interactive_fd < 0) {
                        /* try to dup to any fd */
                        G_interactive_fd = dup(STDIN_FILENO);
@@ -8745,7 +9339,7 @@ int hush_main(int argc, char **argv)
 #elif ENABLE_HUSH_INTERACTIVE
        /* No job control compiled in, only prompt/line editing */
        if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO)) {
-               G_interactive_fd = fcntl(STDIN_FILENO, F_DUPFD, 255);
+               G_interactive_fd = fcntl_F_DUPFD(STDIN_FILENO, 254);
                if (G_interactive_fd < 0) {
                        /* try to dup to any fd */
                        G_interactive_fd = dup(STDIN_FILENO);
@@ -8784,16 +9378,6 @@ int hush_main(int argc, char **argv)
 }
 
 
-#if ENABLE_MSH
-int msh_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
-int msh_main(int argc, char **argv)
-{
-       bb_error_msg("msh is deprecated, please use hush instead");
-       return hush_main(argc, argv);
-}
-#endif
-
-
 /*
  * Built-ins
  */
@@ -8805,15 +9389,11 @@ static int FAST_FUNC builtin_true(char **argv UNUSED_PARAM)
 #if ENABLE_HUSH_TEST || ENABLE_HUSH_ECHO || ENABLE_HUSH_PRINTF || ENABLE_HUSH_KILL
 static int run_applet_main(char **argv, int (*applet_main_func)(int argc, char **argv))
 {
-       int argc = 0;
-       while (*argv) {
-               argc++;
-               argv++;
-       }
-       return applet_main_func(argc, argv - argc);
+       int argc = string_array_len(argv);
+       return applet_main_func(argc, argv);
 }
 #endif
-#if ENABLE_HUSH_TEST
+#if ENABLE_HUSH_TEST || BASH_TEST2
 static int FAST_FUNC builtin_test(char **argv)
 {
        return run_applet_main(argv, test_main);
@@ -8832,6 +9412,30 @@ static int FAST_FUNC builtin_printf(char **argv)
 }
 #endif
 
+#if ENABLE_HUSH_HELP
+static int FAST_FUNC builtin_help(char **argv UNUSED_PARAM)
+{
+       const struct built_in_command *x;
+
+       printf(
+               "Built-in commands:\n"
+               "------------------\n");
+       for (x = bltins1; x != &bltins1[ARRAY_SIZE(bltins1)]; x++) {
+               if (x->b_descr)
+                       printf("%-10s%s\n", x->b_cmd, x->b_descr);
+       }
+       return EXIT_SUCCESS;
+}
+#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
+
 static char **skip_dash_dash(char **argv)
 {
        argv++;
@@ -8840,24 +9444,6 @@ static char **skip_dash_dash(char **argv)
        return argv;
 }
 
-static int FAST_FUNC builtin_eval(char **argv)
-{
-       int rcode = EXIT_SUCCESS;
-
-       argv = skip_dash_dash(argv);
-       if (*argv) {
-               char *str = expand_strvec_to_string(argv);
-               /* bash:
-                * eval "echo Hi; done" ("done" is syntax error):
-                * "echo Hi" will not execute too.
-                */
-               parse_and_run_string(str);
-               free(str);
-               rcode = G.last_exitcode;
-       }
-       return rcode;
-}
-
 static int FAST_FUNC builtin_cd(char **argv)
 {
        const char *newdir;
@@ -8881,10 +9467,55 @@ static int FAST_FUNC builtin_cd(char **argv)
         * Note: do not enforce exporting. If PWD was unset or unexported,
         * set it again, but do not export. bash does the same.
         */
-       set_pwd_var(/*exp:*/ 0);
+       set_pwd_var(/*flag:*/ 0);
+       return EXIT_SUCCESS;
+}
+
+static int FAST_FUNC builtin_pwd(char **argv UNUSED_PARAM)
+{
+       puts(get_cwd(0));
        return EXIT_SUCCESS;
 }
 
+static int FAST_FUNC builtin_eval(char **argv)
+{
+       int rcode = EXIT_SUCCESS;
+
+       argv = skip_dash_dash(argv);
+       if (argv[0]) {
+               char *str = NULL;
+
+               if (argv[1]) {
+                       /* "The eval utility shall construct a command by
+                        * concatenating arguments together, separating
+                        * each with a <space> character."
+                        */
+                       char *p;
+                       unsigned len = 0;
+                       char **pp = argv;
+                       do
+                               len += strlen(*pp) + 1;
+                       while (*++pp);
+                       str = p = xmalloc(len);
+                       pp = argv;
+                       do {
+                               p = stpcpy(p, *pp);
+                               *p++ = ' ';
+                       } while (*++pp);
+                       p[-1] = '\0';
+               }
+
+               /* bash:
+                * eval "echo Hi; done" ("done" is syntax error):
+                * "echo Hi" will not execute too.
+                */
+               parse_and_run_string(str ? str : argv[0]);
+               free(str);
+               rcode = G.last_exitcode;
+       }
+       return rcode;
+}
+
 static int FAST_FUNC builtin_exec(char **argv)
 {
        argv = skip_dash_dash(argv);
@@ -8896,6 +9527,14 @@ static int FAST_FUNC builtin_exec(char **argv)
        if (G_saved_tty_pgrp && getpid() == G.root_pid)
                tcsetpgrp(G_interactive_fd, G_saved_tty_pgrp);
 
+       /* Saved-redirect fds, script fds and G_interactive_fd are still
+        * open here. However, they are all CLOEXEC, and execv below
+        * closes them. Try interactive "exec ls -l /proc/self/fd",
+        * it should show no extra open fds in the "ls" process.
+        * If we'd try to run builtins/NOEXECs, this would need improving.
+        */
+       //close_saved_fds_and_FILE_fds();
+
        /* TODO: if exec fails, bash does NOT exit! We do.
         * We'll need to undo trap cleanup (it's inside execvp_or_die)
         * and tcsetpgrp, and this is inherently racy.
@@ -8930,86 +9569,251 @@ static int FAST_FUNC builtin_exit(char **argv)
        hush_exit(xatoi(argv[0]) & 0xff);
 }
 
-#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_TRAP
-static void print_escaped(const char *s)
+#if ENABLE_HUSH_TYPE
+/* http://www.opengroup.org/onlinepubs/9699919799/utilities/type.html */
+static int FAST_FUNC builtin_type(char **argv)
 {
-       if (*s == '\'')
-               goto squote;
-       do {
-               const char *p = strchrnul(s, '\'');
-               /* print 'xxxx', possibly just '' */
-               printf("'%.*s'", (int)(p - s), s);
-               if (*p == '\0')
-                       break;
-               s = p;
- squote:
-               /* s points to '; print "'''...'''" */
-               putchar('"');
-               do putchar('\''); while (*++s == '\'');
-               putchar('"');
-       } while (*s);
-}
-#endif
+       int ret = EXIT_SUCCESS;
 
-#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_LOCAL
-# if !ENABLE_HUSH_LOCAL
-#define helper_export_local(argv, exp, lvl) \
-       helper_export_local(argv, exp)
-# endif
-static void helper_export_local(char **argv, int exp, int lvl)
-{
-       do {
-               char *name = *argv;
-               char *name_end = strchrnul(name, '=');
+       while (*++argv) {
+               const char *type;
+               char *path = NULL;
 
-               /* So far we do not check that name is valid (TODO?) */
+               if (0) {} /* make conditional compile easier below */
+               /*else if (find_alias(*argv))
+                       type = "an alias";*/
+#if ENABLE_HUSH_FUNCTIONS
+               else if (find_function(*argv))
+                       type = "a function";
+#endif
+               else if (find_builtin(*argv))
+                       type = "a shell builtin";
+               else if ((path = find_in_path(*argv)) != NULL)
+                       type = path;
+               else {
+                       bb_error_msg("type: %s: not found", *argv);
+                       ret = EXIT_FAILURE;
+                       continue;
+               }
 
-               if (*name_end == '\0') {
-                       struct variable *var, **vpp;
+               printf("%s is %s\n", *argv, type);
+               free(path);
+       }
 
-                       vpp = get_ptr_to_local_var(name, name_end - name);
-                       var = vpp ? *vpp : NULL;
+       return ret;
+}
+#endif
 
-                       if (exp == -1) { /* unexporting? */
-                               /* export -n NAME (without =VALUE) */
-                               if (var) {
-                                       var->flg_export = 0;
-                                       debug_printf_env("%s: unsetenv '%s'\n", __func__, name);
-                                       unsetenv(name);
-                               } /* else: export -n NOT_EXISTING_VAR: no-op */
-                               continue;
-                       }
-                       if (exp == 1) { /* exporting? */
-                               /* export NAME (without =VALUE) */
-                               if (var) {
-                                       var->flg_export = 1;
-                                       debug_printf_env("%s: putenv '%s'\n", __func__, var->varstr);
-                                       putenv(var->varstr);
-                                       continue;
-                               }
-                       }
-# if ENABLE_HUSH_LOCAL
-                       if (exp == 0 /* local? */
-                        && var && var->func_nest_level == lvl
-                       ) {
-                               /* "local x=abc; ...; local x" - ignore second local decl */
+#if ENABLE_HUSH_READ
+/* 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;
+ * SIGCHLD from children:
+ * - does not interrupt read regardless of interactivity:
+ *   try: sleep 1 & read x; echo $x
+ */
+static int FAST_FUNC builtin_read(char **argv)
+{
+       const char *r;
+       char *opt_n = NULL;
+       char *opt_p = NULL;
+       char *opt_t = NULL;
+       char *opt_u = NULL;
+       char *opt_d = NULL; /* optimized out if !BASH */
+       const char *ifs;
+       int read_flags;
+
+       /* "!": do not abort on errors.
+        * Option string must start with "sr" to match BUILTIN_READ_xxx
+        */
+       read_flags = getopt32(argv,
+#if BASH_READ_D
+               "!srn:p:t:u:d:", &opt_n, &opt_p, &opt_t, &opt_u, &opt_d
+#else
+               "!srn:p:t:u:", &opt_n, &opt_p, &opt_t, &opt_u
+#endif
+       );
+       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,
+               ifs,
+               read_flags,
+               opt_n,
+               opt_p,
+               opt_t,
+               opt_u,
+               opt_d
+       );
+
+       if ((uintptr_t)r == 1 && errno == EINTR) {
+               unsigned sig = check_and_run_traps();
+               if (sig != SIGINT)
+                       goto again;
+       }
+
+       if ((uintptr_t)r > 1) {
+               bb_error_msg("%s", r);
+               r = (char*)(uintptr_t)1;
+       }
+
+       return (uintptr_t)r;
+}
+#endif
+
+#if ENABLE_HUSH_UMASK
+static int FAST_FUNC builtin_umask(char **argv)
+{
+       int rc;
+       mode_t mask;
+
+       rc = 1;
+       mask = umask(0);
+       argv = skip_dash_dash(argv);
+       if (argv[0]) {
+               mode_t old_mask = mask;
+
+               /* numeric umasks are taken as-is */
+               /* symbolic umasks are inverted: "umask a=rx" calls umask(222) */
+               if (!isdigit(argv[0][0]))
+                       mask ^= 0777;
+               mask = bb_parse_mode(argv[0], mask);
+               if (!isdigit(argv[0][0]))
+                       mask ^= 0777;
+               if ((unsigned)mask > 0777) {
+                       mask = old_mask;
+                       /* bash messages:
+                        * bash: umask: 'q': invalid symbolic mode operator
+                        * bash: umask: 999: octal number out of range
+                        */
+                       bb_error_msg("%s: invalid mode '%s'", "umask", argv[0]);
+                       rc = 0;
+               }
+       } else {
+               /* Mimic bash */
+               printf("%04o\n", (unsigned) mask);
+               /* fall through and restore mask which we set to 0 */
+       }
+       umask(mask);
+
+       return !rc; /* rc != 0 - success */
+}
+#endif
+
+#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_TRAP
+static void print_escaped(const char *s)
+{
+       if (*s == '\'')
+               goto squote;
+       do {
+               const char *p = strchrnul(s, '\'');
+               /* print 'xxxx', possibly just '' */
+               printf("'%.*s'", (int)(p - s), s);
+               if (*p == '\0')
+                       break;
+               s = p;
+ squote:
+               /* s points to '; print "'''...'''" */
+               putchar('"');
+               do putchar('\''); while (*++s == '\'');
+               putchar('"');
+       } while (*s);
+}
+#endif
+
+#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_LOCAL || ENABLE_HUSH_READONLY
+static int helper_export_local(char **argv, unsigned flags)
+{
+       do {
+               char *name = *argv;
+               char *name_end = strchrnul(name, '=');
+
+               /* So far we do not check that name is valid (TODO?) */
+
+               if (*name_end == '\0') {
+                       struct variable *var, **vpp;
+
+                       vpp = get_ptr_to_local_var(name, name_end - name);
+                       var = vpp ? *vpp : NULL;
+
+                       if (flags & SETFLAG_UNEXPORT) {
+                               /* export -n NAME (without =VALUE) */
+                               if (var) {
+                                       var->flg_export = 0;
+                                       debug_printf_env("%s: unsetenv '%s'\n", __func__, name);
+                                       unsetenv(name);
+                               } /* else: export -n NOT_EXISTING_VAR: no-op */
                                continue;
                        }
+                       if (flags & SETFLAG_EXPORT) {
+                               /* export NAME (without =VALUE) */
+                               if (var) {
+                                       var->flg_export = 1;
+                                       debug_printf_env("%s: putenv '%s'\n", __func__, var->varstr);
+                                       putenv(var->varstr);
+                                       continue;
+                               }
+                       }
+                       if (flags & SETFLAG_MAKE_RO) {
+                               /* readonly NAME (without =VALUE) */
+                               if (var) {
+                                       var->flg_read_only = 1;
+                                       continue;
+                               }
+                       }
+# if ENABLE_HUSH_LOCAL
+                       /* Is this "local" bltin? */
+                       if (!(flags & (SETFLAG_EXPORT|SETFLAG_UNEXPORT|SETFLAG_MAKE_RO))) {
+                               unsigned lvl = flags >> SETFLAG_LOCAL_SHIFT;
+                               if (var && var->func_nest_level == lvl) {
+                                       /* "local x=abc; ...; local x" - ignore second local decl */
+                                       continue;
+                               }
+                       }
 # endif
                        /* Exporting non-existing variable.
                         * bash does not put it in environment,
                         * but remembers that it is exported,
                         * and does put it in env when it is set later.
-                        * We just set it to "" and export. */
+                        * We just set it to "" and export.
+                        */
                        /* Or, it's "local NAME" (without =VALUE).
-                        * bash sets the value to "". */
+                        * bash sets the value to "".
+                        */
+                       /* Or, it's "readonly NAME" (without =VALUE).
+                        * bash remembers NAME and disallows its creation
+                        * in the future.
+                        */
                        name = xasprintf("%s=", name);
                } else {
                        /* (Un)exporting/making local NAME=VALUE */
                        name = xstrdup(name);
                }
-               set_local_var(name, /*exp:*/ exp, /*lvl:*/ lvl, /*ro:*/ 0);
+               if (set_local_var(name, flags))
+                       return EXIT_FAILURE;
        } while (*++argv);
+       return EXIT_SUCCESS;
 }
 #endif
 
@@ -9055,9 +9859,7 @@ static int FAST_FUNC builtin_export(char **argv)
                return EXIT_SUCCESS;
        }
 
-       helper_export_local(argv, (opt_unexport ? -1 : 1), 0);
-
-       return EXIT_SUCCESS;
+       return helper_export_local(argv, opt_unexport ? SETFLAG_UNEXPORT : SETFLAG_EXPORT);
 }
 #endif
 
@@ -9068,8 +9870,29 @@ static int FAST_FUNC builtin_local(char **argv)
                bb_error_msg("%s: not in a function", argv[0]);
                return EXIT_FAILURE; /* bash compat */
        }
-       helper_export_local(argv, 0, G.func_nest_level);
-       return EXIT_SUCCESS;
+       argv++;
+       return helper_export_local(argv, G.func_nest_level << SETFLAG_LOCAL_SHIFT);
+}
+#endif
+
+#if ENABLE_HUSH_READONLY
+static int FAST_FUNC builtin_readonly(char **argv)
+{
+       argv++;
+       if (*argv == NULL) {
+               /* bash: readonly [-p]: list all readonly VARs
+                * (-p has no effect in bash)
+                */
+               struct variable *e;
+               for (e = G.top_var; e; e = e->next) {
+                       if (e->flg_read_only) {
+//TODO: quote value: readonly VAR='VAL'
+                               printf("readonly %s\n", e->varstr);
+                       }
+               }
+               return EXIT_SUCCESS;
+       }
+       return helper_export_local(argv, SETFLAG_MAKE_RO);
 }
 #endif
 
@@ -9183,16 +10006,13 @@ static int FAST_FUNC builtin_set(char **argv)
        /* This realloc's G.global_argv */
        G.global_argv = pp = add_strings_to_strings(g_argv, argv, /*dup:*/ 1);
 
-       n = 1;
-       while (*++pp)
-               n++;
-       G.global_argc = n;
+       G.global_argc = 1 + string_array_len(pp + 1);
 
        return EXIT_SUCCESS;
 
        /* Nothing known, so abort */
  error:
-       bb_error_msg("set: %s: invalid option", arg);
+       bb_error_msg("%s: %s: invalid option", "set", arg);
        return EXIT_FAILURE;
 }
 #endif
@@ -9202,7 +10022,18 @@ static int FAST_FUNC builtin_shift(char **argv)
        int n = 1;
        argv = skip_dash_dash(argv);
        if (argv[0]) {
-               n = atoi(argv[0]);
+               n = bb_strtou(argv[0], NULL, 10);
+               if (errno || n < 0) {
+                       /* shared string with ash.c */
+                       bb_error_msg("Illegal number: %s", argv[0]);
+                       /*
+                        * ash aborts in this case.
+                        * bash prints error message and set $? to 1.
+                        * Interestingly, for "shift 99999" bash does not
+                        * print error message, but does set $? to 1
+                        * (and does no shifting at all).
+                        */
+               }
        }
        if (n >= 0 && n < G.global_argc) {
                if (G_global_args_malloced) {
@@ -9218,73 +10049,186 @@ static int FAST_FUNC builtin_shift(char **argv)
        return EXIT_FAILURE;
 }
 
-#if ENABLE_HUSH_READ
-/* 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)
+#if ENABLE_HUSH_GETOPTS
+static int FAST_FUNC builtin_getopts(char **argv)
 {
-       const char *r;
-       char *opt_n = NULL;
-       char *opt_p = NULL;
-       char *opt_t = NULL;
-       char *opt_u = NULL;
-       const char *ifs;
-       int read_flags;
+/* http://pubs.opengroup.org/onlinepubs/9699919799/utilities/getopts.html
 
-       /* "!": do not abort on errors.
-        * Option string must start with "sr" to match BUILTIN_READ_xxx
-        */
-       read_flags = getopt32(argv, "!srn:p:t:u:", &opt_n, &opt_p, &opt_t, &opt_u);
-       if (read_flags == (uint32_t)-1)
-               return EXIT_FAILURE;
-       argv += optind;
-       ifs = get_local_var_value("IFS"); /* can be NULL */
+TODO:
+If a required argument is not found, and getopts is not silent,
+a question mark (?) is placed in VAR, OPTARG is unset, and a
+diagnostic message is printed.  If getopts is silent, then a
+colon (:) is placed in VAR and OPTARG is set to the option
+character found.
 
- again:
-       r = shell_builtin_read(set_local_var_from_halves,
-               argv,
-               ifs,
-               read_flags,
-               opt_n,
-               opt_p,
-               opt_t,
-               opt_u
-       );
+Test that VAR is a valid variable name?
 
-       if ((uintptr_t)r == 1 && errno == EINTR) {
-               unsigned sig = check_and_run_traps();
-               if (sig && sig != SIGINT)
-                       goto again;
+"Whenever the shell is invoked, OPTIND shall be initialized to 1"
+*/
+       char cbuf[2];
+       const char *cp, *optstring, *var;
+       int c, n, exitcode, my_opterr;
+       unsigned count;
+
+       optstring = *++argv;
+       if (!optstring || !(var = *++argv)) {
+               bb_error_msg("usage: getopts OPTSTRING VAR [ARGS]");
+               return EXIT_FAILURE;
        }
 
-       if ((uintptr_t)r > 1) {
-               bb_error_msg("%s", r);
-               r = (char*)(uintptr_t)1;
+       if (argv[1])
+               argv[0] = G.global_argv[0]; /* for error messages in getopt() */
+       else
+               argv = G.global_argv;
+       cbuf[1] = '\0';
+
+       my_opterr = 0;
+       if (optstring[0] != ':') {
+               cp = get_local_var_value("OPTERR");
+               /* 0 if "OPTERR=0", 1 otherwise */
+               my_opterr = (!cp || NOT_LONE_CHAR(cp, '0'));
+       }
+
+       /* getopts stops on first non-option. Add "+" to force that */
+       /*if (optstring[0] != '+')*/ {
+               char *s = alloca(strlen(optstring) + 2);
+               sprintf(s, "+%s", optstring);
+               optstring = s;
+       }
+
+       /* Naively, now we should just
+        *      cp = get_local_var_value("OPTIND");
+        *      optind = cp ? atoi(cp) : 0;
+        *      optarg = NULL;
+        *      opterr = my_opterr;
+        *      c = getopt(string_array_len(argv), argv, optstring);
+        * and be done? Not so fast...
+        * Unlike normal getopt() usage in C programs, here
+        * each successive call will (usually) have the same argv[] CONTENTS,
+        * but not the ADDRESSES. Worse yet, it's possible that between
+        * invocations of "getopts", there will be calls to shell builtins
+        * which use getopt() internally. Example:
+        *      while getopts "abc" RES -a -bc -abc de; do
+        *              unset -ff func
+        *      done
+        * This would not work correctly: getopt() call inside "unset"
+        * modifies internal libc state which is tracking position in
+        * multi-option strings ("-abc"). At best, it can skip options
+        * or return the same option infinitely. With glibc implementation
+        * of getopt(), it would use outright invalid pointers and return
+        * garbage even _without_ "unset" mangling internal state.
+        *
+        * We resort to resetting getopt() state and calling it N times,
+        * until we get Nth result (or failure).
+        * (N == G.getopt_count is reset to 0 whenever OPTIND is [un]set).
+        */
+       GETOPT_RESET();
+       count = 0;
+       n = string_array_len(argv);
+       do {
+               optarg = NULL;
+               opterr = (count < G.getopt_count) ? 0 : my_opterr;
+               c = getopt(n, argv, optstring);
+               if (c < 0)
+                       break;
+               count++;
+       } while (count <= G.getopt_count);
+
+       /* Set OPTIND. Prevent resetting of the magic counter! */
+       set_local_var_from_halves("OPTIND", utoa(optind));
+       G.getopt_count = count; /* "next time, give me N+1'th result" */
+       GETOPT_RESET(); /* just in case */
+
+       /* Set OPTARG */
+       /* Always set or unset, never left as-is, even on exit/error:
+        * "If no option was found, or if the option that was found
+        * does not have an option-argument, OPTARG shall be unset."
+        */
+       cp = optarg;
+       if (c == '?') {
+               /* If ":optstring" and unknown option is seen,
+                * it is stored to OPTARG.
+                */
+               if (optstring[1] == ':') {
+                       cbuf[0] = optopt;
+                       cp = cbuf;
+               }
        }
+       if (cp)
+               set_local_var_from_halves("OPTARG", cp);
+       else
+               unset_local_var("OPTARG");
 
-       return (uintptr_t)r;
+       /* Convert -1 to "?" */
+       exitcode = EXIT_SUCCESS;
+       if (c < 0) { /* -1: end of options */
+               exitcode = EXIT_FAILURE;
+               c = '?';
+       }
+
+       /* Set VAR */
+       cbuf[0] = c;
+       set_local_var_from_halves(var, cbuf);
+
+       return exitcode;
 }
 #endif
 
+static int FAST_FUNC builtin_source(char **argv)
+{
+       char *arg_path, *filename;
+       FILE *input;
+       save_arg_t sv;
+       char *args_need_save;
+#if ENABLE_HUSH_FUNCTIONS
+       smallint sv_flg;
+#endif
+
+       argv = skip_dash_dash(argv);
+       filename = argv[0];
+       if (!filename) {
+               /* bash says: "bash: .: filename argument required" */
+               return 2; /* bash compat */
+       }
+       arg_path = NULL;
+       if (!strchr(filename, '/')) {
+               arg_path = find_in_path(filename);
+               if (arg_path)
+                       filename = arg_path;
+       }
+       input = remember_FILE(fopen_or_warn(filename, "r"));
+       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;
+       }
+
+#if ENABLE_HUSH_FUNCTIONS
+       sv_flg = G_flag_return_in_progress;
+       /* "we are inside sourced file, ok to use return" */
+       G_flag_return_in_progress = -1;
+#endif
+       args_need_save = argv[1]; /* used as a boolean variable */
+       if (args_need_save)
+               save_and_replace_G_args(&sv, argv);
+
+       /* "false; . ./empty_line; echo Zero:$?" should print 0 */
+       G.last_exitcode = 0;
+       parse_and_run_file(input);
+       fclose_and_forget(input);
+
+       if (args_need_save) /* can't use argv[1] instead: "shift" can mangle it */
+               restore_G_args(&sv, argv);
+#if ENABLE_HUSH_FUNCTIONS
+       G_flag_return_in_progress = sv_flg;
+#endif
+
+       return G.last_exitcode;
+}
+
 #if ENABLE_HUSH_TRAP
 static int FAST_FUNC builtin_trap(char **argv)
 {
@@ -9324,10 +10268,10 @@ static int FAST_FUNC builtin_trap(char **argv)
                        sighandler_t handler;
 
                        sig = get_signum(*argv++);
-                       if (sig < 0 || sig >= NSIG) {
+                       if (sig < 0) {
                                ret = EXIT_FAILURE;
                                /* Mimic bash message exactly */
-                               bb_perror_msg("trap: %s: invalid signal specification", argv[-1]);
+                               bb_error_msg("trap: %s: invalid signal specification", argv[-1]);
                                continue;
                        }
 
@@ -9377,41 +10321,6 @@ static int FAST_FUNC builtin_trap(char **argv)
 }
 #endif
 
-#if ENABLE_HUSH_TYPE
-/* http://www.opengroup.org/onlinepubs/9699919799/utilities/type.html */
-static int FAST_FUNC builtin_type(char **argv)
-{
-       int ret = EXIT_SUCCESS;
-
-       while (*++argv) {
-               const char *type;
-               char *path = NULL;
-
-               if (0) {} /* make conditional compile easier below */
-               /*else if (find_alias(*argv))
-                       type = "an alias";*/
-#if ENABLE_HUSH_FUNCTIONS
-               else if (find_function(*argv))
-                       type = "a function";
-#endif
-               else if (find_builtin(*argv))
-                       type = "a shell builtin";
-               else if ((path = find_in_path(*argv)) != NULL)
-                       type = path;
-               else {
-                       bb_error_msg("type: %s: not found", *argv);
-                       ret = EXIT_FAILURE;
-                       continue;
-               }
-
-               printf("%s is %s\n", *argv, type);
-               free(path);
-       }
-
-       return ret;
-}
-#endif
-
 #if ENABLE_HUSH_JOB
 static struct pipe *parse_jobspec(const char *str)
 {
@@ -9437,8 +10346,28 @@ static struct pipe *parse_jobspec(const char *str)
                        return pi;
                }
        }
-       bb_error_msg("%u: no such job", jobnum);
-       return NULL;
+       bb_error_msg("%u: no such job", jobnum);
+       return NULL;
+}
+
+static int FAST_FUNC builtin_jobs(char **argv UNUSED_PARAM)
+{
+       struct pipe *job;
+       const char *status_string;
+
+       checkjobs(NULL, 0 /*(no pid to wait for)*/);
+       for (job = G.job_list; job; job = job->next) {
+               if (job->alive_cmds == job->stopped_cmds)
+                       status_string = "Stopped";
+               else
+                       status_string = "Running";
+
+               printf(JOB_STATUS_FORMAT, job->jobid, status_string, job->cmdtext);
+       }
+
+       clean_up_last_dead_job();
+
+       return EXIT_SUCCESS;
 }
 
 /* built-in 'fg' and 'bg' handler */
@@ -9482,206 +10411,20 @@ static int FAST_FUNC builtin_fg_bg(char **argv)
        i = kill(- pi->pgrp, SIGCONT);
        if (i < 0) {
                if (errno == ESRCH) {
-                       delete_finished_bg_job(pi);
+                       delete_finished_job(pi);
                        return EXIT_SUCCESS;
                }
                bb_perror_msg("kill (SIGCONT)");
        }
 
        if (argv[0][0] == 'f') {
-               remove_bg_job(pi);
+               remove_job_from_table(pi); /* FG job shouldn't be in job table */
                return checkjobs_and_fg_shell(pi);
        }
        return EXIT_SUCCESS;
 }
 #endif
 
-#if ENABLE_HUSH_HELP
-static int FAST_FUNC builtin_help(char **argv UNUSED_PARAM)
-{
-       const struct built_in_command *x;
-
-       printf(
-               "Built-in commands:\n"
-               "------------------\n");
-       for (x = bltins1; x != &bltins1[ARRAY_SIZE(bltins1)]; x++) {
-               if (x->b_descr)
-                       printf("%-10s%s\n", x->b_cmd, x->b_descr);
-       }
-       return EXIT_SUCCESS;
-}
-#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)
-{
-       struct pipe *job;
-       const char *status_string;
-
-       checkjobs(NULL, 0 /*(no pid to wait for)*/);
-       for (job = G.job_list; job; job = job->next) {
-               if (job->alive_cmds == job->stopped_cmds)
-                       status_string = "Stopped";
-               else
-                       status_string = "Running";
-
-               printf(JOB_STATUS_FORMAT, job->jobid, status_string, job->cmdtext);
-       }
-       return EXIT_SUCCESS;
-}
-#endif
-
-#if ENABLE_HUSH_MEMLEAK
-static int FAST_FUNC builtin_memleak(char **argv UNUSED_PARAM)
-{
-       void *p;
-       unsigned long l;
-
-# ifdef M_TRIM_THRESHOLD
-       /* Optional. Reduces probability of false positives */
-       malloc_trim(0);
-# endif
-       /* Crude attempt to find where "free memory" starts,
-        * sans fragmentation. */
-       p = malloc(240);
-       l = (unsigned long)p;
-       free(p);
-       p = malloc(3400);
-       if (l < (unsigned long)p) l = (unsigned long)p;
-       free(p);
-
-
-# if 0  /* debug */
-       {
-               struct mallinfo mi = mallinfo();
-               printf("top alloc:0x%lx malloced:%d+%d=%d\n", l,
-                       mi.arena, mi.hblkhd, mi.arena + mi.hblkhd);
-       }
-# endif
-
-       if (!G.memleak_value)
-               G.memleak_value = l;
-
-       l -= G.memleak_value;
-       if ((long)l < 0)
-               l = 0;
-       l /= 1024;
-       if (l > 127)
-               l = 127;
-
-       /* Exitcode is "how many kilobytes we leaked since 1st call" */
-       return l;
-}
-#endif
-
-static int FAST_FUNC builtin_pwd(char **argv UNUSED_PARAM)
-{
-       puts(get_cwd(0));
-       return EXIT_SUCCESS;
-}
-
-static int FAST_FUNC builtin_source(char **argv)
-{
-       char *arg_path, *filename;
-       FILE *input;
-       save_arg_t sv;
-       char *args_need_save;
-#if ENABLE_HUSH_FUNCTIONS
-       smallint sv_flg;
-#endif
-
-       argv = skip_dash_dash(argv);
-       filename = argv[0];
-       if (!filename) {
-               /* bash says: "bash: .: filename argument required" */
-               return 2; /* bash compat */
-       }
-       arg_path = NULL;
-       if (!strchr(filename, '/')) {
-               arg_path = find_in_path(filename);
-               if (arg_path)
-                       filename = arg_path;
-       }
-       input = remember_FILE(fopen_or_warn(filename, "r"));
-       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;
-       }
-
-#if ENABLE_HUSH_FUNCTIONS
-       sv_flg = G_flag_return_in_progress;
-       /* "we are inside sourced file, ok to use return" */
-       G_flag_return_in_progress = -1;
-#endif
-       args_need_save = argv[1]; /* used as a boolean variable */
-       if (args_need_save)
-               save_and_replace_G_args(&sv, argv);
-
-       /* "false; . ./empty_line; echo Zero:$?" should print 0 */
-       G.last_exitcode = 0;
-       parse_and_run_file(input);
-       fclose_and_forget(input);
-
-       if (args_need_save) /* can't use argv[1] instead: "shift" can mangle it */
-               restore_G_args(&sv, argv);
-#if ENABLE_HUSH_FUNCTIONS
-       G_flag_return_in_progress = sv_flg;
-#endif
-
-       return G.last_exitcode;
-}
-
-#if ENABLE_HUSH_UMASK
-static int FAST_FUNC builtin_umask(char **argv)
-{
-       int rc;
-       mode_t mask;
-
-       rc = 1;
-       mask = umask(0);
-       argv = skip_dash_dash(argv);
-       if (argv[0]) {
-               mode_t old_mask = mask;
-
-               /* numeric umasks are taken as-is */
-               /* symbolic umasks are inverted: "umask a=rx" calls umask(222) */
-               if (!isdigit(argv[0][0]))
-                       mask ^= 0777;
-               mask = bb_parse_mode(argv[0], mask);
-               if (!isdigit(argv[0][0]))
-                       mask ^= 0777;
-               if ((unsigned)mask > 0777) {
-                       mask = old_mask;
-                       /* bash messages:
-                        * bash: umask: 'q': invalid symbolic mode operator
-                        * bash: umask: 999: octal number out of range
-                        */
-                       bb_error_msg("%s: invalid mode '%s'", "umask", argv[0]);
-                       rc = 0;
-               }
-       } else {
-               /* Mimic bash */
-               printf("%04o\n", (unsigned) mask);
-               /* fall through and restore mask which we set to 0 */
-       }
-       umask(mask);
-
-       return !rc; /* rc != 0 - success */
-}
-#endif
-
 #if ENABLE_HUSH_KILL
 static int FAST_FUNC builtin_kill(char **argv)
 {
@@ -9823,6 +10566,7 @@ static int wait_for_child_or_signal(struct pipe *waitfor_pipe, pid_t waitfor_pid
                /* So, did we get a signal? */
                sig = check_and_run_traps();
                if (sig /*&& sig != SIGCHLD - always true */) {
+                       /* Do this for any (non-ignored) signal, not only for ^C */
                        ret = 128 + sig;
                        break;
                }
@@ -9867,8 +10611,12 @@ static int FAST_FUNC builtin_wait(char **argv)
                                wait_pipe = parse_jobspec(*argv);
                                if (wait_pipe) {
                                        ret = job_exited_or_stopped(wait_pipe);
-                                       if (ret < 0)
+                                       if (ret < 0) {
                                                ret = wait_for_child_or_signal(wait_pipe, 0);
+                                       } else {
+                                               /* waiting on "last dead job" removes it */
+                                               clean_up_last_dead_job();
+                                       }
                                }
                                /* else: parse_jobspec() already emitted error msg */
                                continue;
@@ -9884,14 +10632,15 @@ static int FAST_FUNC builtin_wait(char **argv)
                ret = waitpid(pid, &status, WNOHANG);
                if (ret < 0) {
                        /* No */
+                       ret = 127;
                        if (errno == ECHILD) {
-                               if (G.last_bg_pid > 0 && pid == G.last_bg_pid) {
+                               if (pid == G.last_bg_pid) {
                                        /* "wait $!" but last bg task has already exited. Try:
                                         * (sleep 1; exit 3) & sleep 2; echo $?; wait $!; echo $?
                                         * In bash it prints exitcode 0, then 3.
                                         * In dash, it is 127.
                                         */
-                                       /* ret = G.last_bg_pid_exitstatus - FIXME */
+                                       ret = G.last_bg_pid_exitcode;
                                } else {
                                        /* Example: "wait 1". mimic bash message */
                                        bb_error_msg("wait: pid %d is not a child of this shell", (int)pid);
@@ -9900,7 +10649,6 @@ static int FAST_FUNC builtin_wait(char **argv)
                                /* ??? */
                                bb_perror_msg("wait %s", *argv);
                        }
-                       ret = 127;
                        continue; /* bash checks all argv[] */
                }
                if (ret == 0) {
@@ -9984,3 +10732,81 @@ static int FAST_FUNC builtin_return(char **argv)
        return rc;
 }
 #endif
+
+#if ENABLE_HUSH_TIMES
+static int FAST_FUNC builtin_times(char **argv UNUSED_PARAM)
+{
+       static const uint8_t times_tbl[] ALIGN1 = {
+               ' ',  offsetof(struct tms, tms_utime),
+               '\n', offsetof(struct tms, tms_stime),
+               ' ',  offsetof(struct tms, tms_cutime),
+               '\n', offsetof(struct tms, tms_cstime),
+               0
+       };
+       const uint8_t *p;
+       unsigned clk_tck;
+       struct tms buf;
+
+       clk_tck = bb_clk_tck();
+
+       times(&buf);
+       p = times_tbl;
+       do {
+               unsigned sec, frac;
+               unsigned long t;
+               t = *(clock_t *)(((char *) &buf) + p[1]);
+               sec = t / clk_tck;
+               frac = t % clk_tck;
+               printf("%um%u.%03us%c",
+                       sec / 60, sec % 60,
+                       (frac * 1000) / clk_tck,
+                       p[0]);
+               p += 2;
+       } while (*p);
+
+       return EXIT_SUCCESS;
+}
+#endif
+
+#if ENABLE_HUSH_MEMLEAK
+static int FAST_FUNC builtin_memleak(char **argv UNUSED_PARAM)
+{
+       void *p;
+       unsigned long l;
+
+# ifdef M_TRIM_THRESHOLD
+       /* Optional. Reduces probability of false positives */
+       malloc_trim(0);
+# endif
+       /* Crude attempt to find where "free memory" starts,
+        * sans fragmentation. */
+       p = malloc(240);
+       l = (unsigned long)p;
+       free(p);
+       p = malloc(3400);
+       if (l < (unsigned long)p) l = (unsigned long)p;
+       free(p);
+
+
+# if 0  /* debug */
+       {
+               struct mallinfo mi = mallinfo();
+               printf("top alloc:0x%lx malloced:%d+%d=%d\n", l,
+                       mi.arena, mi.hblkhd, mi.arena + mi.hblkhd);
+       }
+# endif
+
+       if (!G.memleak_value)
+               G.memleak_value = l;
+
+       l -= G.memleak_value;
+       if ((long)l < 0)
+               l = 0;
+       l /= 1024;
+       if (l > 127)
+               l = 127;
+
+       /* Exitcode is "how many kilobytes we leaked since 1st call" */
+       return l;
+}
+#endif