`ERR` trap is not executed when syntax error

Viewed 1082

According to man bash,

set -e

Exit immediately if (snip). A trap on ERR, if set, is executed before the shell exits.

However, the script below doesn't invoke the ERR trap.

trap 'echo ERR; sleep 1' ERR
trap 'echo EXIT; sleep 1' EXIT

set -e

array=(a b c)
echo $(( ${#array[@] - 1)) #note the closing bracket } is forgotten

echo "after"

The expected result is

$ bash script.sh 
4.sh: line 7:  ${#array[@] - 1: bad substitution
ERR
EXIT
# and then shell exits

The actual result is

$ bash script.sh 
4.sh: line 7:  ${#array[@] - 1: bad substitution
EXIT
# and then shell exits

If I delete the line set -e, then

$ bash script2.sh
4.sh: line 7:  ${#array[@] - 1: bad substitution
after #the last echo command is executed
EXIT

This means set -e catches the syntax error. Why isn't the ERR trap invoked although the shell does exit?


Environment:

I tested the script on two machines.

$ bash --version
GNU bash, version 4.4.12(1)-release (arm-unknown-linux-gnueabihf)
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ bash --version
GNU bash, version 5.0.11(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Supplement:

According to the posted answers, it is natural the trap isn't executed and this is because echo $(( ${#array[@] - 1)) doesn't complete its execution to return exit status. Is my understanding right?

However, man bash explains set -e as

Exit immediately if a pipeline (which may consist of a single simple command), a list, or a compound command (see SHELL GRAMMAR above), exits with a non-zero status.

I think this also requires the command to complete. If echo $(( ${#array[@] - 1)) doesn't complete its execution, I believe set -e should not work here and echo "after" should be executed.


Supplement2:

According to oguz ismail's comment, this is a documentation issue. POSIX says set -e shall exit the shell when

  • non-zero exit status is returned (as you can see in man bash)

  • or shell error occurs

and shell error includes syntax error. But man bash is missing the explanation for the second case, right? If so, everything is consistent other than the fact that man bash doesn't explain the implementation accurately and the statement "These are the same conditions obeyed by the errexit (-e) option." found in the trap's explanation in man bash.

2 Answers

This is because of a weird way how set -e is implemented in bash over the years. It comes around with lot of specific cases under which is expected to work.

From man bash:

  • trap ... ERR
    • If a sigspec is ERR, the command arg is executed whenever a pipeline (which may consist of a single simple command), a list, or a compound command returns a non-zero exit status, subject to the following conditions:
      • The ERR trap is not executed if the failed command is part of the command list immediately following a while or until keyword...
      • ...part of the test in an if statement...
      • ...part of a command executed in a && or || list except the command following the final && or ||...
      • ...any command in a pipeline but the last...
      • ...or if the command's return value is being inverted using !.
    • These are the same conditions obeyed by the errexit -e option.

What you have in this example code is a shell expansion error for which the above mentioned clauses never really apply. In all the above clauses an incorrect command runs/completes and sets an return-code back to the shell for your ERR trap to fire.

But when the line echo $(( ${#array[@] - 1)) is encountered, soon after the arithmetic expansion fails, there is no command that is actually run that would fire an ERR trap, because to fire a trap the command needs to be complete. From the man bash page

  • If Bash is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes.

The ERR trap has never trapped syntax errors, and is not designed for that. From man bash:

  If  a  sigspec  is  ERR,  the command arg is executed whenever a
  pipeline (which may consist of a single simple command), a list,
  or a compound command returns a non-zero exit status ...

in this case the command is never executed, so does not return a non-zero status, the script fails before that.

Related