Trap, ERR, and echoing the error line

74,690

Solution 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

Solution 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

Solution 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%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

Solution 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 '%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...

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 '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 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 '%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.

Solution 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 >&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)
Share:
74,690
Anthony Miller
Author by

Anthony Miller

"Your competition is a hungry immigrant with a digital handheld assistant. America is made up of immigrants... if your children are to be successful they must act like immigrants in their own country. Just by being born here doesn't give you the ability to be successful... it is the work ethic... the pioneering ethic... the service ethic that will win. Your competition is always a hungry immigrant with a digital assistant: hungry in the belly for food, hungry in the mind for knowledge, and the hunger is something that should never leave you." ~Dr. Dennis Waitley

Updated on September 18, 2022

Comments

  • Anthony Miller
    Anthony Miller over 1 year

    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.

    • donothingsuccessfully
      donothingsuccessfully almost 12 years
      You can look at the bash debugger script bashdb. It seems that the first argument to trap can contain variables that are evaluated in the desired context. So trap 'echo $LINENO' ERR' should work.
    • Anthony Miller
      Anthony Miller almost 12 years
      hmm just tried this with a bad echo | grep command and it returns the line of the Trap statement. But I'll take a look at bashdb
    • Anthony Miller
      Anthony Miller almost 12 years
      I'm so sorry... I didn't specify in my original question that I need a native solution. I edited the question.
    • Anthony Miller
      Anthony Miller almost 12 years
      Didn't work. Still returned 2.
    • Gilles 'SO- stop being evil'
      Gilles 'SO- stop being evil' almost 12 years
      @Mechaflash It would have to be 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 is echo 2 (or rather ECHO 2, which would output bash: ECHO: command not found).
  • Anthony Miller
    Anthony Miller almost 12 years
    thanks for the example with a function call. I didn't know that double quotes expanded the variable in this case.
  • Obviously
    Obviously over 8 years
    echo hello | grep foo doesn't seem to throw error for me. Am I misunderstanding something?
  • Patrick
    Patrick over 8 years
    @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 with echo hello | grep foo; echo $?
  • Obviously
    Obviously over 8 years
    No you're right it is an error :)
  • Tim Bird
    Tim Bird about 7 years
    Don't you need to use -e on the invocation line, to cause error on command failure? That is: #!/bin/bash -e ?
  • Chef Pharaoh
    Chef Pharaoh over 6 years
    Do you need to add set -e for this to work? I think so from my testing.
  • n.caillou
    n.caillou over 6 years
    It 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/169077
  • Trevor Boyd Smith
    Trevor Boyd Smith over 5 years
    i like the way you still keep the trap a function... but just pass the lineno as a parameter. it keeps the code more readable easy... better than not using a function and then you have a 160 character line.
  • tricasse
    tricasse about 5 years
    This 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!
  • Mathias Begert
    Mathias Begert about 5 years
    there'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?
  • sanmai
    sanmai about 5 years
    @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 need
  • Mathias Begert
    Mathias Begert about 5 years
    Ok my friend,+1
  • MestreLion
    MestreLion almost 4 years
    Loved the mention about set -euo pipefail!!! But... is there any way to trap the undefined var case?
  • RichVel
    RichVel almost 4 years
    I 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.
  • MestreLion
    MestreLion almost 4 years
    Yeah, 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 became set -Eeuo pipefail
  • Tarek Eldeeb
    Tarek Eldeeb almost 3 years
    I'm confused about "set -e", I think this means exit on error. But you're handling the error. I cannot understand the purpose.
  • unpythonic
    unpythonic almost 3 years
    @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.
  • CIsForCookies
    CIsForCookies over 2 years
    Very good answer! This should get a lot more up-votes. Excellent error tracing that actually enables debug
  • Jonathan Hartley
    Jonathan Hartley over 2 years
    Can 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.
  • S0AndS0
    S0AndS0 about 2 years
    Here's how to jump to the related manual sections man -P 'less -ip "^\s+lineno"' bash and man -P 'less -ip "^\s+bash_lineno"' bash at this moment I cannot remember why I chose to pass LINENO by name/reference instead of value... but I've a feeling it had to do with order of execution/expansion when the trap gets tripped vs when it is set.