#! /bin/bash
# bashdb - Bash shell debugger
#
# Adapted from an idea in O'Reilly's `Learning the Korn Shell'
# Copyright (C) 1993-1994 O'Reilly and Associates, Inc.
# Copyright (C) 1998, 1999, 2001 Gary V. Vaughan <gvv@techie.com>>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# As a special exception to the GNU General Public License, if you
# distribute this file as part of a program that contains a
# configuration script generated by Autoconf, you may include it under
# the same distribution terms that you use for the rest of that program.

# NOTE:
#
# This program requires bash 2.x.
# If bash 2.x is installed as "bash2", you can invoke  bashdb like this:
#
#   DEBUG_SHELL=/bin/bash2 /bin/bash2 bashdb script.sh

# TODO:
#
# break [regexp]
# cond [break] [condition]
# tbreak [regexp|+lines]
# restart
# Variable watchpoints
# Output colourization
# History with csh ^ substitution? Or write a readline frontend?
# Instrument shell functions with the _steptrap in $_potbelliedpig
# Instrument `source' and `.' files in $_potbelliedpig
# be cleverer about lines we allow breakpoints to be set on
# break [function_name]


echo 'Bash Debugger version 1.2.4'

export _dbname=$(echo "X$0"|sed -e 's,^X,,' -e 's,^.*/,,')

if test $# -lt 1; then
  echo "$_dbname: Usage: $_dbname <filename>" >&2
  exit 1
fi

_guineapig=$1

if test ! -r $1; then
  echo "$_dbname: Cannot read file '$_guineapig'." >&2
  exit 1
fi

shift

__debug=${TMPDIR-/tmp}/bashdb.$$
sed -e '/^# bashdb - Bash shell debugger/,/^# -- DO NOT DELETE THIS LINE -- /d' "$0" > $__debug
cat $_guineapig >> $__debug
exec ${DEBUG_SHELL-bash} $__debug $_guineapig "$@"

exit 1

# -- DO NOT DELETE THIS LINE -- The program depends on it

#bashdb preamble
# $1 name of the original guinea pig script

__debug=$0
_guineapig=$1

shift

function _steptrap
{
  local i=0
  
  _curline=$1

  if [ -n "$_disps" ]
  then
    while (( $i < ${#_disps[@]} ))
    do
      if [ -n "${_disps[$i]}" ]
      then
        _msg "${_disps[$i]}: \c"
        eval _msg ${_disps[$i]}
      fi
      let i=$i+1
    done
  fi

  if (( $_trace )); then
    _showline $_curline
  fi
  
  if (( $_steps >= 0 )); then
    let _steps="$_steps - 1"
  fi

  if _at_linenumbp ; then
    _msg "Reached breakpoint at line $_curline"
    _showline $_curline
    _cmdloop
  elif [ -n "$_brcond" ] && eval $_brcond; then
    _msg "Break condition $_brcond true at line $_curline"
    _showline $_curline
    _cmdloop
  elif (( $_steps == 0 && $_curline > 1)); then
    # Assuming a real script will have the "#! /bin/sh" at line 1,
    # assume that when $_curline == 1 we are inside backticks.
    if (( ! $_trace )); then
      _msg "Stopped at line $_curline"
      _showline $_curline
    fi
    _cmdloop
  fi
}

function _setbp
{
  local i f line

  if [ -z "$1" ]
  then
    _listbp
  elif [ $(echo $1 | grep '^\+*[1-9][0-9]*') ]
  then
    case $1 in
    +*)
      let f="$1 + `expr $1 : '+*\([1-9][0-9]*\)'`"
      ;;
    *)
      let f=$1
      ;;
    esac

    # find the next valid line
    line="${_lines[$f]}"
    while _invalidbreakp $f
    do
      let f="$f + 1"
      line="${_lines[$f]}"
    done

    if (( $f != $1 ))
    then
      _msg "Line $1 is not a valid breakpoint"
    fi

    if [ -n "${_lines[$f]}" ]
    then
      _linebp=($(echo $( (for i in ${_linebp[*]} $1; do
      echo $i; done) | sort -n) ))
      _msg "Breakpoint set at line $f"
    else
      _msg "Breakpoints can only be set on executable lines"
    fi
  else
    _msg "Please specify a numeric line number"
  fi
}

function _listbp
{
  local i
  
  if [ -n "$_linebp" ]
  then
    _msg "Breakpoints:"
    for i in ${_linebp[*]}; do
      _showline $i
    done
  else
    _msg "No breakpoints have been set"
  fi
}

function _clearbp
{
  local i
  if [ -z "$1" ]; then
    read -e -p "Delete all breakpoints? "
    case $REPLY in
    y*)
      unset _linebp[*]
      _msg "All breakpoints have been cleared"
      ;;
    esac
  elif [ $(echo $1 | grep '^[0-9]*') ]; then
    _linebp=($(echo $(for i in ${_linebp[*]}; do
              if (( $1 != $i )); then echo $1;  fi; done) ))
    _msg "Breakpoint cleared at line $1"
  else
    _msg "Please specify a numeric line number"
  fi
}

function _setbc
{
  if [ -n "$*" ]
  then
    _brcond=$args
    _msg "Break when true: $_brcond"
  else
    _brcond=
    _msg "Break condition cleared"
  fi
}

function _setdisp
{
  if [ -z "$1" ]
  then
    _listdisp
  else
    _disps[${#_disps[@]}]="$1"
    if (( ${#_disps[@]} < 10 ))
    then
      _msg " ${#_disps[@]}: $1"
    else
      _msg "${#_disps[@]}: $1"
    fi
  fi
}

function _listdisp
{
  local i=0 j
  
  if [ -n "$_disps" ]
  then
    while (( $i < ${#_disps[@]} ))
    do
      let j=$i+1
    if (( ${#_disps[@]} < 10 ))
    then
      _msg " $j: ${_disps[$i]}"
    else
      _msg "$j: ${_disps[$i]}"
    fi
      let i=$j
    done
  else
    _msg "No displays have been set"
  fi
}

function _cleardisp
{
  if (( $# < 1 ))
  then
    read -e -p "Delete all display expressions? "
    case $REPLY in
    y*)
      unset _disps[*]
      _msg "All breakpoints have been cleared"
      ;;
    esac
  elif [ $(echo $1 | grep '^[0-9]*') ]
  then
    unset _disps[$1]
    _msg "Display $i has been cleared"
  else
    _listdisp
    _msg "Please specify a numeric display number"
  fi
}   

function _cmdloop
{
  local cmd args

  while read -e -p "bashdb> " cmd args; do
    test -n "$cmd" || { set $_lastcmd; cmd=$1; shift; args=$*; }
    if [ -n "$cmd" ]
    then
      case $cmd in
	b|br|bre|brea|break)
	  _setbp $args
	  _lastcmd="break $args"
	  ;;
	co|con)
	  _msg "ambiguous command: '$cmd', condition, continue?"
	  ;;
	cond|condi|condit|conditi|conditio|condition)
	  _setbc $args
	  _lastcmd="condition $args"
	  ;;
	c|cont|conti|contin|continu|continue)
	  _lastcmd="continue"
	  return
	  ;;
	d)
	  _msg "ambiguous command: '$cmd', delete, display?"
	  ;;
	de|del|dele|delet|delete)
	  _clearbp $args
	  _lastcmd="delete $args"
	  ;;
	di|dis|disp|displ|displa|display)
	  _setdisp $args
	  _lastcmd="display $args"
	  ;;
	\?|h|he|hel|help)
	  _menu
	  _lastcmd="help"
	  ;;
	l|li|lis|list)
	  _displayscript $args
	  # _lastcmd is set in the _displayscript function
	  ;;
	p|pr|pri|prin|print)
	  _examine $args
	  _lastcmd="print $args"
	  ;;
	q|qu|qui|quit)
	  exit
	  ;;
	s|st|ste|step|n|ne|nex|next)
	  let _steps=${args:-1}
	  _lastcmd="next $args"
	  return
	  ;;
	t|tr|tra|trac|trace)
	  _xtrace
	  ;;
	u|un|und|undi|undis|undisp|undispl|undispla|undisplay)
	  _cleardisp $args
	  _lastcmd="undisplay $args"
	  ;;
	!*)
	  eval ${cmd#!} $args
	  _lastcmd="$cmd $args"
	  ;;
	*)
	  _msg "Invalid command: '$cmd'"
	  ;;
      esac
    fi
  done
}

function _at_linenumbp
{
  local i=0

  if [ "$_linebp" ]
  then
    while (( $i < ${#_linebp[@]} )); do
      if (( ${_linebp[$i]} == $_curline )); then
	return 0
      fi
      let i=$i+1
    done
  fi

  return 1
}

function _invalidbreakp
{
  local line=${_lines[$1]}

  if test -z "$line" \
      || expr "$line" : '[ \t]*#.*' > /dev/null \
      || expr "$line" : '[ \t]*;;[ \t]*$' > /dev/null \
      || expr "$line" : '[ \t]*[^)]*)[ \t]*$' > /dev/null \
      || expr "$line" : '[ \t]*;;[ \t]*#.**$' > /dev/null \
      || expr "$line" : '[ \t]*[^)]*)[ \t]*;;[ \t]*$' > /dev/null \
      || expr "$line" : '[ \t]*[^)]*)[ \t]*;;*[ \t]*#.*$' > /dev/null
  then
    return 0
  fi

  return 1
}

function _examine
{
  if [ -n "$*" ]
  then
    _msg "$args: \c"
    eval _msg $args
  else
    _msg "Nothing to print"
  fi
}

function _displayscript
{
  local i j start end bp cl

  if (( $# == 1 ))
  then
    if test $1 = "%"
    then
      let start=1
      let end=${#_lines[@]}
    else
      let start=$1-5
      let end=$1+5
    fi
  elif (( $# > 1 ))
  then
    if test $1 = "^"
    then
      let start=1
    else
      let start=$1
    fi

    if test $2 = "\$"
    then
      let end=${#_lines[@]}
    else
      let end=$2
    fi
  else
    let start=$_curline-5
    let end=$_curline+5
  fi

  if (( $start < 1 ))
  then
    start=1
  fi
  if (( $end > ${#_lines[@]} ))
  then
    end=${#_lines[@]}
  fi

  let cl=$end-$start
  if (( $cl > ${LINES-24} ))
  then
    pager=${PAGER-more}
  else
    pager=cat
  fi
  
  i=$start
  ( while (( $i <= $end )); do
      _showline $i
      let i=$i+1
    done ) 2>&1 | $pager

  # calculate the next block of lines
  let start=$end+1
  let end=$start+11
  if (( $end > ${#_lines[@]} ))
  then
    end=${#_lines[@]}
  fi

  _lastcmd="list $start $end"
}

function _xtrace
{
  let _trace="! $_trace"
  if (( $_trace )); then
    _msg "Execution trace on"
  else
    _msg "Execution trace off"
  fi
}
	
function _msg
{
  echo -e "$@" >&2
}

function _showline
{
  local i=0 bp=' ' line=$1
   
  while (( $i < ${#_linebp[@]} ))
  do
    if [ ${_linebp[$i]} ] && (( ${_linebp[$i]} == $line ))
    then
      bp='*'
    fi
    let i=$i+1
  done

  if  (( $_curline == $line )); then
    cl=">"
  else
    cl=" "
  fi

  if (( $line < 100 )); then
    _msg "$_guineapig:$line   $bp $cl${_lines[$line]}"
  elif (( $line < 10 )); then
    _msg "$_guineapig:$line  $bp $cl${_lines[$line]}"
  elif (( $line > 0 )); then
    _msg "$_guineapig:$line $bp $cl${_lines[$line]}"
  fi
}

function _cleanup
{
  rm -f $__debug $_potbelliedpig 2> /dev/null
}

function _menu
{
  _msg 'bashdb commands:
	break N		set breakpoint at line N
	break		list breakpoints & break condition
	condition foo	set break condition to foo
	condition	clear break condition
	delete N	clear breakpoint at line N
	delete		clear all breakpoints
	display EXP	evaluate and display EXP for each debug step
	display		show a list of display expressions
	undisplay N	remove display expression N
	list N M        display all lines of script between N and M
	list N          display 5 lines of script either side of line N
	list		display 5 lines if script either side of current line
	continue	continue execution upto next breakpoint
	next [N]	execute [N] statements (default 1)
	print expr	prints the value of an expression
	trace		toggle execution trace on/off
	help		print this menu
	! string	passes string to a shell
	quit		quit'
}


_linebp=
let _trace=0
let _i=1

# Be careful about quoted newlines
_potbelliedpig=${TMPDIR-/tmp}/$_guineapig.$$
sed 's,\\$,\\\\,' $_guineapig > $_potbelliedpig

_msg "Reading source from file: $_guineapig"
while read; do
  _lines[$_i]=$REPLY
  let _i=$_i+1
done < $_potbelliedpig

trap _cleanup EXIT
# Assuming a real script will have the "#! /bin/sh" at line 1,
# don't stop at line 1 on the first run
let _steps=2
LINENO=-2
trap '_steptrap $LINENO' DEBUG
:
