docwhat's avatardocwhat's blog

Tracebacks in bash

last updated

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!


docwhat

The personal blog of Christian Höltje.
docwhat docwhat contact