Trap, ERR, and echoing the error line

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.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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

Method 2

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

Method 3

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%sn",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%sn",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

Method 4

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…

failure.sh

## 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 '%sn' "${_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…

example_usage.sh

#!/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 '<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c5b685">[email protected]</a>^.* -> (.*)@<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="cafb8aba">[email protected]</a>')"
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 and BASH_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 output
  • Values 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 '%sn' "${_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.

Method 5

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' ERR

This works quite well with set -euo 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)

Method 6

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.

Method 7

The trap is very useful for finding undefined variables and array elements. There are a couple of “gotchas”:

  1. Incremented variables

    ((i++)) post-increments. If i 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 away

  2. Testing 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.

Method 8

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

Method 9

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   


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x