#! /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 > # # 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 # 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=${0##*/} 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 __steptrap_calls=0 shift shopt -s extglob # turn on extglob so we can parse the debugger funcs function _steptrap { local i=0 _curline=$1 if (( ++__steptrap_calls > 1 && $_curline == 1 )); then return fi 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 )); 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 _x if [ -z "$1" ]; then _listbp return fi eval "$_seteglob" if [[ $1 == *(\+)[1-9]*([0-9]) ]]; then case $1 in +*) # normalize argument, then double it (+2 -> +2 + 2 = 4) _x=${1##*([!1-9])} # cut off non-numeric prefix _x=${x%%*([!0-9])} # cut off non-numeric suffix f=$(( $1 + $_x )) ;; *) f=$(( $1 )) ;; esac # find the next valid line line="${_lines[$f]}" while _invalidbreakp $f do (( f++ )) line="${_lines[$f]}" done if (( $f != $1 )) then _msg "Line $1 is not a valid breakpoint" fi if [ -n "${_lines[$f]}" ]; then _linebp[$1]=$1; _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 eval "$_resteglob" } 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 [yY]*) unset _linebp[*] _msg "All breakpoints have been cleared" ;; esac return 0 fi eval "$_seteglob" if [[ $1 == [1-9]*([0-9]) ]]; then unset _linebp[$1] _msg "Breakpoint cleared at line $1" else _msg "Please specify a numeric line number" fi eval "$_resteglob" } function _setbc { if (( $# > 0 )); then _brcond=$@ _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 [Yy]*) unset _disps[*] _msg "All breakpoints have been cleared" ;; esac return 0 fi eval "$_seteglob" if [[ $1 == [1-9]*([0-9]) ]]; then unset _disps[$1] _msg "Display $i has been cleared" else _listdisp _msg "Please specify a numeric display number" fi eval "$_resteglob" } # usage _ftrace -u funcname [funcname...] function _ftrace { local _opt=-t _tmsg="enabled" _func if [[ $1 == -u ]]; then _opt=+t _tmsg="disabled" shift fi for _func; do declare -f $_opt $_func _msg "Tracing $_tmsg for function $_func" done } function _cmdloop { local cmd args while read -e -p "bashdb> " cmd args; do test -n "$cmd" && history -s "$cmd $args" # save on history list 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" ;; f|ft|ftr|ftra|ftrace) _ftrace $args _lastcmd="ftrace $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 { [[ -n ${_linebp[$_curline]} ]] } function _invalidbreakp { local line=${_lines[$1]} # XXX - should use shell patterns 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 # list 5 lines on either side of $1 if [ $1 = "%" ]; then let start=1 let end=${#_lines[@]} else let start=$1-5 let end=$1+5 fi elif (( $# > 1 )); then # list between start and end if [ $1 = "^" ]; then let start=1 else let start=$1 fi if [ $2 = "\$" ]; then let end=${#_lines[@]} else let end=$2 fi else # list 5 lines on either side of current line let start=$_curline-5 let end=$_curline+5 fi # normalize start and end if (( $start < 1 )); then start=1 fi if (( $end > ${#_lines[@]} )); then end=${#_lines[@]} fi 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 start=$(( $end + 1 )) 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 cl=' ' if [[ -n ${_linebp[$line]} ]]; then bp='*' fi if (( $_curline == $line )); then 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 ftrace [-u] func make the debugger step into function FUNC (-u turns off tracing FUNC) help print this menu ! string passes string to a shell quit quit' } shopt -u extglob HISTFILE=~/.bashdb_history set -o history set +H # strings to save and restore the setting of `extglob' in debugger functions # that need it _seteglob='local __eopt=-u ; shopt -q extglob && __eopt=-s ; shopt -s extglob' _resteglob='shopt $__eopt extglob' _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=1 LINENO=-1 trap '_steptrap $LINENO' DEBUG