lisch@mentor.com (Ray Lischner) (05/03/90)
I cobbled together a perldb-mode for GNU Emacs, based on gdb-mode. Basically, you invoke perldb (M-x perldb), and you are prompted for a Perl file and additional command line arguments. The file is shown with an arrow (=>) at the beginning of the first executable line, and another window is used for interaction with the Perl debugger. A modified form of perldb.pl is required. You can set breakpoints in the file's window by moving point to the line and entering C-x SPC. In the perldb window, debugger commands can be bound to keys. By default, M-s single steps, M-n steps over subroutines, and M-c continues. The current line is always displayed, marked by an arrow. I'd rather have an X-windows perldb, but this was easier. Following are patches for perldb.pl and perldb.el. Apply the patches in the lib/ subdirectory; put perldb.el wherever you want it. Improvements are encouraged. *** /dev/null Fri Dec 1 12:08:18 1989 --- perldb.el Wed May 2 16:04:49 1990 *************** *** 0 **** --- 1,435 ---- + ;; Run perl -d under Emacs + ;; Modification by Ray Lischner (uunet!mntgfx!lisch) 2 May 1990 + ;; A modified perldb.pl is required. + ;; + ;; based on gdb.el by W. Schelter, rewritten by rms: + ;; Author: W. Schelter, University of Texas + ;; wfs@rascal.ics.utexas.edu + ;; Rewritten by rms. + + ;; Some ideas are due to Masanobu. + + ;; This file is part of GNU Emacs. + ;; Copyright (C) 1988, 1990 Free Software Foundation, Inc. + + ;; GNU Emacs is distributed in the hope that it will be useful, but + ;; WITHOUT ANY WARRANTY. No author or distributor accepts responsibility + ;; to anyone for the consequences of using it or for whether it serves + ;; any particular purpose or works at all, unless he says so in writing. + ;; Refer to the GNU Emacs General Public License for full details. + + ;; Everyone is granted permission to copy, modify and redistribute GNU + ;; Emacs, but only under the conditions described in the GNU Emacs + ;; General Public License. A copy of this license is supposed to have + ;; been given to you along with GNU Emacs so you can know your rights and + ;; responsibilities. It should be in a file named COPYING. Among other + ;; things, the copyright notice and this notice must be preserved on all + ;; copies. + + ;; Description of perldb interface: + + ;; A facility is provided for the simultaneous display of the source code + ;; in one window, while using perl -d to step through a function in the + ;; other. A small arrow in the source window, indicates the current + ;; line. + + ;; Starting up: + + ;; In order to use this facility, invoke the command perldb to obtain a + ;; shell window with the appropriate command bindings. You will be asked + ;; for the name of a file to run. Perl will be invoked on this file, in a + ;; window named *perldb-foo* if the file is foo. You will then be asked + ;; for additional command line arguments. Arguments are interpreted + ;; normally, i.e., separated by white space, except for single and + ;; double quotes. Shell and environment variables are NOT interpolated. + + ;; M-s steps by one line, and redisplays the source file and line. + + ;; You may easily create additional commands and bindings to interact + ;; with the display. For example to put the perldb command next on \M-n + ;; (def-perldb next "\M-n") + + ;; This causes the emacs command perldb-next to be defined, and runs + ;; perldb-display-frame after the command. + + ;; perldb-display-frame is the basic display function. It tries to display + ;; in the other window, the file and line corresponding to the current + ;; position in the perldb window. For example after a perldb-step, it would + ;; display the line corresponding to the position for the last step. Or + ;; if you have done a backtrace in the perldb buffer, and move the cursor + ;; into one of the frames, it would display the position corresponding to + ;; that frame. + + ;; perldb-display-frame is invoked automatically when a filename-and-line-number + ;; appears in the output. + + + (require 'shell) + + (defvar perldb-prompt-pattern "^ DB<[0-9]+> " + "A regexp to recognize the prompt for perldb.") + + (defvar perldb-file "" + "The full path name of the file being debugged by perldb.") + + (defvar perldb-mode-map nil + "Keymap for perldb-mode.") + + (if perldb-mode-map + nil + (setq perldb-mode-map (copy-keymap shell-mode-map)) + (define-key perldb-mode-map "\C-l" 'perldb-refresh)) + + (define-key ctl-x-map " " 'perldb-break) + (define-key ctl-x-map "&" 'send-perldb-command) + + ;;Of course you may use `def-perldb' with any other perldb command, including + ;;user defined ones. + + (defmacro def-perldb (name key &optional doc) + (let* ((fun (intern (format "perldb-%s" name))) + (cstr (list 'if '(not (= 1 arg)) + (list 'format "%s %s" name 'arg) + name))) + (list 'progn + (list 'defun fun '(arg) + (or doc "") + '(interactive "p") + (list 'perldb-call cstr)) + (list 'define-key 'perldb-mode-map key (list 'quote fun))))) + + (def-perldb "s" "\M-s" "Step one source line with display") + (def-perldb "n" "\M-n" "Step one source line (skip functions)") + (def-perldb "c" "\M-c" "Continue with display") + + + (defun perldb-mode () + "Major mode for interacting with an inferior Perl process. + The following commands are available: + + \\{perldb-mode-map} + + \\[perldb-display-frame] displays in the other window + the last line referred to in the perldb buffer. + + \\[perldb-s],\\[perldb-n], and \\[perldb-c] in the perldb window, + call perldb to step, next or continue and then update the other window + with the current file and position. + + If you are in a source file, you may select a point to break + at, by doing \\[perldb-break]. + + Commands: + Many commands are inherited from shell mode. + Additionally we have: + + \\[perldb-s] advance one line in program + \\[perldb-n] advance one line in program (skip over calls). + \\[send-perldb-command] used for special printing of an arg at the current point. + C-x SPACE sets break point at current line." + (interactive) + (kill-all-local-variables) + (setq major-mode 'perldb-mode) + (setq mode-name "Inferior Perl") + (setq mode-line-process '(": %s")) + (use-local-map perldb-mode-map) + (make-local-variable 'last-input-start) + (setq last-input-start (make-marker)) + (make-local-variable 'last-input-end) + (setq last-input-end (make-marker)) + (make-local-variable 'perldb-last-frame) + (setq perldb-last-frame nil) + (make-local-variable 'perldb-last-frame-displayed-p) + (setq perldb-last-frame-displayed-p t) + (make-local-variable 'perldb-delete-prompt-marker) + (setq perldb-delete-prompt-marker nil) + (make-local-variable 'perldb-filter-accumulator) + (setq perldb-filter-accumulator nil) + (make-local-variable 'shell-prompt-pattern) + (make-local-variable 'perdb-file) + (setq shell-prompt-pattern perldb-prompt-pattern) + (run-hooks 'shell-mode-hook 'perldb-mode-hook)) + + (defvar current-perldb-buffer nil) + + (defvar perldb-command-name "perl" + "Pathname for executing perl -d.") + (defun end-of-quoted-arg (argstr start end) + (let* ((chr (substring argstr start (1+ start))) + (idx (string-match (concat "[^\\]" chr) argstr (1+ start)))) + (and idx (1+ idx)) + ) + ) + + (defun parse-args-helper (arglist argstr start end) + (while (and (< start end) (string-match "[ \t\n\f\r\b]" + (substring argstr start (1+ start)))) + (setq start (1+ start))) + (cond + ((= start end) arglist) + ((string-match "[\"']" (substring argstr start (1+ start))) + (let ((next (end-of-quoted-arg argstr start end))) + (parse-args-helper (cons (substring argstr (1+ start) next) arglist) + argstr (1+ next) end))) + (t (let ((next (string-match "[ \t\n\f\b\r]" argstr start))) + (if next + (parse-args-helper (cons (substring argstr start next) arglist) + argstr (1+ next) end) + (cons (substring argstr start) arglist)))) + ) + ) + + (defun parse-args (args) + "Extract arguments from a string ARGS. + White space separates arguments, with single or double quotes + used to protect spaces. A list of strings is returned, e.g., + (parse-args \"foo bar 'two args'\") => (\"foo\" \"bar\" \"two args\")." + (nreverse (parse-args-helper '() args 0 (length args))) + ) + + (defun perldb (path args) + "Run perldb on program FILE in buffer *perldb-FILE*. + The directory containing FILE becomes the initial working directory." + (interactive "fRun perl -d on file: + sCommand line arguments: ") + (setq path (expand-file-name path)) + (let ((file (file-name-nondirectory path))) + (switch-to-buffer (concat "*perldb-" file "*")) + (setq default-directory (file-name-directory path)) + (or (bolp) (newline)) + (insert "Current directory is " default-directory "\n") + (setq perldb-file path) + (apply 'make-shell + (concat "perldb-" file) perldb-command-name nil "-d" file "-emacs" + (parse-args args)) + (perldb-mode) + (set-process-filter (get-buffer-process (current-buffer)) 'perldb-filter) + (set-process-sentinel (get-buffer-process (current-buffer)) 'perldb-sentinel) + (perldb-set-buffer))) + + (defun perldb-set-buffer () + (cond ((eq major-mode 'perldb-mode) + (setq current-perldb-buffer (current-buffer))))) + + ;; This function is responsible for inserting output from perldb + ;; into the buffer. + ;; Aside from inserting the text, it notices and deletes + ;; each filename-and-line-number; + ;; that perldb prints to identify the selected frame. + ;; It records the filename and line number, and maybe displays that file. + (defun perldb-filter (proc string) + (let ((inhibit-quit t)) + (if perldb-filter-accumulator + (perldb-filter-accumulate-marker proc + (concat perldb-filter-accumulator string)) + (perldb-filter-scan-input proc string)))) + + (defun perldb-filter-accumulate-marker (proc string) + (setq perldb-filter-accumulator nil) + (if (> (length string) 1) + (if (= (aref string 1) ?\032) + (let ((end (string-match "\n" string))) + (if end + (progn + (let* ((first-colon (string-match ":" string 2)) + (second-colon + (string-match ":" string (1+ first-colon)))) + (setq perldb-last-frame + (cons perldb-file + (string-to-int + (substring string (1+ first-colon) + second-colon))))) + (setq perldb-last-frame-displayed-p nil) + (perldb-filter-scan-input proc + (substring string (1+ end)))) + (setq perldb-filter-accumulator string))) + (perldb-filter-insert proc "\032") + (perldb-filter-scan-input proc (substring string 1))) + (setq perldb-filter-accumulator string))) + + (defun perldb-filter-scan-input (proc string) + (if (equal string "") + (setq perldb-filter-accumulator nil) + (let ((start (string-match "\032" string))) + (if start + (progn (perldb-filter-insert proc (substring string 0 start)) + (perldb-filter-accumulate-marker proc + (substring string start))) + (perldb-filter-insert proc string))))) + + (defun perldb-filter-insert (proc string) + (let ((moving (= (point) (process-mark proc))) + (output-after-point (< (point) (process-mark proc))) + (old-buffer (current-buffer)) + start) + (set-buffer (process-buffer proc)) + (unwind-protect + (save-excursion + ;; Insert the text, moving the process-marker. + (goto-char (process-mark proc)) + (setq start (point)) + (insert string) + (set-marker (process-mark proc) (point)) + (perldb-maybe-delete-prompt) + ;; Check for a filename-and-line number. + (perldb-display-frame + ;; Don't display the specified file + ;; unless (1) point is at or after the position where output appears + ;; and (2) this buffer is on the screen. + (or output-after-point + (not (get-buffer-window (current-buffer)))) + ;; Display a file only when a new filename-and-line-number appears. + t)) + (set-buffer old-buffer)) + (if moving (goto-char (process-mark proc))))) + + (defun perldb-sentinel (proc msg) + (cond ((null (buffer-name (process-buffer proc))) + ;; buffer killed + ;; Stop displaying an arrow in a source file. + (setq overlay-arrow-position nil) + (set-process-buffer proc nil)) + ((memq (process-status proc) '(signal exit)) + ;; Stop displaying an arrow in a source file. + (setq overlay-arrow-position nil) + ;; Fix the mode line. + (setq mode-line-process + (concat ": " + (symbol-name (process-status proc)))) + (let* ((obuf (current-buffer))) + ;; save-excursion isn't the right thing if + ;; process-buffer is current-buffer + (unwind-protect + (progn + ;; Write something in *compilation* and hack its mode line, + (set-buffer (process-buffer proc)) + ;; Force mode line redisplay soon + (set-buffer-modified-p (buffer-modified-p)) + (if (eobp) + (insert ?\n mode-name " " msg) + (save-excursion + (goto-char (point-max)) + (insert ?\n mode-name " " msg))) + ;; If buffer and mode line will show that the process + ;; is dead, we can delete it now. Otherwise it + ;; will stay around until M-x list-processes. + (delete-process proc)) + ;; Restore old buffer, but don't restore old point + ;; if obuf is the perldb buffer. + (set-buffer obuf)))))) + + + (defun perldb-refresh () + "Fix up a possibly garbled display, and redraw the arrow." + (interactive) + (redraw-display) + (perldb-display-frame)) + + (defun perldb-display-frame (&optional nodisplay noauto) + "Find, obey and delete the last filename-and-line marker from perldb. + The marker looks like \\032\\032LINE\\n. + Obeying it means displaying in another window the specified file and line." + (interactive) + (perldb-set-buffer) + (and perldb-last-frame (not nodisplay) + (or (not perldb-last-frame-displayed-p) (not noauto)) + (progn (perldb-display-line (car perldb-last-frame) (cdr perldb-last-frame)) + (setq perldb-last-frame-displayed-p t)))) + + ;; Make sure the file named TRUE-FILE is in a buffer that appears on the screen + ;; and that its line LINE is visible. + ;; Put the overlay-arrow on the line LINE in that buffer. + + (defun perldb-display-line (true-file line) + (let* ((buffer (find-file-noselect true-file)) + (window (display-buffer buffer t)) + (pos)) + (save-excursion + (set-buffer buffer) + (save-restriction + (widen) + (goto-line line) + (setq pos (point)) + (setq overlay-arrow-string "=>") + (or overlay-arrow-position + (setq overlay-arrow-position (make-marker))) + (set-marker overlay-arrow-position (point) (current-buffer))) + (cond ((or (< pos (point-min)) (> pos (point-max))) + (widen) + (goto-char pos)))) + (set-window-point window overlay-arrow-position))) + + (defun perldb-call (command) + "Invoke perldb COMMAND displaying source in other window." + (interactive) + (goto-char (point-max)) + (setq perldb-delete-prompt-marker (point-marker)) + (perldb-set-buffer) + (send-string (get-buffer-process current-perldb-buffer) + (concat command "\n"))) + + (defun perldb-maybe-delete-prompt () + (if (and perldb-delete-prompt-marker + (> (point-max) (marker-position perldb-delete-prompt-marker))) + (let (start) + (goto-char perldb-delete-prompt-marker) + (setq start (point)) + (beginning-of-line) + (delete-region (point) start) + (setq perldb-delete-prompt-marker nil)))) + + (defun perldb-break () + "Set perldb breakpoint at this source line." + (interactive) + (let ((file-name (file-name-nondirectory buffer-file-name)) + (line (save-restriction + (widen) + (1+ (count-lines 1 (point)))))) + (send-string (get-buffer-process current-perldb-buffer) + (concat "b " line "\n")))) + + (defun perldb-read-address() + "Return a string containing the core-address found in the buffer at point." + (save-excursion + (let ((pt (dot)) found begin) + (setq found (if (search-backward "0x" (- pt 7) t)(dot))) + (cond (found (forward-char 2)(setq result + (buffer-substring found + (progn (re-search-forward "[^0-9a-f]") + (forward-char -1) + (dot))))) + (t (setq begin (progn (re-search-backward "[^0-9]") (forward-char 1) + (dot))) + (forward-char 1) + (re-search-forward "[^0-9]") + (forward-char -1) + (buffer-substring begin (dot))))))) + + + (defvar perldb-commands nil + "List of strings or functions used by send-perldb-command. + It is for customization by you.") + + (defun send-perldb-command (arg) + + "This command reads the number where the cursor is positioned. It + then inserts this ADDR at the end of the perldb buffer. A numeric arg + selects the ARG'th member COMMAND of the list perldb-print-command. If + COMMAND is a string, (format COMMAND ADDR) is inserted, otherwise + (funcall COMMAND ADDR) is inserted. eg. \"p (rtx)%s->fld[0].rtint\" + is a possible string to be a member of perldb-commands. " + + + (interactive "P") + (let (comm addr) + (if arg (setq comm (nth arg perldb-commands))) + (setq addr (perldb-read-address)) + (if (eq (current-buffer) current-perldb-buffer) + (set-mark (point))) + (cond (comm + (setq comm + (if (stringp comm) (format comm addr) (funcall comm addr)))) + (t (setq comm addr))) + (switch-to-buffer current-perldb-buffer) + (goto-char (dot-max)) + (insert-string comm))) *** perldb.pl-dist Tue Mar 13 10:09:29 1990 --- perldb.pl Wed May 2 15:48:41 1990 *************** *** 9,14 **** --- 9,18 ---- # a do DB'DB(<linenum>); in front of every place that can # have a breakpoint. It also inserts a do 'perldb.pl' before the first line. # + # If we are running in perldb-mode from Emacs, the first command line + # argument is "-emacs", which we remove. In perldb-mode, we output a + # magic string to identify the current line, rather than printing it. + # # $Log: perldb.pl,v $ # Revision 3.0.1.2 90/03/12 16:39:39 lwall # patch13: perl -d didn't format stack traces of *foo right *************** *** 35,40 **** --- 39,47 ---- $header =~ s/.Header: ([^,]+),v(\s+\S+\s+\S+).*$/$1$2/; print OUT "\nLoading DB from $header\n\n"; + $emacs = $#main'ARGV > $[ && $main'ARGV[$[] eq '-emacs'; + shift(@main'ARGV) if $emacs; + sub DB { local($. ,$@, $!, $[, $,, $/, $\); $[ = 0; $, = ""; $/ = "\n"; $\ = ""; *************** *** 50,59 **** } } if ($single || $trace || $signal) { ! print OUT "$sub($line):\t",$line[$line]; ! for ($i = $line + 1; $i <= $max && $line[$i] == 0; ++$i) { ! last if $line[$i] =~ /^\s*(}|#|\n)/; ! print OUT "$sub($i):\t",$line[$i]; } } if ($action[$line]) { --- 57,70 ---- } } if ($single || $trace || $signal) { ! if ($emacs) { ! print OUT "\32\32:$line:1\n"; ! } else { ! print OUT "$sub($line):\t",$line[$line]; ! for ($i = $line + 1; $i <= $max && $line[$i] == 0; ++$i) { ! last if $line[$i] =~ /^\s*(}|#|\n)/; ! print OUT "$sub($i):\t",$line[$i]; ! } } } if ($action[$line]) { *************** *** 184,192 **** $i = $2; $i = $line if $i eq '.'; $i = 1 if $i < 1; ! for (; $i <= $end; $i++) { ! print OUT "$i:\t", $line[$i]; ! last if $signal; } $start = $i; # remember in case they want more $start = $max if $start > $max; --- 195,207 ---- $i = $2; $i = $line if $i eq '.'; $i = 1 if $i < 1; ! if ($emacs) { ! print OUT "\32\32:$line:1\n"; ! } else { ! for (; $i <= $end; $i++) { ! print OUT "$i:\t", $line[$i]; ! last if $signal; ! } } $start = $i; # remember in case they want more $start = $max if $start > $max; *************** *** 299,305 **** $start = 1 if ($start > $max); last if ($start == $end); if ($line[$start] =~ m'."\n$pat\n".'i) { ! print OUT "$start:\t", $line[$start], "\n"; last; } } '; --- 314,324 ---- $start = 1 if ($start > $max); last if ($start == $end); if ($line[$start] =~ m'."\n$pat\n".'i) { ! if ($emacs) { ! print OUT "\32\32:$start:1\n"; ! } else { ! print OUT "$start:\t", $line[$start], "\n"; ! } last; } } '; *************** *** 323,329 **** $start = $max if ($start <= 0); last if ($start == $end); if ($line[$start] =~ m'."\n$pat\n".'i) { ! print OUT "$start:\t", $line[$start], "\n"; last; } } '; --- 342,352 ---- $start = $max if ($start <= 0); last if ($start == $end); if ($line[$start] =~ m'."\n$pat\n".'i) { ! if ($emacs) { ! print OUT "\32\32:$start:1\n"; ! } else { ! print OUT "$start:\t", $line[$start], "\n"; ! } last; } } '; -- Ray Lischner UUCP: {uunet,apollo,decwrl}!mntgfx!lisch