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
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
I want to know whether any commands in a bash script exited with a non-zero status.
I want something similar to set -e functionality, except that I don't want it to exit when a command exits with a non-zero status. I want it to run the whole script, and then I want to know that either:
a) all commands exited with exit status 0
-or-
b) one or more commands exited with a non-zero status
e.g., given the following:
#!/bin/bash
command1 # exits with status 1
command2 # exits with status 0
command3 # exits with status 0
I want all three commands to run. After running the script, I want an indication that at least one of the commands exited with a non-zero status.
Set a trap on ERR:
#!/bin/bash
err=0
trap 'err=1' ERR
command1
command2
command3
test $err = 0 # Return non-zero if any command failed
You might even throw in a little introspection to get data about where the error occurred:
#!/bin/bash
for i in 1 2 3; do
eval "command$i() { echo command$i; test $i != 2; }"
done
err=0
report() {
err=1
printf '%s' "error at line ${BASH_LINENO[0]}, in call to "
sed -n ${BASH_LINENO[0]}p $0
} >&2
trap report ERR
command1
command2
command3
exit $err
You could try to do something with a trap for the DEBUG pseudosignal, such as
trap '(( $? && ++errcount ))' DEBUG
The DEBUG trap is executed
before every simple command, for command, case command, select command, every arithmetic for command, and before the first command executes in a shell function
(quote from manual).
So if you add this trap and as the last command something to print the error count, you get the proper value:
#!/usr/bin/env bash
trap '(( $? && ++errcount ))' DEBUG
true
false
true
echo "Errors: $errcount"
returns Errors: 1 and
#!/usr/bin/env bash
trap '(( $? && ++errcount ))' DEBUG
true
false
true
false
echo "Errors: $errcount"
prints Errors: 2. Beware that that last statement is actually required to account for the second false because the trap is executed before the commands, so the exit status for the second false is only checked when the trap for the echo line is executed.
I am not sure if there is a ready-made solution for your requirement. I would write a function like this:
function run_cmd_with_check() {
"$#"
[[ $? -ne 0 ]] && ((non_zero++))
}
Then, use the function to run all the commands that need tracking:
run_cmd_with_check command1
run_cmd_with_check command2
run_cmd_with_check command3
printf "$non_zero commands exited with non-zero exit code\n"
If required, the function can be enhanced to store all failed commands in an array which can be printed out at the end.
You may want to take a look at this post for more info: Error handling in Bash
You have the magic variable $? available in bash which tells the exit code of last command:
#!/bin/bash
command1 # exits with status 1
C1_output=$? # will be 1
command2 # exits with status 0
C2_output=$? # will be 0
command3 # exits with status 0
C3_output=$? # will be 0
For each command you could do this:
if ! Command1 ; then an_error=1; fi
And repeat this for all commands
At the end an_error will be 1 if any of them failed.
If you want a count of failures set an_error to 0 at the beginning and do $((an_error++)). Instead of an_error=1
You could place your list of commands into an array and then loop over the commands. Any that return an error code your keep the results for later viewing.
declare -A results
commands=("your" "commands")
for cmd in "${commands[#]}"; do
out=$($cmd 2>&1)
[[ $? -eq 0 ]] || results[$cmd]="$out"
done
Then to see any non zero exit codes:
for cmd in "${!results[#]}"; do echo "$cmd = ${results[$cmd]}"; done
If the length of results is 0, there were no errors on your list of commands.
This requires Bash 4+ (for the associative array)
You can use the DEBUG trap like:
trap 'code+=$?' DEBUG
code=0
# run commands here normally
exit $code
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?
I want to know whether any commands in a bash script exited with a non-zero status.
I want something similar to set -e functionality, except that I don't want it to exit when a command exits with a non-zero status. I want it to run the whole script, and then I want to know that either:
a) all commands exited with exit status 0
-or-
b) one or more commands exited with a non-zero status
e.g., given the following:
#!/bin/bash
command1 # exits with status 1
command2 # exits with status 0
command3 # exits with status 0
I want all three commands to run. After running the script, I want an indication that at least one of the commands exited with a non-zero status.
Set a trap on ERR:
#!/bin/bash
err=0
trap 'err=1' ERR
command1
command2
command3
test $err = 0 # Return non-zero if any command failed
You might even throw in a little introspection to get data about where the error occurred:
#!/bin/bash
for i in 1 2 3; do
eval "command$i() { echo command$i; test $i != 2; }"
done
err=0
report() {
err=1
printf '%s' "error at line ${BASH_LINENO[0]}, in call to "
sed -n ${BASH_LINENO[0]}p $0
} >&2
trap report ERR
command1
command2
command3
exit $err
You could try to do something with a trap for the DEBUG pseudosignal, such as
trap '(( $? && ++errcount ))' DEBUG
The DEBUG trap is executed
before every simple command, for command, case command, select command, every arithmetic for command, and before the first command executes in a shell function
(quote from manual).
So if you add this trap and as the last command something to print the error count, you get the proper value:
#!/usr/bin/env bash
trap '(( $? && ++errcount ))' DEBUG
true
false
true
echo "Errors: $errcount"
returns Errors: 1 and
#!/usr/bin/env bash
trap '(( $? && ++errcount ))' DEBUG
true
false
true
false
echo "Errors: $errcount"
prints Errors: 2. Beware that that last statement is actually required to account for the second false because the trap is executed before the commands, so the exit status for the second false is only checked when the trap for the echo line is executed.
I am not sure if there is a ready-made solution for your requirement. I would write a function like this:
function run_cmd_with_check() {
"$#"
[[ $? -ne 0 ]] && ((non_zero++))
}
Then, use the function to run all the commands that need tracking:
run_cmd_with_check command1
run_cmd_with_check command2
run_cmd_with_check command3
printf "$non_zero commands exited with non-zero exit code\n"
If required, the function can be enhanced to store all failed commands in an array which can be printed out at the end.
You may want to take a look at this post for more info: Error handling in Bash
You have the magic variable $? available in bash which tells the exit code of last command:
#!/bin/bash
command1 # exits with status 1
C1_output=$? # will be 1
command2 # exits with status 0
C2_output=$? # will be 0
command3 # exits with status 0
C3_output=$? # will be 0
For each command you could do this:
if ! Command1 ; then an_error=1; fi
And repeat this for all commands
At the end an_error will be 1 if any of them failed.
If you want a count of failures set an_error to 0 at the beginning and do $((an_error++)). Instead of an_error=1
You could place your list of commands into an array and then loop over the commands. Any that return an error code your keep the results for later viewing.
declare -A results
commands=("your" "commands")
for cmd in "${commands[#]}"; do
out=$($cmd 2>&1)
[[ $? -eq 0 ]] || results[$cmd]="$out"
done
Then to see any non zero exit codes:
for cmd in "${!results[#]}"; do echo "$cmd = ${results[$cmd]}"; done
If the length of results is 0, there were no errors on your list of commands.
This requires Bash 4+ (for the associative array)
You can use the DEBUG trap like:
trap 'code+=$?' DEBUG
code=0
# run commands here normally
exit $code
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