A weekend experiment with a Tk GUI for gdb

Arjen Markus (4 october 2010) I thought I'd give it a try - a Tcl/Tk GUI for the GNU debugger (gdb). Just to see what it takes. Well, it turned out to be both simpler and more complex than I thought at first:

  • You need Expect to control gdb - simply a pipe won't do it.
  • gdb produces different output depending on the command you gave - you need to match all of them
  • Making the user-interface intuitive enough will require hiding some peculiarities of gdb, like that you need the run command to start the program.

The script below is far from complete and I would not even claim it to be useful, but it was a nice experiment anyway.

(BTW, I was unaware of Insight, which is another Tcl/Tk GUI for gdb - probably much more capable than the one below)


# guigdb.tcl --
#     Attempt to create a GUI for the GNU debugger
#
#     (just a first experiment!)
#
#     Note:
#     We need Expect because gdb takes over the standard
#     input.
#
package require Expect

# findSourceFile --
#     Find the name of the source file
#
# Arguments:
#     None
#
# Returns:
#     Nothing
#
# Side effects:
#     Show the source file
#
proc findSourceFile {} {

    expect -re {\(gdb\)} {
        if { [regexp {file (.+), line ([0-9]+)} $expect_out(buffer) ==> filename lineno] } {
            showCode $filename $lineno
        }
    }
}

# findLineno --
#     Find the current line number and highlight
#
# Arguments:
#     None
#
# Returns:
#     Nothing
#
# Side effects:
#     Show the current line highlighted
#
proc findLineno {} {

    global currentFile

    expect -re {\(gdb\)} {
        if { [regexp {Breakpoint [0-9]+.*:([0-9]+)} $expect_out(buffer) ==> lineno] } {
            showCode $currentFile $lineno
        } elseif { [regexp {\(\) at (.*):([0-9]+)} $expect_out(buffer) ==> filename lineno] } {
            showCode $filename $lineno
        } elseif { [regexp {^[^0-9]*([0-9]+)} $expect_out(buffer) ==> lineno] } {
            showCode $currentFile $lineno
        }
    }
}

# showCode --
#     Show the source file
#
# Arguments:
#     filename        Name of the file to show
#     lineno          Line number to highlight
#
# Returns:
#     Nothing
#
# Side effects:
#     Show the complete file
#
proc showCode {filename lineno} {
    global codeWindow
    global currentFile
    global currentLine

    if { $filename != $currentFile } {
        $codeWindow delete 1.0 end

        set infile [open $filename]

        $codeWindow insert 1.0 [read $infile]

        close $infile

        set currentLine ""
    }

    if { $currentLine != "" } {
        $codeWindow tag remove highlight $currentLine.0 [expr {$currentLine+1}].0
    }
    set currentLine $lineno
    $codeWindow tag add highlight $currentLine.0 [expr {$currentLine+1}].0
    $codeWindow see $currentLine.0
}

# setupWindow --
#     Set up the main window
#
# Arguments:
#     None
#
# Returns:
#     Nothing
#
# Side effects:
#     The main window is filled with a menu bar
#     and two text widgets
#
proc setupWindow {} {

    global codeWindow
    global commandWindow

    set codeWindow    [text      .codeWindow    -yscrollcommand [list .yscroll set] -height 30 -xscrollcommand [list .xscroll set]]
    set yscroll       [scrollbar .yscroll       -command [list .codeWindow yview]]
    set xscroll       [scrollbar .xscroll       -command [list .codeWindow xview] -orient horizontal]

    set commandWindow [text      .commandWindow -yscrollcommand [list .cscroll set] -height 10]
    set cscroll       [scrollbar .cscroll       -command [list .commandWindow yview]]

    set commandFrame  [frame     .frame]

    set command        [entry     .frame.command -textvariable ::cmd]
    set commandButton  [button    .frame.send     -text "Command"   -command runCommand -width 10]
    set stepButton     [button    .frame.step     -text "Step"      -command {runCommand s} -width 10]
    set stepOverButton [button    .frame.stepover -text "Step over" -command {runCommand n} -width 10]
    set continueButton [button    .frame.continue -text "Continue"  -command {runCommand c} -width 10]

    grid $codeWindow    $yscroll       -sticky news
    grid $xscroll                      -sticky news
    grid $commandWindow $cscroll       -sticky news
    grid $commandFrame  -              -sticky news
    grid $command       $commandButton $stepButton $stepOverButton $continueButton -sticky news

    $codeWindow tag configure highlight -background lightblue
}

# runGdb --
#     Start the debugger for the specified program
#
# Arguments:
#     exename          Name of the executable to run
#
# Returns:
#     Nothing
#
proc runGdb {exename} {

    global spawn_id  ;# Really required by Expect!

    exp_spawn gdb $exename
    showResult

    exp_send "break main\r"
    findSourceFile

    #console show
    showResult
}

# showResult --
#     Read the output from the debugger and show it
#
# Arguments:
#     None
#
# Returns:
#     Nothing
#
# Side effects:
#     Show the output in the output window
#
proc showResult {} {

    global commandWindow

    expect -re {\(gdb\)} {
        $commandWindow insert end "$expect_out(buffer)\n"
        $commandWindow see end
    } timeout {
        $commandWindow insert end "Timeout!"
        $commandWindow see end
    }
    #console show
    #parray ::expect_out
}

# runCommand --
#     Send a command to the debugger
#
# Arguments:
#     command          Command to run (optional)
#
# Returns:
#     Nothing
#
# Side effects:
#     Send the command to the debugger - whatever
#     comes back will be shown or analysed
#
#     Uses the global variable "cmd"
#
proc runCommand {{command {}}} {

    global gdb
    global spawn_id
    global cmd

    if { $command != "" } {
        set cmd $command
    }

    exp_send "$cmd\r"

    switch -- $cmd {
        "s" - "n" - "c" {
            findLineno
        }
        default {
            showResult
        }
    }

    set cmd ""
}

# main --
#     Bring up the GUI and run the debugger
#
set currentFile ""

setupWindow

runGdb [lindex $argv 1]