581 lines
12 KiB
Bash
Executable file
581 lines
12 KiB
Bash
Executable file
#! /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
|
|
# 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
|