Exercises

We'll conclude this chapter with some suggested enhancements to our simple debugger and a complete listing of the debugger command source code.

 

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

 
  1. Improve command error handling in these ways:
    1. Check that the arguments to s are valid numbers and print an appropriate error message if they aren't.
    2. Check that a breakpoint actually exists before clearing it and warn the user if the line doesn't have a breakpoint.
    3. Any other error handling that you can think of.
  2. Add code to remove duplicate breakpoints (more than one breakpoint on one line).
  3. Enhance the cb command so that the user can specify more than one breakpoint to be cleared at a time.
  4. Implement an option that causes a break into the debugger whenever a command exits with non-zero status:
    1. Implement it as the command-line option -e.
    2. Implement it as the debugger command e to toggle it on and off. (Hint: when you enter _steptrap, $? is still the exit status of the last command that ran.)
  5. Implement a command that prints out the status of the debugger: whether execution trace is on/off, error exit is on/off, and the number of the last line to be executed. In addition, move the functionality for displaying the breakpoints from bp to the new option.
  6. Add support for multiple break conditions, so that bashdb stops execution whenever one of them becomes true and prints a message indicating which one became true. Do this by storing the break conditions in an array. Try to make this as efficient as possible, since the checking will take place after every statement.
  7. Add the ability to watch variables.
    1. Add a command aw that takes a variable name as an argument and adds it to a list of variables to watch. Any watched variables are printed out when execution trace is toggled on.
    2. Add another command cw that, without an argument, removes all of the variables from the watch list. With an argument, it removes the specified variable.
  8. Although placing an underscore at the start of the debugger identifiers will avoid name clashes in most cases, think of ways to automatically detect name clashes with the guinea pig script and how to get around this problem. (Hint: you could rename the clashing names in the guinea pig script at the point where it gets combined with the preamble and placed in the temporary file.)
  9. Add any other features you can think of.

Finally, here is a complete source listing of the debugger function file bashdb.fns:

# After each line of the test script is executed the shell traps to
# this function.
    
function _steptrap
{
    _curline=$1        # the number of the line that just ran
    
    (( $_trace )) && _msg "$PS4 line $_curline: ${_lines[$_curline]}"
  
    if (( $_steps >= 0 )); then
        let _steps="$_steps - 1"
    fi
    
    # First check to see if a line number breakpoint was reached.
    # If it was, then enter the debugger.
    if _at_linenumbp ; then
        _msg "Reached breakpoint at line $_curline"
        _cmdloop
    
    # It wasn't, so check whether a break condition exists and is true.
    # If it is, then enter the debugger
    elif [ -n "$_brcond" ] && eval $_brcond; then
        _msg "Break condition $_brcond true at line $_curline"
        _cmdloop
    
    # It wasn't, so check if we are in step mode and the number of
    # steps is up. If it is, then enter the debugger.
    elif (( $_steps == 0 )); then
        _msg "Stopped at line $_curline"
        _cmdloop
    
    fi
}
    
# The Debugger Command Loop
    
function _cmdloop {
  local cmd args
    
  while read -e -p "bashdb> " cmd args; do
    case $cmd in
      \? | h  ) _menu ;;          # print command menu
      bc ) _setbc $args ;;        # set a break condition
      bp ) _setbp $args ;;        # set a breakpoint at the given line
      cb ) _clearbp $args ;;      # clear one or all breakpoints
      ds ) _displayscript ;;      # list the script and show the
                                  # breakpoints
      g  ) return ;;              # "go": start/resume execution of
                                  # the script
      q  ) exit ;;                # quit
      s  ) let _steps=${args:-1}  # single step N times (default = 1)
           return ;;
      x  ) _xtrace ;;             # toggle execution trace
     * ) eval ${cmd#!} $args ;; # pass to the shell
      *  ) _msg "Invalid command: '$cmd'" ;;
    esac
  done
}
    
    
# See if this line number has a breakpoint
function _at_linenumbp
{
    local i=0
    
    # Loop through the breakpoints array and check to see if any of
    # them match the current line number. If they do return true (0)
    # otherwise return false.
    
    if [ "$_linebp" ]; then
        while (( $i < ${#_linebp[@]} )); do
            if (( ${_linebp[$i]} == $_curline )); then
                return 0
            fi
            let i=$i+1
        done
    fi
    return 1
}
    
    
# Set a breakpoint at the given line number or list breakpoints
function _setbp
{
    local i
    
    # If there are no arguments call the breakpoint list function.
    # Otherwise check to see if the argument was a positive number.
    # If it wasn't then print an error message. If it was then check
    # to see if the line number contains text. If it doesn't then
    # print an error message. If it does then echo the current
    # breakpoints and the new addition and pipe them to "sort" and
    # assign the result back to the list of breakpoints. This results
    # in keeping the breakpoints in numerical sorted order.
    
    # Note that we can remove duplicate breakpoints here by using
    # the -u option to sort which uniquifies the list.
    
    if [ -z "$1" ]; then
        _listbp
    elif [ $(echo $1 | grep '^[0-9]*')  ]; then
        if [ -n "${_lines[$1]}" ]; then
            _linebp=($(echo $( (for i in ${_linebp[*]} $1; do
                      echo $i; done) | sort -n) ))
            _msg "Breakpoint set at line $1"
        else
            _msg "Breakpoints can only be set on non-blank lines"
        fi
    else
        _msg "Please specify a numeric line number"
    fi
}
    
    
# List breakpoints and break conditions
function _listbp
{
    if [ -n "$_linebp" ]; then
        _msg "Breakpoints at lines: ${_linebp[*]}"
    else
        _msg "No breakpoints have been set"
    fi
    
    _msg "Break on condition:"
    _msg "$_brcond"
}
    
# Clear individual or all breakpoints
function _clearbp
{
    local i bps
    
    # If there are no arguments, then delete all the breakpoints.
    # Otherwise, check to see if the argument was a positive number.
    # If it wasn't, then print an error message. If it was, then
    # echo all of the current breakpoints except the passed one
    # and assign them to a local variable. (We need to do this because
    # assigning them back to _linebp would keep the array at the same
    # size and just move the values "back" one place, resulting in a
    # duplicate value). Then destroy the old array and assign the
    # elements of the local array, so we effectively recreate it,
    # minus the passed breakpoint.
    
    if [ -z "$1" ]; then
        unset _linebp[*]
        _msg "All breakpoints have been cleared"
    elif [ $(echo $1 | grep '^[0-9]*')  ]; then
          bps=($(echo $(for i in ${_linebp[*]}; do
                if (( $1 != $i )); then echo $i; fi; done) ))
          unset _linebp[*]
          _linebp=(${bps[*]})
          _msg "Breakpoint cleared at line $1"
    else
        _msg "Please specify a numeric line number"
    fi
}
    
    
# Set or clear a break condition
function _setbc
{
    if [ -n "$*" ]; then
        _brcond=$args
        _msg "Break when true: $_brcond"
    else
        _brcond=
        _msg "Break condition cleared"
    fi
}
    
    
# Print out the shell script and mark the location of breakpoints
# and the current line
    
function _displayscript
{
    local i=1 j=0 bp cl
    
    ( while (( $i < ${#_lines[@]} )); do
          if [ ${_linebp[$j]} ] && (( ${_linebp[$j]} == $i )); then
              bp='*'
              let j=$j+1
          else
              bp=' '
          fi
          if (( $_curline == $i )); then
              cl=">"
          else
              cl=" "
          fi
          echo "$i:$bp $cl  ${_lines[$i]}"
          let i=$i+1
      done
    ) | more
}
    
    
# Toggle execution trace on/off
function _xtrace
{
    let _trace="! $_trace"
    _msg "Execution trace "
    if (( $_trace )); then
        _msg "on"
    else
        _msg "off"
    fi
}
    
    
# Print the passed arguments to Standard Error
function _msg
{
    echo -e "$@" >&2
}
    
    
# Print command menu
function _menu {
    _msg 'bashdb commands:
         bp N                set breakpoint at line N
         bp                  list breakpoints and break condition
         bc string           set break condition to string
         bc                  clear break condition
         cb N                clear breakpoint at line N
         cb                  clear all breakpoints
         ds                  displays the test script and breakpoints
         g                   start/resume execution
         s [N]               execute N statements (default 1)
         x                   toggle execution trace on/off
         h, ?                print this menu
         ! string            passes string to a shell
         q                   quit'
}
    
    
# Erase the temporary file before exiting
function _cleanup
{
    rm $_debugfile 2>/dev/null
}

 


[10] Unfortunately, the debugger will not work with versions of bash prior to 2.0, because they do not implement the DEBUG signal.

[11] All function names and variables (except those local to functions) in bashdb have names beginning with an underscore (_), to minimize the possibility of clashes with names in the guinea pig script.

[12] exec can also be used with an I/O redirector only; this makes the redirector take effect for the remainder of the script or login session. For example, the line exec 2>errlog at the top of a script directs standard error to the file errlog for the rest of the script.

[13] If you are typing or scanning in the preamble code from this book, make sure that the last line in the file is the call to set the trap, i.e., no blank lines should appear after the call to trap.

[14] There is nothing to stop you from changing the commands to something you find easier to remember. There is no "official" bash debugger, so feel free to change the debugger to suit your needs.

[15] This isn't a complete solution. Certain other lines (e.g., comments) will also be ignored by the DEBUG trap. See the list of limitations and the exercises at the end of this chapter.

[16] bash versions 2.01 and earlier have a bug in assigning arrays to themselves that prevents the code for setbp and clearbp from working. In each case, you can get around this bug by assigning _linebp to a local variable first, unsetting it, and then assigning the local variable back to it. Better yet, update to a more recent version of bash.

[17] Bear in mind that if your break condition sends anything to standard output or standard error, you will see it after every statement executed. Also, make sure your break condition doesn't take a long time to run; otherwise your script will run very, very slowly.