This is working:
#! /bin/bash
set -o errexit
trap 'echo Error on line $LINENO' ERR
echo "start"
false
Output is:
start
Error on line 6
When same code is executed in a function, the trap is not executed:
#! /bin/bash
set -o errexit
trap 'echo Error on line $LINENO' ERR
function willFail() {
false
}
echo "start"
willFail
echo "end"
Output is:
start
How to make a trap executed when something fails inside a function?
Related
Short version
In a Bash script, I activate a trap, and later deactivate it by calling trap - EXIT ERR SIGHUP SIGINT SIGTERM. When I do the deactivation directly in the script, it works. However, when I put the exact same line of code in a Bash function, it is ignored, i.e. the trap is still activated if, later, a command returns an exit code different from zero. Why?
Long version
I have a bunch of functions to work with traps:
trap_stop()
{
echo "trap_stop"
trap - EXIT ERR SIGHUP SIGINT SIGTERM
}
trap_terminate()
{
local exitCode="$?"
echo "trap_terminate"
trap_stop
local file="${BASH_SOURCE[1]}"
local stack=$(caller)
local line="${stack% *}"
if [ $exitCode == 0 ]; then
echo "Finished."
else
echo "The initialization failed with code $exitCode in $file:${line}."
fi
exit $exitCode
}
trap_start()
{
echo "trap_start"
trap "trap_terminate $LINENO" EXIT ERR SIGHUP SIGINT SIGTERM
}
When used like this:
trap_start # <- Trap started.
echo "Stopping traps."
trap_stop # <- Trap stopped before calling a command which exits with exit code 2.
echo "Performing a command which will fail."
ls /tmp/missing
exit_code="$?"
echo "The result of the check is $exit_code."
I get the following output:
trap_start
Stopping traps.
trap_stop
Performing a command which will fail.
ls: cannot access '/tmp/missing': No such file or directory
trap_terminate
trap_stop
The initialization failed with code 2 in ./init:41.
Despite the fact that function deactivating the trap was called, the trap was still triggered when calling ls on a directory which doesn't exist.
On the other hand, when I replace the call to trap_stop by the actual trap - statement, like this:
trap_start
echo "Stopping traps."
trap - EXIT ERR SIGHUP SIGINT SIGTERM # <- This statement replaced the call to `trap_stop`.
echo "Performing a command which will fail."
ls /tmp/missing
exit_code="$?"
echo "The result of the check is $exit_code."
then the output is correct, i.e. the trap is not activated and I reach the end of the script.
trap_start
Stopping traps.
Performing a command which will fail.
ls: cannot access '/tmp/missing': No such file or directory
The result of the check is 2.
Why is moving trap - to a function makes it stop working?
EDIT (courtesy of #KamilCuk): If your bash is older than 4.4, upgrade your bash, it could solve the problem.
I added some debugging to your code:
echo "Stopping traps."
trap -p
trap_stop # <- Trap stopped before calling a command which exits with exit code 2.
trap -p
And got:
Stopping traps.
trap -- 'trap_terminate 29' EXIT
trap -- 'trap_terminate 29' SIGHUP
trap -- 'trap_terminate 29' SIGINT
trap -- '' SIGFPE
trap -- 'trap_terminate 29' SIGTERM
trap -- '' SIGXFSZ
trap -- '' SIGPWR
trap -- 'trap_terminate 29' ERR
trap_stop
trap -- '' SIGFPE
trap -- '' SIGXFSZ
trap -- '' SIGPWR
trap -- 'trap_terminate 29' ERR
As you can see, the trap - part does work, except for the ERR condition.
After some man page time:
echo "Stopping traps."
set -E
trap_stop # <- Trap stopped before calling a command which exits with exit code 2.
yields:
trap_start
Stopping traps.
trap_stop
Performing a command which will fail.
ls: cannot access '/tmp/missing': No such file or directory
The result of the check is 2.
The relevant part of bash(1):
-E
If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment. The ERR trap is normally not inherited in such cases.
That said, this seems to be a bug in bash:
#!/bin/bash
t1()
{
trap 'echo t1' ERR
}
t2()
{
trap 'echo t2' ERR
}
t1
false
t2
false
yields:
t1
t1
whereas I'd expect at the very least:
t1
t2
My scripts have as first instruction:
set -e
So that whenever an error occurs, the script aborts. I would like to trap this situation to show an information message, but I do not want to show that message whenever the script exits; ONLY when set -e triggers the abortion. Is it possible to trap this situation?
This:
set -e
function mytrap {
echo "Abnormal termination!"
}
trap mytrap EXIT
error
echo "Normal termination"
Is called in any exit (whether an error happens or not), which is not what I want.
Instead of using trap on EXIT, use it on ERR event:
trap mytrap ERR
Full Code:
set -e
function mytrap {
echo "Abnormal termination!"
}
trap mytrap ERR
(($#)) && error
echo "Normal termination"
Now run it for error generation:
bash sete.sh 123
sete.sh: line 9: error: command not found
Abnormal termination!
And here is normal exit:
bash sete.sh
Normal termination
Consider the following shell function:
f() {
echo "function"
trap 'echo trap; sleep 1' EXIT
}
Under bash this will print the following:
~$ f
function
~$ exit
trap
On zsh however this is the result:
~$ f
function
trap
~$ exit
This is as explained in the zshbuiltins man page:
If sig is 0 or EXIT and the trap statement is executed inside the body of a function, then the command arg is executed after the function completes.
My question: Is there a way of setting an EXIT trap that only executes on shell exit in both bash and zsh?
Obligatory boring and uninteresting answer:
f() {
if [ "$ZSH_VERSION" ]
then
zshexit() { echo trap; sleep 1; } # zsh specific
else
trap 'echo trap; sleep 1' EXIT # POSIX
fi
}
I want to have a cleanup action in my Bash scripts, like this:
#! /bin/bash
set -eu
trap 'echo "E: failed with exitcode $?" 1>&2' ERR
true
false
Using $? came to mind as a natural choice, but this isn't the case. It always contains 0. Is there any way that I can "spy" on the exitcode in the ERR trap?
[Update:] I have no idea what I had tested before. This code works like a charm, so I'm leaving it here as a small and good example.
Your (probably simplified) example doesn't exhibit the problem you've mentioned:
+ set -eu
+ trap 'echo "E: failed with exitcode $?" 1>&2' ERR
+ true
+ false
++ echo 'E: failed with exitcode 1'
E: failed with exitcode 1
Chances are that the command returning ERR is executed in a && or ||, or subject to other conditions mentioned in the snippet below. Quoting from the manual:
If a sigspec is ERR, the command arg is executed whenever a simple
command has 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 an until or while keyword,
part of the test following the if or elif reserved words, part of a
command executed in a && or || list, or if the command’s return status
is being inverted using !. These are the same conditions obeyed by the
errexit option.
So if you have, for example, the following:
#! /bin/bash
set -eu
trap 'echo "E: failed with exitcode $?" 1>&2' ERR
false && true
Executed it wouldn't cause the failure to be trapped:
+ set -eu
+ trap 'echo "E: failed with exitcode $?" 1>&2' ERR
+ false
I was just playing with bash to bypass this summer afternoon heat, when suddenly I've got a mysterious result for which I cannot determine it's origin.
Let me explain it bit a bit.
I'm playing with trap ERR to create some debugging functions for my bash scripts.
This is the script that runs fine:
traperror () {
local err=$? # error status
local line=$1 # LINENO
[ "$2" != "" ] && local funcstack=$2 # funcname
[ "$3" != "" ] && local linecallfunc=$3 # line where func was called
echo "<---"
echo "ERROR: line $line - command exited with status: $err"
if [ "$funcstack" != "" ]; then
echo -n " ... Error at function ${funcstack[0]}() "
if [ "$linecallfunc" != "" ]; then
echo -n "called at line $3"
fi
echo
fi
echo "--->"
}
#trap 'traperror $LINENO ${FUNCNAME}' ERR
somefunction () {
trap 'traperror $LINENO ${FUNCNAME} $BASH_LINENO' ERR
asdfas
}
somefunction
echo foo
The output is (stderr goes to /dev/null for clarity; the bash error is of course foo.sh: line 23: asdfas: command not found which is as you know error code 127)
~$ bash foo.sh 2> /dev/null
<---
ERROR: line 21 - command exited with status: 127
... Error at function somefunction() called at line 24
--->
foo
All the line numbers are right, line 21 is where starts the function "somefunction" and line 24 is where it is called.
However if I uncomment the first trap (the one in main) I get this output:
~$ bash foo.sh 2> /dev/null
<---
ERROR: line 21 - command exited with status: 127
... Error at function somefunction() called at line 24
--->
<---
ERROR: line 15 - command exited with status: 127
--->
foo
In case I uncomment the first trap and comment the second one I get that the error is in line 23 which is right too because it is the absolute line where the wrong command is placed.
~$ bash foo.sh
<---
ERROR: line 23 - command exited with status: 127
--->
foo
So my question is: why line 15? where does that line number come from? Line 15 is the last line in the trap function. Can anyone explain in plain English why trap returns the last line of the function it calls as the line that produced the error in line 21?
Thanks in advance!
EDIT
Just in case someone is interested in the debug function. This is the production version:
# Copyright (c): Hilario J. Montoliu <hmontoliu#gmail.com>
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.
set -o errtrace
trap 'traperror $? $LINENO $BASH_LINENO "$BASH_COMMAND" $(printf "::%s" ${FUNCNAME[#]})' ERR
traperror () {
local err=$1 # error status
local line=$2 # LINENO
local linecallfunc=$3
local command="$4"
local funcstack="$5"
echo "<---"
echo "ERROR: line $line - command '$command' exited with status: $err"
if [ "$funcstack" != "::" ]; then
echo -n " ... Error at ${funcstack} "
if [ "$linecallfunc" != "" ]; then
echo -n "called at line $linecallfunc"
fi
else
echo -n " ... internal debug info from function ${FUNCNAME} (line $linecallfunc)"
fi
echo
echo "--->"
}
somefunction () {
asdfasdf param1
}
somefunction
echo foo
Which will work as:
~$ bash foo.sh 2> /dev/null
<---
ERROR: line 26 - command 'asdfasdf param1' exited with status: 127
... Error at ::somefunction::main called at line 29
--->
<---
ERROR: line 22 - command 'asdfasdf param1' exited with status: 127
... internal debug info from function traperror (line 0)
--->
foo
Some relevant facts/background info:
Traps on ERR are not inherited by shell functions even though they get the rest of the environment, unless errtrace is set.
The exit status of a function is that of its last command.
My guess as to what is happening:
In the case where both traps are active,
The nonexistent command triggers the ERR trap in the function. LINENO is that of the nonexistent command.
The trap finishes executing. Since the nonexistent command was the last command, the return status of the function is nonzero, so the ERR trap in the shell is triggered. LINENO is still set to the last line of traperror since it was the last line to execute and is still the current line, as no new line has been executed yet.
In the case where only the shell trap is active (the one in the function is commented out)
The nonexistent command is the last command in the function, so causes the function to return non-zero, thus causing the shell's ERR trap to trigger. For the same reason above, LINENO is the last line of the function as it was the last line to execute and is still the current line.
To make sure in your first version of your traperror function that the ERR signal handler will not be executed twice, you can ignore or reset the ERR signal handler to its default action for the rest of your program - within the definition of the ERR signal handler itself. And this should always be done for a custom EXIT signal handler as well.
trap "" EXIT ERR # ignore
trap - EXIT ERR # reset
# for the first version of your traperror function
- trap 'traperror $LINENO ${FUNCNAME}' ERR
- trap 'traperror $LINENO ${FUNCNAME} $BASH_LINENO' ERR
+ trap 'traperror $LINENO ${FUNCNAME}; trap - ERR' ERR
+ trap 'traperror $LINENO ${FUNCNAME} $BASH_LINENO; trap - ERR' ERR