Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ The ERR_EXIT and ERR_RETURN options were refined to be more self-
consistent and better aligned with the POSIX-2017 specification of
`set -e`:

- The EXIT trap is now correctly executed when ERR_EXIT triggers
inside a function. Previously, `starttrapscope()` hid the EXIT
trap on function entry, and the ERR_EXIT code path called
`realexit()` without restoring it. Example:

setopt ERR_EXIT
trap 'echo exiting' EXIT
f() { false }
f
# "exiting" is printed only since 5.10.

- Function calls or anonymous functions prefixed with `!` now never
trigger exit or return. Negated function calls or anonymous
functions used to trigger exit or return if ERR_EXIT or ERR_RETURN
Expand Down
27 changes: 20 additions & 7 deletions Src/builtin.c
Original file line number Diff line number Diff line change
Expand Up @@ -5823,6 +5823,25 @@ mod_export volatile int exit_pending;
/**/
mod_export volatile int exit_level;

/*
* Set up exit_pending unwind: instead of exiting immediately,
* force all active functions to return so that endtrapscope()
* runs at each level and all saved EXIT traps are executed.
*/

/**/
mod_export void
set_exit_pending(int val)
{
if (trap_state)
trap_state = TRAP_STATE_FORCE_RETURN;
retflag = 1;
breaks = loops;
exit_pending = 1;
exit_level = locallevel;
exit_val = val;
}

/* we have printed a 'you have stopped (running) jobs.' message */

/**/
Expand Down Expand Up @@ -5905,13 +5924,7 @@ bin_break(char *name, char **argv, UNUSED(Options ops), int func)
* a bad job.
*/
if (stopmsg || (zexit(0, ZEXIT_DEFERRED), !stopmsg)) {
if (trap_state)
trap_state = TRAP_STATE_FORCE_RETURN;
retflag = 1;
breaks = loops;
exit_pending = 1;
exit_level = locallevel;
exit_val = num;
set_exit_pending(num);
}
} else
zexit(num, ZEXIT_NORMAL);
Expand Down
22 changes: 16 additions & 6 deletions Src/exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -1616,12 +1616,22 @@ execlist(Estate state, int dont_change_job, int exiting)
!(noerrexit & NOERREXIT_EXIT);
if (errexit) {
errflag = 0;
if (sigtrapped[SIGEXIT])
dotrap(SIGEXIT);
if (mypid != getpid())
_realexit();
else
realexit();
if (locallevel > forklevel) {
/*
* Inside a function: use the same unwind
* mechanism as the exit builtin so that
* endtrapscope() runs at each level and
* all saved EXIT traps are executed.
*/
set_exit_pending(lastval);
} else {
if (sigtrapped[SIGEXIT])
dotrap(SIGEXIT);
if (mypid != getpid())
_realexit();
else
realexit();
}
}
if (errreturn) {
retflag = 1;
Expand Down
63 changes: 44 additions & 19 deletions Src/signals.c
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,36 @@ starttrapscope(void)
* endparamscope() so that the locallevel has been decremented.
*/

/*
* Restore a single saved trap entry: call settrap() to reinstall it,
* update sigtrapped[], and for function-style traps re-add the node
* to shfunctab. Used by endtrapscope().
*/

static void
restore_saved_trap(struct savetrap *st)
{
int sig = st->sig;

dontsavetrap++;
if (st->flags & ZSIG_FUNC)
settrap(sig, NULL, ZSIG_FUNC);
else
settrap(sig, (Eprog) st->list, 0);
if (sig == SIGEXIT)
exit_trap_posix = st->posix;
dontsavetrap--;
/*
* counting of nsigtrapped should presumably be handled
* in settrap...
*/
DPUTS((sigtrapped[sig] ^ st->flags) & ZSIG_TRAPPED,
"BUG: settrap didn't restore correct ZSIG_TRAPPED");
if ((sigtrapped[sig] = st->flags) & ZSIG_FUNC)
shfunctab->addnode(shfunctab, ((Shfunc)st->list)->node.nam,
(Shfunc) st->list);
}

/**/
void
endtrapscope(void)
Expand Down Expand Up @@ -911,24 +941,7 @@ endtrapscope(void)
remnode(savetraps, ln);

if (st->flags && (st->list != NULL)) {
/* prevent settrap from saving this */
dontsavetrap++;
if (st->flags & ZSIG_FUNC)
settrap(sig, NULL, ZSIG_FUNC);
else
settrap(sig, (Eprog) st->list, 0);
if (sig == SIGEXIT)
exit_trap_posix = st->posix;
dontsavetrap--;
/*
* counting of nsigtrapped should presumably be handled
* in settrap...
*/
DPUTS((sigtrapped[sig] ^ st->flags) & ZSIG_TRAPPED,
"BUG: settrap didn't restore correct ZSIG_TRAPPED");
if ((sigtrapped[sig] = st->flags) & ZSIG_FUNC)
shfunctab->addnode(shfunctab, ((Shfunc)st->list)->node.nam,
(Shfunc) st->list);
restore_saved_trap(st);
} else if (sigtrapped[sig]) {
/*
* Don't restore the old state if someone has set a
Expand All @@ -947,7 +960,20 @@ endtrapscope(void)
* We already made sure this wasn't set as a POSIX exit trap.
* We respect the user's intention when the trap in question
* was set.
*
* If we are exiting, clear errflag so the trap can run.
* Matches what zexit() does before calling dotrap().
*
* If errflag is set in a non-interactive shell (which will
* cause exit via the main loop), convert to exit_pending so
* that all nested EXIT traps are properly executed bottom-up.
* This handles NOUNSET errors and any other zerr()-based
* errors that would cause exit.
*/
if (!exit_pending && !interact && (errflag & ERRFLAG_ERROR))
set_exit_pending(lastval ? lastval : 1);
if (exit_pending)
errflag = 0;
dotrapargs(SIGEXIT, &exittr, exitfn);
if (exittr & ZSIG_FUNC)
shfunctab->freenode((HashNode)exitfn);
Expand All @@ -958,7 +984,6 @@ endtrapscope(void)
"BUG: still saved traps outside all function scope");
}


/*
* Decide whether a trap needs handling.
* If so, see if the trap should be run now or queued.
Expand Down
18 changes: 13 additions & 5 deletions Src/subst.c
Original file line number Diff line number Diff line change
Expand Up @@ -3343,15 +3343,23 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int pf_flags,
*/
errflag |= ERRFLAG_HARD;
if (!interact) {
if (mypid == getpid()) {
if (locallevel > forklevel) {
/*
* paranoia: don't check for jobs, but there
* shouldn't be any if not interactive.
* Inside a function: use the same unwind
* mechanism as the exit builtin so that
* endtrapscope() runs at each level and
* all saved EXIT traps are executed.
*/
set_exit_pending(1);
} else {
/*
* Not inside a function: zexit() runs the
* EXIT trap and handles both main process
* and subshell (_exit) cases.
*/
stopmsg = 1;
zexit(1, ZEXIT_NORMAL);
} else
_exit(1);
}
}
}
return NULL;
Expand Down
68 changes: 68 additions & 0 deletions Test/C03traps.ztst
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,74 @@ F:Must be tested with a top-level script rather than source or function
>one
>one

(setopt err_exit; trap 'echo EXIT' EXIT; f() { false; }; f)
1:EXIT trap called when ERR_EXIT triggers inside function
>EXIT

(setopt err_exit
trap 'echo A' EXIT
f1() {
trap 'echo B' EXIT
f2() {
trap 'echo C' EXIT
false
}
f2
}
f1)
1:nested EXIT traps all called when ERR_EXIT triggers inside function
>C
>B
>A

(trap 'echo EXIT' EXIT; f() { echo "a${unsetvar:?oops}z"; }; f)
1:EXIT trap called when ${var:?} triggers inside function
*?*unsetvar: oops
>EXIT

(trap 'echo A' EXIT
f1() {
trap 'echo B' EXIT
f2() {
trap 'echo C' EXIT
echo "a${unsetvar:?oops}z"
}
f2
}
f1)
1:nested EXIT traps all called when ${var:?} triggers inside function
*?*unsetvar: oops
>C
>B
>A

(setopt nounset; trap 'echo EXIT' EXIT; f() { echo $unset; }; f)
1:EXIT trap called when NOUNSET triggers inside function
*?*unset: parameter not set
>EXIT

(setopt nounset
trap 'echo A' EXIT
f1() {
trap 'echo B' EXIT
f2() {
trap 'echo C' EXIT
echo $unset
}
f2
}
f1)
1:nested EXIT traps all called when NOUNSET triggers inside function
*?*unset: parameter not set
>C
>B
>A

(setopt nounset; trap 'echo EXIT' EXIT; f() { echo $(( unset )); }; f)
1:EXIT trap called when NOUNSET triggers in math context inside function
*?*unset: parameter not set
>EXIT

( set -o ERR_RETURN; f() { false; echo NOT REACHED; }; f || true; echo OK )
( set -o ERR_RETURN; f() { true && false; echo NOT REACHED; }; f || true; echo OK )
( set -o ERR_RETURN; f() { true && { false }; echo NOT REACHED; }; f || true; echo OK )
Expand Down