I'm trying to create some error reporting using a Trap to call a function on all errors:
Trap "_func" ERR
Is it possible to get what line the ERR signal was sent from? The shell is bash.
If I do that, I can read and report what command was used and log/perform some actions.
Or maybe I'm going at this all wrong?
I tested with the following:
#!/bin/bash
trap "ECHO $LINENO" ERR
echo hello | grep "asdf"
And $LINENO
is returning 2. Not working.
9 Answers 9
As pointed out in comments, your quoting is wrong. You need single quotes to prevent $LINENO
from being expanded when the trap line is first parsed.
This works:
#! /bin/bash
err_report() {
echo "Error on line 1ドル"
}
trap 'err_report $LINENO' ERR
echo hello | grep foo # This is line number 9
Running it:
$ ./test.sh
Error on line 9
-
thanks for the example with a function call. I didn't know that double quotes expanded the variable in this case.Anthony Miller– Anthony Miller2012年05月30日 14:23:00 +00:00Commented May 30, 2012 at 14:23
-
echo hello | grep foo
doesn't seem to throw error for me. Am I misunderstanding something?geotheory– geotheory2015年12月02日 22:15:49 +00:00Commented Dec 2, 2015 at 22:15 -
1@geotheory On my system
grep
has an exit status of 0 if there was a match, 1 if there was no match and >1 for an error. You can check the behavior on your system withecho hello | grep foo; echo $?
Patrick– Patrick2015年12月07日 23:17:50 +00:00Commented Dec 7, 2015 at 23:17 -
No you're right it is an error :)geotheory– geotheory2015年12月08日 09:56:49 +00:00Commented Dec 8, 2015 at 9:56
-
4It is noteworthy that this only works for ERR traps; e.g. LINENO may always be 1 in an EXIT trap. c.f. unix.stackexchange.com/a/270623/169077n.caillou– n.caillou2017年11月03日 08:22:29 +00:00Commented Nov 3, 2017 at 8:22
You can also use the bash builtin 'caller':
#!/bin/bash
err_report() {
echo "errexit on line $(caller)" >&2
}
trap err_report ERR
echo hello | grep foo
it prints filename too:
$ ./test.sh
errexit on line 9 ./test.sh
Is it possible to get what line the ERR signal was sent from?
Yes, LINENO
and BASH_LINENO
variables are supper useful for getting the line of failure and the lines that lead up to it.
Or maybe I'm going at this all wrong?
Nope, just missing -q
option with grep...
echo hello | grep -q "asdf"
... With the -q
option grep
will return 0
for true
and 1
for false
. And in Bash it's trap
not Trap
...
trap "_func" ERR
... I need a native solution...
Here's a trapper that ya might find useful for debugging things that have a bit more cyclomatic complexity...
## Outputs Front-Mater formatted failures for functions not returning 0
## Use the following line after sourcing this file to set failure trap
## trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
failure(){
local -n _lineno="${1:-LINENO}"
local -n _bash_lineno="${2:-BASH_LINENO}"
local _last_command="${3:-${BASH_COMMAND}}"
local _code="${4:-0}"
## Workaround for read EOF combo tripping traps
if ! ((_code)); then
return "${_code}"
fi
local _last_command_height="$(wc -l <<<"${_last_command}")"
local -a _output_array=()
_output_array+=(
'---'
"lines_history: [${_lineno} ${_bash_lineno[*]}]"
"function_trace: [${FUNCNAME[*]}]"
"exit_code: ${_code}"
)
if [[ "${#BASH_SOURCE[@]}" -gt '1' ]]; then
_output_array+=('source_trace:')
for _item in "${BASH_SOURCE[@]}"; do
_output_array+=(" - ${_item}")
done
else
_output_array+=("source_trace: [${BASH_SOURCE[*]}]")
fi
if [[ "${_last_command_height}" -gt '1' ]]; then
_output_array+=(
'last_command: ->'
"${_last_command}"
)
else
_output_array+=("last_command: ${_last_command}")
fi
_output_array+=('---')
printf '%s\n' "${_output_array[@]}" >&2
exit ${_code}
}
... and an example usage script for exposing the subtle differences in how to set the above trap for function tracing too...
#!/usr/bin/env bash
set -E -o functrace
## Optional, but recommended to find true directory this script resides in
__SOURCE__="${BASH_SOURCE[0]}"
while [[ -h "${__SOURCE__}" ]]; do
__SOURCE__="$(find "${__SOURCE__}" -type l -ls | sed -n 's@^.* -> \(.*\)@1円@p')"
done
__DIR__="$(cd -P "$(dirname "${__SOURCE__}")" && pwd)"
## Source module code within this script
source "${__DIR__}/modules/trap-failure/failure.sh"
trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
something_functional() {
_req_arg_one="${1:?something_functional needs two arguments, missing the first already}"
_opt_arg_one="${2:-SPAM}"
_opt_arg_two="${3:0}"
printf 'something_functional: %s %s %s' "${_req_arg_one}" "${_opt_arg_one}" "${_opt_arg_two}"
## Generate an error by calling nothing
"${__DIR__}/nothing.sh"
}
## Ignoring errors prevents trap from being triggered
something_functional || echo "Ignored something_functional returning $?"
if [[ "$(something_functional 'Spam!?')" == '0' ]]; then
printf 'Nothing somehow was something?!\n' >&2 && exit 1
fi
## And generating an error state will cause the trap to _trace_ it
something_functional '' 'spam' 'Jam'
The above where tested on Bash version 4+, so leave a comment if something for versions prior to four are needed, or Open an Issue if it fails to trap failures on systems with a minimum version of four.
The main takeaways are...
set -E -o functrace
-E
causes errors within functions to bubble up-o functrace
causes allows for more verbosity when something within a function fails
trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
Single quotes are used around function call and double quotes are around individual arguments
References to
LINENO
andBASH_LINENO
are passed instead of the current values, though this might be shortened in later versions of linked to trap, such that the final failure line makes it into outputValues of
BASH_COMMAND
and exit status ($?
) are passed, first to get the command that returned an error, and second for ensuring that the trap does not trigger on non-error statuses
And while others may disagree I find it's easier to build an output array and use printf for printing each array element on it's own line...
printf '%s\n' "${_output_array[@]}" >&2
... also the >&2
bit at the end causes errors to go where they should (standard error), and allows for capturing just errors...
## ... to a file...
some_trapped_script.sh 2>some_trapped_errros.log
## ... or by ignoring standard out...
some_trapped_script.sh 1>/dev/null
As shown by these and other examples on Stack Overflow, there be lots of ways to build a debugging aid using built in utilities.
-
1Very good answer! This should get a lot more up-votes. Excellent error tracing that actually enables debugCIsForCookies– CIsForCookies2022年01月04日 17:15:44 +00:00Commented Jan 4, 2022 at 17:15
-
1Can anyone help me understand why the "trap" line contains args like "LINENO" (the variable name) instead of "$LINENO" (the variable value). If I try something similar myself, passing the value appears to do the right thing.Jonathan Hartley– Jonathan Hartley2022年02月15日 16:48:19 +00:00Commented Feb 15, 2022 at 16:48
-
1Here's how to jump to the related manual sections
man -P 'less -ip "^\s+lineno"' bash
andman -P 'less -ip "^\s+bash_lineno"' bash
at this moment I cannot remember why I chose to passLINENO
by name/reference instead of value... but I've a feeling it had to do with order of execution/expansion when thetrap
gets tripped vs when it is set.S0AndS0– S0AndS02022年02月18日 06:08:46 +00:00Commented Feb 18, 2022 at 6:08
I really like the answer given by @Mat above. Building on this, I wrote a little helper which gives a bit more context for the error:
We can inspect the script for the line which caused the failure:
err() {
echo "Error occurred:"
awk 'NR>L-4 && NR<L+4 { printf "%-5d%3s%s\n",NR,(NR==L?">>>":""),0ドル }' L=1ドル 0ドル
}
trap 'err $LINENO' ERR
Here it is in a small test script:
#!/bin/bash
set -e
err() {
echo "Error occurred:"
awk 'NR>L-4 && NR<L+4 { printf "%-5d%3s%s\n",NR,(NR==L?">>>":""),0ドル }' L=1ドル 0ドル
}
trap 'err $LINENO' ERR
echo one
echo two
echo three
echo four
false
echo five
echo six
echo seven
echo eight
When we run it we get:
$ /tmp/test.sh
one
two
three
four
Error occurred:
12 echo two
13 echo three
14 echo four
15 >>>false
16 echo five
17 echo six
18 echo seven
-
3This would be even better using
$(caller)
's data to give the context even if the failure is not in the current script but one of its imports. Very nice though!tricasse– tricasse2019年03月07日 18:42:04 +00:00Commented Mar 7, 2019 at 18:42 -
1I'm confused about "set -e", I think this means exit on error. But you're handling the error. I cannot understand the purpose.Tarek Eldeeb– Tarek Eldeeb2021年06月17日 13:13:01 +00:00Commented Jun 17, 2021 at 13:13
-
1@TarekEldeeb - This isn't like catching an error in other languages, by the time that the error trap is called, the script is in the process of stopping. There's no option here to return to the command which caused the errexit to be invoked.unpythonic– unpythonic2021年06月18日 17:04:23 +00:00Commented Jun 18, 2021 at 17:04
Here's another version, inspired by @sanmai and @unpythonic. It shows script lines around the error, with line numbers, and the exit status - using tail & head as that seems simpler than the awk solution.
Showing this as two lines here for readability - you can join these lines into one if you prefer (preserving the ;
):
trap 'echo >&2 "Error - exited with status $? at line $LINENO:";
pr -tn 0ドル | tail -n+$((LINENO - 3)) | head -n7 >&2' ERR
This works quite well with set -eEuo pipefail
(unofficial strict mode)
- any undefined variable error gives a line number without firing the
ERR
pseudo-signal, but the other cases do show context.
Example output:
myscript.sh: line 27: blah: command not found
Error - exited with status 127 at line 27:
24 # Do something
25 lines=$(wc -l /etc/passwd)
26 # More stuff
27 blah
28
29 # Check time
30 time=$(date)
-
Loved the mention about
set -euo pipefail
!!! But... is there any way to trap the undefined var case?MestreLion– MestreLion2020年08月15日 04:29:08 +00:00Commented Aug 15, 2020 at 4:29 -
1I don't know of a way to trap the undefined var error, which seems to be detected without firing this trap. However, the built-in error message is quite clear and has a line number:
foo.sh: line 7: x: unbound variable
.RichVel– RichVel2020年08月15日 06:09:47 +00:00Commented Aug 15, 2020 at 6:09 -
1Yeah, just noticed that. That builtin message is enough for me. By the way, you might add
-E
to your "unofficial strict mode" so the trap also catches errors inside functions. My final strict mode becameset -Eeuo pipefail
MestreLion– MestreLion2020年08月15日 07:06:18 +00:00Commented Aug 15, 2020 at 7:06 -
@MestreLion - updated answer to include
-E
in 'unofficial strict mode', good tip!RichVel– RichVel2022年08月07日 12:47:12 +00:00Commented Aug 7, 2022 at 12:47
Inspired by other answer, here's a simpler contextual error handler:
trap '>&2 echo Command failed: $(tail -n+$LINENO 0ドル | head -n1)' ERR
You can also use awk instead of tail & head if needed.
-
1there's a reason the other answer provides context by way of 3 lines above and 3 lines below the offending line - what if the error emanates from a continuation line?iruvar– iruvar2019年03月23日 03:13:04 +00:00Commented Mar 23, 2019 at 3:13
-
1@iruvar this is understood, but I don't need any of that extra context; one line of context is as simple as it gets, and as sufficient as I needsanmai– sanmai2019年03月23日 04:42:53 +00:00Commented Mar 23, 2019 at 4:42
-
The trap is very useful for finding undefined variables and array elements. There are a couple of "gotchas":
Incremented variables
((i++))
post-increments. Ifi
is zero, the return code is 1, triggering an apparent error. See Why does a=0; let a++ return exit code 1?Change to pre-increment
((++i))
and the problem goes awayTesting for unset array elements
if [[ -z ${array[$element]} ]] then ...
will report 'unbound variable'. The +_ syntax does not trigger this error
if ! [[ ${array[$element]+_} ]]
This form is not exactly easy to remember 8-{
Apologies for posting a comment as an answer: the "gotchas" can easily put you off using a trap so it seemed worth noting the ways to avoid them.
-
2That unbound variable fix seems more complex than necessary. When in
set -u
mode, simply use${foo:-}
for variables where you expect a variable may be unset and you still want an empty value rather than an error, e.g.[[ -z ${foo:-} ]]
.cjs– cjs2024年04月23日 07:16:57 +00:00Commented Apr 23, 2024 at 7:16 -
Thanks. This is indeed simpler. It makes my point about these structures being difficult to remember 8-|user8150417– user81504172024年04月30日 16:17:18 +00:00Commented Apr 30, 2024 at 16:17
first
# bash strict mode
set -Eeuo pipefail
# debug mode
# set -x
second
function __error_handing__(){
local last_status_code=1ドル;
local error_line_number=2ドル;
echo 1>&2 "Error - exited with status $last_status_code at line $error_line_number";
perl -slne 'if($.+5 >= $ln && $.-4 <= $ln){ $_="$. $_"; s/$ln/">" x length($ln)/eg; s/^\D+.*?$/\e[1;31m$&\e[0m/g; print}' -- -ln=$error_line_number 0ドル
}
third
trap '__error_handing__ $? $LINENO' ERR
sample output
which: no no-cmd in (/home/shm/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/var/lib/snapd/snap/bin)
Error - exited with status 1 at line 98
93 declare -A pse_pre_check_list;
94 pse_pre_check_list['yq is installed']='which docker';
95 pse_pre_check_list['mc is installed']='which docker-compose';
96 pse_pre_check_list['shell is bash']='grep bash <<< $SHELL';
97
>> which no-cmd
99
100 declare -a pse_pre_check_order;
101 pse_pre_check_order+=('yq is installed');
102 pse_pre_check_order+=('mc is installed');
If we set -E
it handles errors inside functions as well (see man bash)
For more you can read bash-error-handling
Using @RichVel, this is how I use traps:
set -eEuo pipefail
readonly LOG=".log"; echo "######### $(date) #########" >> $LOG
--trap 'echo "Failed. Exited with status $?. See $LOG" | tee -a $LOG; echo "Failed at ${LINENO}" >> $LOG; pr -tn 0ドル | tail -n+$((LINENO - 3)) | head -n7 | sed "4s/^\s*/>>> /" >> $LOG' ERR
Running this from a faulty script would give a nice log:
set -eEuo pipefail
readonly LOG=".log"; echo "######### $(date) #########" >> $LOG
trap 'echo "Failed. Exited with status $?. See $LOG" | tee -a $LOG; echo "Failed at ${LINENO}" >> $LOG; pr -tn 0ドル | tail -n+$((LINENO - 3)) | head -n7 | sed "4s/^\s*/>>> /" >> $LOG' ERR
function bla(){
blabla
}
echo a
if true; then
if false; then
echo;
fi
bla
fi
$ bash error.sh
$ cat .log
######### Mon Feb 28 19:19:05 IST 2022 #########
Failed. Exited with status 127. See .log
Failed at 7
4 trap 'echo "Failed. Exited with status $?. See $LOG" | tee -a $LOG; echo "Failed at ${LINENO}" >> $LOG; pr -tn 0ドル | tail -n+$((LINENO - 3)) | head -n7 | sed "4s/^\s*/>>> /" >> $LOG' ERR
5
6 function bla(){
>>> 7 blabla
8 }
9
10
You must log in to answer this question.
Explore related questions
See similar questions with these tags.
bashdb
. It seems that the first argument totrap
can contain variables that are evaluated in the desired context. Sotrap 'echo $LINENO' ERR'
should work.trap 'echo $LINENO' ERR
. The first argument totrap
is the entireecho $LINENO
hardquoted. This is in bash.trap 'echo $LINENO' ERR
, with single quotes, not double quotes. With the command you wrote,$LINENO
is expanded when line 2 is parsed, so the trap isecho 2
(or ratherECHO 2
, which would outputbash: ECHO: command not found
).