hush: add support for "set -e"
authorDenys Vlasenko <vda.linux@googlemail.com>
Fri, 14 Jul 2017 11:36:48 +0000 (13:36 +0200)
committerDenys Vlasenko <vda.linux@googlemail.com>
Fri, 14 Jul 2017 11:36:48 +0000 (13:36 +0200)
function                                             old     new   delta
run_list                                             978    1046     +68
o_opt_strings                                         24      32      +8
reset_traps_to_defaults                              136     142      +6
pick_sighandler                                       57      60      +3
packed_usage                                       31772   31770      -2
hush_main                                            983     961     -22
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 4/2 up/down: 85/-24)             Total: 61 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
shell/hush.c
shell/hush_test/hush-misc/errexit1.right [new file with mode: 0644]
shell/hush_test/hush-misc/errexit1.tests [new file with mode: 0755]
shell/hush_test/hush-signals/signal8.right [new file with mode: 0644]
shell/hush_test/hush-signals/signal8.tests [new file with mode: 0755]
shell/hush_test/hush-signals/signal9.right [new file with mode: 0644]
shell/hush_test/hush-signals/signal9.tests [new file with mode: 0755]

index 89cd47d8fbfe9f462caa21305ab5dd5849ae0525..553c8e64a01341c8dcd673092f2a8636f19db34f 100644 (file)
@@ -49,7 +49,6 @@
  *          [un]alias, command, fc, getopts, newgrp, readonly, times
  *      make complex ${var%...} constructs support optional
  *      make here documents optional
- *      set -e (some ash testsuite entries use it, want to adopt those)
  *
  * Bash compat TODO:
  *      redirection of stdout+stderr: &> and >&
  * 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"
 
@@ -747,6 +746,7 @@ struct function {
 static const char o_opt_strings[] ALIGN1 =
        "pipefail\0"
        "noexec\0"
+       "errexit\0"
 #if ENABLE_HUSH_MODE_X
        "xtrace\0"
 #endif
@@ -754,6 +754,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
@@ -810,6 +811,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])
@@ -5159,7 +5179,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 +5188,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;)
@@ -8021,6 +8041,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;
@@ -8030,6 +8051,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 */
@@ -8243,6 +8271,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)
@@ -8422,22 +8458,9 @@ static int set_mode(int state, char mode, const char *o_opt)
                        G.o_opt[idx] = state;
                        break;
                }
-/* TODO: set -e
-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.
-*/
+       case 'e':
+               G.o_opt[OPT_O_ERREXIT] = state;
+               break;
        default:
                return EXIT_FAILURE;
        }
@@ -8564,7 +8587,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
@@ -8682,6 +8705,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:
diff --git a/shell/hush_test/hush-misc/errexit1.right b/shell/hush_test/hush-misc/errexit1.right
new file mode 100644 (file)
index 0000000..d86bac9
--- /dev/null
@@ -0,0 +1 @@
+OK
diff --git a/shell/hush_test/hush-misc/errexit1.tests b/shell/hush_test/hush-misc/errexit1.tests
new file mode 100755 (executable)
index 0000000..7b4a156
--- /dev/null
@@ -0,0 +1,5 @@
+set -e
+(true)
+echo OK
+(false)
+echo FAIL
diff --git a/shell/hush_test/hush-signals/signal8.right b/shell/hush_test/hush-signals/signal8.right
new file mode 100644 (file)
index 0000000..39572f3
--- /dev/null
@@ -0,0 +1,3 @@
+Removing traps
+End of exit_func
+Done: 0
diff --git a/shell/hush_test/hush-signals/signal8.tests b/shell/hush_test/hush-signals/signal8.tests
new file mode 100755 (executable)
index 0000000..731af74
--- /dev/null
@@ -0,0 +1,18 @@
+"$THIS_SH" -c '
+exit_func() {
+    echo "Removing traps"
+    trap - EXIT TERM INT
+    echo "End of exit_func"
+}
+set -e
+trap exit_func EXIT TERM INT
+sleep 2
+exit 77
+' &
+
+sleep 1
+# BUG: ash kills -PGRP, but in non-interactive shell we do not create pgrps!
+# In this case, bash kills by PID, not PGRP.
+kill -TERM %1
+wait
+echo Done: $?
diff --git a/shell/hush_test/hush-signals/signal9.right b/shell/hush_test/hush-signals/signal9.right
new file mode 100644 (file)
index 0000000..39572f3
--- /dev/null
@@ -0,0 +1,3 @@
+Removing traps
+End of exit_func
+Done: 0
diff --git a/shell/hush_test/hush-signals/signal9.tests b/shell/hush_test/hush-signals/signal9.tests
new file mode 100755 (executable)
index 0000000..18e7101
--- /dev/null
@@ -0,0 +1,21 @@
+# Note: the inner script is a test which checks for a different bug
+# (ordering between INT handler and exit on "set -e"),
+# but so far I did not figure out how to simulate it non-interactively.
+
+"$THIS_SH" -c '
+exit_func() {
+    echo "Removing traps"
+    trap - EXIT TERM INT
+    echo "End of exit_func"
+}
+set -e
+trap exit_func EXIT TERM INT
+sleep 2
+exit 77
+' &
+
+child=$!
+sleep 1
+kill -TERM $child
+wait
+echo Done: $?