Why can't I exit from an exit trap when I'm inside of a function in ZSH, unless I'm in a loop?

Viewed 184

I'm really trying to understand the difference in how ZSH and Bash are handling signal traps, but I'm having a very hard time grasping why ZSH is doing what it's doing.

In short, I'm not able to exit a script with exit in ZSH from within a trap if the execution point is within a function, unless it's also within a loop.

Here is an example of how exit in a trap action behaves in the global / file level scope.

#!/bin/zsh

trap 'echo "Trap SIGINT" ; exit 130' SIGINT
sleep 1
echo "1"
sleep 1
echo "2"
sleep 1
echo "3"

If I call the script, I can send an INT signal by pressing Cntrl+C at any time to echo "Trap SIGINT" and exit the script immediately.

If I hit Cntrl+C after I see the first 1, the output looks like this:

$ ./foobar
1
^CTrap SIGINT

But if I wrap the code in a function, then the trap doesn't want to stop script execution until the function finishes. Using exit 130 from within the trap action just continues the code from the execution point within the function.

Here is an example of how using trap behaves in the function level scope.

#!/bin/zsh

trap 'echo "Trap SIGINT" ; exit 130' SIGINT

foobar() {
  sleep 1
  echo "1"
  sleep 1
  echo "2"
  sleep 1
  echo "3"
}

foobar

echo "Finished"

If I call the script, the only thing that an INT signal does is end the sleep command early. The script will just keep on going from the same execution point after that.

If I hit Cntrl+C repeatedly the output looks like this.

$ ./foobar
^CTrap SIGINT
1
^CTrap SIGINT
2
^CTrap SIGINT
3

It doesn't echo the "Finished" at the end, so it is exiting when the function is finished, but I can't seem to exit before it's finished.

It doesn't make a difference if I set the trap in the global / file scope or from within the function.

If I change exit 130 to return 130, then it will jump out of that function early but continue script execution. This is expected behavior from what I could read in the ZSH documentation.

If I wrap the code inside of a for or while loop as shown in the code below, the code then has no problem breaking out of the loop.

#!/bin/zsh

trap 'echo "Trap SIGINT" ; exit 130' SIGINT

foobar() {
  for i in 1; do
    sleep 1
    echo "1"
    sleep 1
    echo "2"
    sleep 1
    echo "3"
  done
  sleep 1
  echo "Outside of loop"
}

foobar

echo "Finished"

Even if I have the loop in the global / file scope and calling foobar from within the loop, it still has no problem exiting within the trap action. I assume it's because using

The one thing that does work correctly is defining a TRAPINT function instead of using the trap built-in, and returning a non-exit code from that function. However exiting from the TRAPINT function works the same way it does with the trap built-in.

I've tried to find anything on why it acts like this but I couldn't find anything.

So what's actually happening here? Why is ZSH not letting me exit from the trap action when the execution point is inside a function?

1 Answers

One way to make this work as expected is setting the ERR_EXIT option.

From the documentation:

If a command has a non-zero exit status, execute the ZERR trap, if set, and exit. This is disabled while running initialization scripts.

There's also ERR_RETURN:

If a command has a non-zero exit status, return immediately from the enclosing function. The logic is similar to that for ERR_EXIT, except that an implicit return statement is executed instead of an exit. This will trigger an exit at the outermost level of a non-interactive script.

Both options have some caveats and notes; refer to the documentation.

Adding a setopt localoptions err_exit as the first line of the foobar function (You probably don't want to do this globally) in your script causes:

$ ./foobar
1
^CTrap SIGINT
$

Now, the interesting bit. In your demonstration script, if you change your exit value from 130 to some other number, and the echo lines to echo "1 - $?" etc., you get:

$ ./foobar
1 - 0
2 - 0
^CTrap SIGINT
3 - 130

The sleep is still exiting with 130, the normal value for a process killed by a SIGINT. What happened to your exit in the trap and its value? Not a clue (I'll update the answer if I figure it out) .

I'd just stick with the TRAPnal functions when writing zsh scripts that care about signals.

Related