BashStackTraces

7th June 2017 at 11:18pm
Bash CodeSnippets

This code is an adaptation of the following post http://www.runscripts.com/support/guides/scripting/bash/debugging-bash/stack-trace.

If you want to learn more about how this works, then you would do well to read about shopt -s extdebug, caller, BASH_ARGC and BASH_ARGV in the bash documentation; https://www.gnu.org/software/bash/manual/bashref.html.

Example Output

With shopt -s extdebug set on:

nicolaw@sansa:~$ ./test.sh cmd_ln_arg1 "cmd line arg 2" 3 four       
hello world
inside woop_function() inside sourced file
subshell 1
subshell 2
test2.sh: line 14: RandomCommandThatDoesNotExist: command not found
Unexpected error executing ( RandomCommandThatDoesNotExist ) at test2.sh line 6
test2.sh:6 woop_function:: __stacktrace__ 
  test2.sh:22 source:: woop_function my arguments 'are awesome'
    ./test.sh:52 my_function3:: source test2.sh
      ./test.sh:57 my_function2:: my_function3 'hello world' b2
        ./test.sh:61 my_function1:: my_function2 b1 b2 f1 f2 b3
          ./test.sh:64 main:: my_function1 f1 f2
            /bin/bash -huBE ./test.sh cmd_ln_arg1 'cmd line arg 2' 3 four
Unexpected error executing false at test2.sh line 19
test2.sh:19 woop_function:: __stacktrace__ 
  test2.sh:22 source:: woop_function my arguments 'are awesome'
    ./test.sh:52 my_function3:: source test2.sh
      ./test.sh:57 my_function2:: my_function3 'hello world' b2
        ./test.sh:61 my_function1:: my_function2 b1 b2 f1 f2 b3
          ./test.sh:64 main:: my_function1 f1 f2
            /bin/bash -huBE ./test.sh cmd_ln_arg1 'cmd line arg 2' 3 four

With shopt -s extdebug set off (the pauper's stack trace):

nicolaw@sansa:~$ ./test.sh cmd_ln_arg1 "cmd line arg 2" 3 four
hello world
inside woop_function() inside sourced file
subshell 1
subshell 2
test2.sh: line 14: RandomCommandThatDoesNotExist: command not found
Unexpected error executing ( RandomCommandThatDoesNotExist ) at test2.sh line 6
  test2.sh:22 woop_function 
    test2.sh:52 source 
      ./test.sh:57 my_function3 
        ./test.sh:61 my_function2 
          ./test.sh:64 my_function1 
            ./test.sh:0 main 
Unexpected error executing false at test2.sh line 19
  test2.sh:22 woop_function 
    test2.sh:52 source 
      ./test.sh:57 my_function3 
        ./test.sh:61 my_function2 
          ./test.sh:64 my_function1 
            ./test.sh:0 main 

Source Code:

#!/bin/bash
# file: test.sh
# vim:ft=bash:

shopt -s extdebug
set -uE -o pipefail

__stacktrace__() {
  declare -i frame=0 argv_offset=0
  if shopt -q extdebug ; then
    while : ; do
      declare -a caller_info=( $(caller $frame) ) argv=('')
      declare -i argc=0 frame_argc=0
      [[ $frame -lt ${#BASH_ARGC[@]} ]] && frame_argc=${BASH_ARGC[frame]}
      for ((frame_argc--,argc=0; frame_argc >= 0; argc++,frame_argc--)); do
        argv[argc]=${BASH_ARGV[argv_offset + frame_argc]}
        case "${argv[argc]}" in
          *[[:space:]]*) argv[argc]="'${argv[argc]}'" ;;
        esac
      done
      if [[ ${#caller_info[@]} -gt 0 ]] ; then
        printf "%$(( frame * 2 ))s%s:%s %s:: %s %s\n" \
          "" "${caller_info[2]}" "${caller_info[0]}" "${caller_info[1]}" \
          "${FUNCNAME[frame]}" "${argv[*]}"
      else
        printf "%$(( frame * 2 ))s%s %s %s %s\n" \
          "" "$BASH" "-$-" "$0" "${argv[*]}"
        break
      fi
      argv_offset=$((argv_offset + ${BASH_ARGC[frame]}))
      frame=$((frame+1))
    done
  else
    # Pauper's stack trace without extdebug's provision of BASH_ARG*
    for fn in "${FUNCNAME[@]}"; do
      (($frame)) && printf "%$(( frame * 2 ))s%s:%s %s %s\n" \
        "" "${BASH_SOURCE[$frame]}" "${BASH_LINENO[$frame]}" "$fn"
      frame=$((frame+1))
    done
  fi
}

trap '>&2 echo "Unexpected error executing $BASH_COMMAND at ${BASH_SOURCE[0]} line $LINENO"; __stacktrace__ >&2; exit' ERR

exit() {
    trap - ERR
    command exit "$@"
}

my_function3() {
    source test2.sh
    false
}

my_function2() {
    my_function3 "hello world" "$2"
}

my_function1() {
    my_function2 b1 b2 "$@" b3
}

my_function1 f1 f2
# file: test2.sh
# vim:ft=bash:

echo "hello world"

function woop_function() {
  echo "inside woop_function() inside sourced file"

  (
    echo "subshell 1"
    (
      echo "subshell 2"
      (
        RandomCommandThatDoesNotExist
      )
    )
  )

  false
}

woop_function my arguments "are awesome"