ash: "you disabled math" is wrong: user did not disable it, builder of ash did
[oweals/busybox.git] / shell / hush.c
index 8223cdbc5ac892f5fd5a86ec26094161cd0e104f..c8356f4b8552745349bbe2742ff5994309374b4d 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, readonly, 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
+ *          readonly VAR[=VAL]...: make VARs readonly
+ *          readonly [-p]: list all such VARs (-p has no effect in bash)
+ *          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"
  * 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 +766,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 +774,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 +831,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])
@@ -3352,12 +3393,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.
@@ -4799,7 +4877,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 */
@@ -5158,7 +5238,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);
@@ -5167,7 +5247,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;)
@@ -5643,31 +5723,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
@@ -7216,11 +7299,42 @@ static const char *get_cmdtext(struct pipe *pi)
        return pi->cmdtext;
 }
 
+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;
 
+       clean_up_last_dead_job();
+
        /* Find the end of the list, and find next job ID to use */
        i = 0;
        jobp = &G.job_list;
@@ -7246,30 +7360,6 @@ static void insert_job_into_table(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_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;
-       }
-       if (G.job_list)
-               G.last_jobid = G.job_list->jobid;
-       else
-               G.last_jobid = 0;
-}
-
-static void delete_finished_job(struct pipe *pi)
-{
-       remove_job_from_table(pi);
-       free_pipe(pi);
-}
 #endif /* JOB */
 
 static int job_exited_or_stopped(struct pipe *pi)
@@ -7394,14 +7484,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_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 */
@@ -8020,6 +8118,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;
@@ -8029,6 +8128,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 */
@@ -8242,6 +8348,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)
@@ -8421,6 +8535,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;
        }
@@ -8547,7 +8664,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
@@ -8665,6 +8782,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:
@@ -9655,6 +9773,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;
 }
 
@@ -9898,17 +10019,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 keeping some dead jobs, this
-//(and more) would be necessary to avoid accumulating dead jobs:
-# if 0
-                                       else {
-                                               if (!wait_pipe->alive_cmds)
-                                                       delete_finished_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;
@@ -9926,7 +10042,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.