docwhat's avatardocwhat's blog

Tracebacks in bash

I don’t like to write programs in bash. It’s not a very pretty language. But it has one advantage over a lot of other languages:

It’s on your system. Every Unix-like system has /bin/bash; Redhat, Ubuntu, and even OS X.

But bash is still a lousy language.

This is where bash tracebacks come in…

“Whaaaaa? Bash has tracebacks?” I can hear you ask.

Yup, it can.

Check out the gist below. It is both a demonstration of the traceback as well as a template; grab the bits between the “cut here” and paste it into your own program.

#!/bin/bash
#
# Tracebacks in bash
# https://docwhat.org/tracebacks-in-bash/
#
# Just take the code between the "cut here" lines
# and put it in your own program.
#
# Written by Christian Höltje
# Donated to the public domain in 2013

#--------->8---------cut here---------8<---------
set -eu

trap _exit_trap EXIT
trap _err_trap ERR
_showed_traceback=f

function _exit_trap() {
  local _ec="$?"
  if [[ $_ec != 0 && ${_showed_traceback} != t ]]; then
    traceback 1
  fi
}

function _err_trap() {
  local _ec="$?"
  local _cmd="${BASH_COMMAND:-unknown}"
  traceback 1
  _showed_traceback=t
  echo "The command ${_cmd} exited with exit code ${_ec}." 1>&2
}

function traceback() {
  # Hide the traceback() call.
  local -i start=$((${1:-0} + 1))
  local -i end=${#BASH_SOURCE[@]}
  local -i i=0
  local -i j=0

  echo "Traceback (last called is first):" 1>&2
  for ((i = start; i < end; i++)); do
    j=$((i - 1))
    local function="${FUNCNAME[$i]}"
    local file="${BASH_SOURCE[$i]}"
    local line="${BASH_LINENO[$j]}"
    echo "     ${function}() in ${file}:${line}" 1>&2
  done
}
#--------->8---------cut here---------8<---------

########
## Demos

function bomb() {
  trap _err_trap ERR
  local limit=${1:-5}
  echo -n " ${limit}"
  if [ "${limit}" -le 0 ]; then
    echo " BOOM"
    return 10
  else
    bomb $((limit - 1))
  fi
}

function stack() {
  stack_1
}
function stack_1() {
  stack_2
}
function stack_2() {
  stack_3
}
function stack_3() {
  no_such_function
}

#######
## Main

case "${1:-}" in
stack)
  stack
  ;;
bomb)
  echo -n "Counting down..."
  bomb
  ;;
badvar)
  echo "This shouldn't be shown because \${bad_variable} isn't set"
  ;;
false)
  false
  ;;
true)
  true
  ;;
*)
  echo "Usage: $0 [bomb|badvar|true|false|stack]"
  ;;
esac

# EOF

The gist (pun intended) of it that it traps ERR and EXIT interrupts in the shell. It then walks the FUNCNAME, BASH_SOURCE, and BASH_LINENO arrays to show where the callers were.

There is a little extra bits to ensure the traceback function itself doesn’t appear in the output and to format it nicely.

Not only are the tracebacks useful, but they make using set -eu much less painful. And you are using set -eu in your bash programs, right? Right?

I hope it is useful. If you have suggestions or questions, just ask!

Ciao!

Edit on GitHub