hush: implement "readonly" builtin
[oweals/busybox.git] / shell / hush.c
index b131f6095b75dcc2469a712b12b043d64839c2e7..a68986329c9c08dfbe2d387a8d68f35d429377c0 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
  *      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, command, fc, getopts, times:
+ *          command -v CMD: print "/path/to/CMD"
+ *              prints "CMD" for builtins
+ *              prints "alias ALIAS='EXPANSION'" for aliases
+ *              prints nothing and sets $? to 1 if not found
+ *          command -V CMD: print "CMD is /path/CMD|a shell builtin|etc"
+ *          command [-p] CMD: run CMD, even if a function CMD also exists
+ *              (can use this to override standalone shell as well)
+ *              -p: use default $PATH
+ *          command BLTIN: disables special-ness (e.g. errors do not abort)
+ *          getopts: getopt() for shells
+ *          times: print getrusage(SELF/CHILDREN).ru_utime/ru_stime
+ *          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"
 //config:      help
 //config:        export -n unexports variables. It is a bash extension.
 //config:
+//config:config HUSH_READONLY
+//config:      bool "readonly builtin"
+//config:      default y
+//config:      help
+//config:        Enable support for read-only variables.
+//config:
 //config:config HUSH_KILL
 //config:      bool "kill builtin (supports kill %jobspec)"
 //config:      default y
  * 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"
 
@@ -746,6 +771,7 @@ struct function {
 static const char o_opt_strings[] ALIGN1 =
        "pipefail\0"
        "noexec\0"
+       "errexit\0"
 #if ENABLE_HUSH_MODE_X
        "xtrace\0"
 #endif
@@ -753,6 +779,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
@@ -809,6 +836,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])
@@ -923,6 +969,9 @@ 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;
@@ -1041,6 +1090,9 @@ 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"),
 #endif
@@ -2011,19 +2063,10 @@ static const char* FAST_FUNC get_local_var_value(const char *name)
  * -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)
+static int set_local_var(char *str,
+               int flg_export UNUSED_PARAM,
+               int local_lvl UNUSED_PARAM,
+               int flg_read_only UNUSED_PARAM)
 {
        struct variable **var_pp;
        struct variable *cur;
@@ -2047,9 +2090,7 @@ 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);
                        free(str);
                        return -1;
@@ -2117,10 +2158,12 @@ static int set_local_var(char *str, int flg_export, int local_lvl, int flg_read_
 
  set_str_and_exp:
        cur->varstr = str;
-#if !BB_MMU
-       cur->flg_read_only = flg_read_only;
-#endif
  exp:
+#if !BB_MMU || ENABLE_HUSH_READONLY
+       if (flg_read_only != 0) {
+               cur->flg_read_only = flg_read_only;
+       }
+#endif
        if (flg_export == 1)
                cur->flg_export = 1;
        if (name_len == 4 && cur->varstr[0] == 'P' && cur->varstr[1] == 'S')
@@ -3352,12 +3395,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.
@@ -3527,9 +3607,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))) {
@@ -4800,7 +4879,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 */
@@ -5159,7 +5240,7 @@ static struct pipe *parse_stream(char **pstring,
                         * and it will match } earlier (not here). */
                        syntax_error_unexpected_ch(ch);
                        G.last_exitcode = 2;
-                       goto parse_error1;
+                       goto parse_error2;
                default:
                        if (HUSH_DEBUG)
                                bb_error_msg_and_die("BUG: unexpected %c\n", ch);
@@ -5168,7 +5249,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;)
@@ -5644,31 +5725,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) {
-                                       /* 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 || !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 */
+                                       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 /* HUSH_SUBSTR_EXPANSION && FEATURE_SH_MATH */
-                       {
-                               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) */
+                       die_if_script("malformed ${%s:...}", var);
+                       val = NULL;
+#endif
                } else { /* one of "-=+?" */
                        /* Standard-mandated substitution ops:
                         * ${var?word} - indicate error if unset
@@ -7217,24 +7301,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 */
@@ -7248,31 +7362,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)
@@ -7359,7 +7448,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;
                                }
@@ -7397,14 +7486,22 @@ static int process_wait_result(struct pipe *fg_pipe, pid_t childpid, int status)
                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);
-//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.
+                               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 */
@@ -8023,6 +8120,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;
@@ -8032,6 +8130,13 @@ 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_HAS_KEYWORDS(rword == RES_IF || rword == RES_ELIF ||)
+                   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 */
@@ -8219,7 +8324,7 @@ 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;
@@ -8245,6 +8350,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)
@@ -8424,6 +8537,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;
        }
@@ -8550,7 +8666,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
@@ -8668,6 +8784,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:
@@ -9193,12 +9310,11 @@ static void print_escaped(const char *s)
 }
 #endif
 
-#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)
+#if ENABLE_HUSH_EXPORT || ENABLE_HUSH_LOCAL || ENABLE_HUSH_READONLY
+static int helper_export_local(char **argv,
+               int exp UNUSED_PARAM,
+               int ro UNUSED_PARAM,
+               int lvl UNUSED_PARAM)
 {
        do {
                char *name = *argv;
@@ -9231,7 +9347,7 @@ static void helper_export_local(char **argv, int exp, int lvl)
                                }
                        }
 # if ENABLE_HUSH_LOCAL
-                       if (exp == 0 /* local? */
+                       if (exp == 0 && ro == 0 /* local? */
                         && var && var->func_nest_level == lvl
                        ) {
                                /* "local x=abc; ...; local x" - ignore second local decl */
@@ -9242,16 +9358,23 @@ static void helper_export_local(char **argv, int exp, int lvl)
                         * 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);
+               set_local_var(name, /*exp:*/ exp, /*lvl:*/ lvl, /*ro:*/ ro);
        } while (*++argv);
+       return EXIT_SUCCESS;
 }
 #endif
 
@@ -9297,9 +9420,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, /*exp:*/ (opt_unexport ? -1 : 1), /*ro:*/ 0, /*lvl:*/ 0);
 }
 #endif
 
@@ -9310,11 +9431,32 @@ 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, /*exp:*/ 0, /*ro:*/ 0, /*lvl:*/ G.func_nest_level);
+}
+#endif
+
+#if ENABLE_HUSH_READONLY
+static int FAST_FUNC builtin_readonly(char **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, /*exp:*/ 0, /*ro:*/ 1, /*lvl:*/ 0);
 }
 #endif
 
+
 #if ENABLE_HUSH_UNSET
 /* http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#unset */
 static int FAST_FUNC builtin_unset(char **argv)
@@ -9658,6 +9800,9 @@ static int FAST_FUNC builtin_jobs(char **argv UNUSED_PARAM)
 
                printf(JOB_STATUS_FORMAT, job->jobid, status_string, job->cmdtext);
        }
+
+       clean_up_last_dead_job();
+
        return EXIT_SUCCESS;
 }
 
@@ -9702,14 +9847,14 @@ 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;
@@ -9901,17 +10046,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);
-//bash immediately deletes finished jobs from job table only in interactive mode,
-//we _always_ delete them at once. If we'd start doing that, this (and more)
-//would be necessary to avoid accumulating dead jobs:
-# if 0
-                                       else {
-                                               if (!wait_pipe->alive_cmds)
-                                                       delete_finished_bg_job(wait_pipe);
+                                       } else {
+                                               /* waiting on "last dead job" removes it */
+                                               clean_up_last_dead_job();
                                        }
-# endif
                                }
                                /* else: parse_jobspec() already emitted error msg */
                                continue;
@@ -9929,7 +10069,7 @@ static int FAST_FUNC builtin_wait(char **argv)
                        /* 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.