[comp.emacs] ksh for GnuEmacs

wolfgang@mgm.mit.edu (Wolfgang Rupprecht) (01/06/88)

This is a ksh-like extention to shell.el.  These extentiions implement
command history (backwards, forwards, back-search, forward-srearch),
filename completion, and history printout for an emacs shell window.
The one glaring difference between this and ksh, is that all of the
shell-mode commands are bound to the Control-C prefix map. (Eg.
previous command is C-c C-p).

The full list of shell commands is:

	C-c C-a         shell-beginning-of-line
	C-c C-c         interrupt-shell-subjob
	C-c C-d         shell-send-eof
	C-c C-h         Prefix Command
	C-c TAB         shell-filename-expand
	C-c RET         shell-push-input
	C-c C-n         shell-next-command
	C-c C-o         kill-output-from-shell
	C-c C-p         shell-previous-command
	C-c C-r         shell-history-search-backward
	C-c C-s         shell-history-search-forward
	C-c C-u         kill-shell-input
	C-c C-w         backward-kill-word
	C-c C-x         Prefix Command
	C-c C-y         copy-last-shell-input
	C-c C-z         stop-shell-subjob
	C-c ESC         Prefix Command
	C-c C-\         quit-shell-subjob
	C-c [           show-output-from-shell
	C-c h           shell-list-history

This is a diff for shell.el(18.49) (to add a few hooks, and some
personal bug-fixes/hacks) and a file shellext.el, which does the real
work.

This basic code has been running on 18.26 as well as 18.49, so you
shouldn't have much trouble, even on older emacses.

			-enjoy
			wolfgang

----cut here---
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;									     ;;
;;	File:     shellext.el						     ;;
;;	Author:   Wolfgang Rupprecht					     ;;
;;      Address:  wolfgang@mgm.mit.edu (IP addr 18.82.0.114)                 ;;
;;	Created:  Mon Oct 12 18:52:34 EDT 1987				     ;;
;;	Contents: ksh-like extensions to shell.el		             ;;
;;									     ;;
;;	Copyright (c) 1987 Wolfgang Rupprecht.				     ;;
;;									     ;;
;;	$Log$								     ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; GNU Emacs and this file "shellext.el", 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 and shellext.el, 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.

;; If you like this shell hack, and would like other custom
;; non-proprietary GnuEmacs extensions, let me know. I may be
;; interested in doing it for you on a contract basis. -wsr

(provide 'shellext)			; for require 'shellext in 
					; patched shell.el

(defvar shell-last-search "" "Last shell search string.")
(defvar shell-max-history 60
  "*Max shell history retained")
(defvar shell-history-list nil
  "History list of past shell commands.")
(defvar shell-history-index -1
  "Where we are on the history list. It is -1 unless currently
walking up/down the list")
(defvar shell-history-list-order nil
  "*If t, list shell history with most recent command last." nil)
(defvar shell-read-history t
  "*If t, the emacs shell will read in the user's ~/.history file.
This somewhat slows down shell startup.")

(define-key shell-mode-map "\C-c\t" 'shell-filename-expand)
(define-key shell-mode-map "\C-c\C-p" 'shell-previous-command)
(define-key shell-mode-map "\C-c\C-n" 'shell-next-command)
(define-key shell-mode-map "\C-c\C-r" 'shell-history-search-backward)
(define-key shell-mode-map "\C-c\C-s" 'shell-history-search-forward)
(define-key shell-mode-map "\C-ch" 'shell-list-history)
(define-key shell-mode-map "\C-c\C-a" 'shell-beginning-of-line)
(define-key shell-mode-map "\C-c\r" 'shell-push-input)
; and now rebind 'show-output-from shell (which was on C-cC-r)
(define-key shell-mode-map "\C-c[" 'show-output-from-shell)

(defun shell-filename-expand ()
  "Complete the filename before (point) as far as possible."
	(interactive)	       
	  (let* ((end (point))
		 (beg (progn
			(re-search-backward "[ \t\n;]")
			(forward-char 1)
			(point)))
		 (new (path-name-completion (buffer-substring beg end))))
	    (cond ((eq new t)
		   (progn
		     (goto-char end)
		     (message "File name is already complete")))
		  ((null new)
		   (progn
		     (goto-char end)
		     (error "No completion possible")))
		   (t
		    (progn
		      (delete-region beg end)
		      (insert new))))))

(defun path-name-completion (path-name)
  "Complete PATHNAME as far as possible, return this string."
  (let* ((dir (file-name-directory path-name))
	 (file (file-name-nondirectory path-name))
	 (completion
	   (file-name-completion file (or dir ""))))
    (if (string-equal file completion)	; we are at a branch point
	(let ((list (sort (file-name-all-completions (or file "") (or dir ""))
			  'string-lessp)))
	  (with-output-to-temp-buffer " *Completions*"
		    (princ "Possible completions are:\n")
		    (while list
		      ;; -40 padding spec doesn't work !!!
		      ;; (princ (format "%-40s %s" (car list)
		      ;;                (or (car (cdr list)) "")))
		      (princ (car list))
		      (if (cdr list)
			  (progn
			    (princ
			      (make-string
				(- 35 (length (car list)))
				?\ ))
			    (princ (car (cdr list)))))
		      (terpri)
		      (setq list (cdr (cdr list)))))
		  path-name)		; give 'em back their input
      ;; BUG?: emacs outputs to wrong-buffer (inserted text destined for
      ;; the *shell* buffer goes to next buffer on buffer list.)
      ;; Seemingly unneeded save-excursion around kill-buffer solves
      ;; this problem. -wsr
      (save-excursion
	(let ((buffer (get-buffer " *Completions*")))
	  (if buffer (kill-buffer buffer))))
      (if dir
	  (and completion (or (eq completion t)	; leave t or nil alone
			      (concat dir completion)))
	completion))))

(defun shell-previous-command ()
  "Insert the previous command into the shell buffer."
  (interactive)
  (let ((history (nthcdr (1+ shell-history-index) shell-history-list)))
    (if history
	(progn
	  (delete-region (process-mark (get-buffer-process (current-buffer)))
			 (point-max))
	  (goto-char (point-max))
	  (insert (car history))
	  (setq shell-history-index (1+ shell-history-index)))
;	  (message "history %d" shell-history-index)
      (error "End of history list, (history %d)" shell-history-index))))

(defun shell-next-command ()
  "Insert the next command into the shell buffer."
  (interactive)
  (delete-region (process-mark (get-buffer-process (current-buffer)))
	       (point-max))
  (goto-char (point-max))
  (if (<= 0 shell-history-index)  
      (setq shell-history-index (1- shell-history-index))
    (error "Top of history list (history %d)" shell-history-index))
  (if (<= 0 shell-history-index)
      (progn 
;	(message "history %d" shell-history-index)
	(insert (nth shell-history-index shell-history-list)))))

(defun shell-history-search-backward (string)
  "Search the history list for an occurance of STRING."
  (interactive (list (setq shell-last-search
			   (read-string
			    "History search for: " shell-last-search))))
  (let* ((index (1+ shell-history-index)) ; start at next command
	 (history (nthcdr index shell-history-list)))
    (while (and history
		(null (string-match string (car history))))
      (setq index (1+ index)
	    history (cdr history)))
    (if history
	(progn
	  (setq shell-history-index index)
	  (delete-region (process-mark (get-buffer-process (current-buffer)))
			 (point-max))
	  (goto-char (point-max))
	  (insert (car history))
;	  (message "history %d" shell-history-index)
	  )
      (error "No match found, history %d" shell-history-index))))


(defun shell-history-search-forward (string)
  "Search the history list for an occurance of STRING."
  (interactive (list (setq shell-last-search
			   (read-string
			    "History search for: " shell-last-search))))
  (let ((index shell-history-index))
    (while (and (<= 0 index)		; not as effecient as backwards hum...
		(null (string-match
		       string (nth (setq index (1- index))
				   shell-history-list)))))
    ;; index is bounded by: (-1 <= index <= shell-history-index) 
    (if (<= 0 index)
	(progn
	  (setq shell-history-index index)
	  (delete-region (process-mark (get-buffer-process (current-buffer)))
			 (point-max))
	  (goto-char (point-max))
	  (insert (or (nth index shell-history-list) ""))
;	  (message "history %d" shell-history-index)
	  )
      (error "No match found, history %d" shell-history-index))))

(defun shell-list-history ()
  "List the history in the *History* buffer. A '*' indicates current
position on the history list."
  (interactive)
  (with-output-to-temp-buffer "*History*"
    (if shell-history-list-order
	(let ((history (reverse (cons "<none>" shell-history-list)))
	      (index (1- (length shell-history-list))))
	  (while history
	    (princ (format "%c [%d] %s\n" 
			   (if (= index shell-history-index)
			       ?* ?\ )
			   index (car history)))
	    (setq history (cdr history)
		  index (1- index))))
      (let ((history (cons "<none>" shell-history-list))
	    (index -1))
	(while history
	  (princ (format "%c [%d] %s\n" 
			 (if (= index shell-history-index)
			     ?* ?\ )
			 index (car history)))
	  (setq history (cdr history)
		index (1+ index)))))))

(defun shell-beginning-of-line ()
  "Goto the beggining of the commad line. (ie. just after the prompt)"
  (interactive)
  (goto-char (process-mark (get-buffer-process (current-buffer)))))

(defun shell-input-history-file ()
  "Read in the user's ~/.history file."
  (if shell-history-list		; if non-nil, its only a shell restart,
      nil				; so don't trash existing history list.
    (let (list)
      (save-excursion
	(set-buffer (get-buffer-create "*history-temp*"))
	(erase-buffer)
	(insert-file (expand-file-name "~/.history"))
	(goto-char (point-min))
	(while (re-search-forward "[^\^@\n]+$" nil t)
	  (setq list
		(cons (buffer-substring (match-beginning 0)
					(match-end 0))
		      list)))
	(kill-buffer (current-buffer)))
      (let ((prune-pt (nthcdr shell-max-history list)))
	(and prune-pt (rplacd prune-pt nil)))
      (make-local-variable 'shell-history-index)
      (make-local-variable 'shell-history-list)
      (setq shell-history-list list)
      (setq shell-history-index -1))))

(defun shell-output-history-file ()
  "Write out the user's ~/.history file from the internal history list."
  (save-excursion
    (let ((list shell-history-list))	; hold onto that slippery local var
      (set-buffer (get-buffer-create "*history-temp*"))
      (erase-buffer)
      (while list
	(insert (car list) "\n\^@")
	(setq list (cdr list))))
    (write-region (point-min) (point-max)
		  (expand-file-name "~/.history") nil 'nomsg)))

(defun shell-save-history ()
  "Save this command on the shell-history-list."
  (let ((command (buffer-substring last-input-start (1- last-input-end))))
    (if (or (string-match "^[ \t]*$" command)
	    (string-equal command (car shell-history-list)))
	nil				; don't hang dups on list
      (setq shell-history-list (cons command shell-history-list))
      (let ((prune-pt (nthcdr shell-max-history shell-history-list)))
	(and prune-pt (rplacd prune-pt nil)))))
  (setq shell-history-index -1))

(defun shell-push-input ()
  "Pushes all pending shell input to shell. Like \\[shell-send-input], only
it doesn't append a newline. Useful for programs that expect to talk 
to a tty in raw mode (eg. tip(1)). The pushed input doesn't get recorded
on the shell's history list."
  (interactive)
  (goto-char (point-max))
  (move-marker last-input-start
	       (process-mark (get-buffer-process (current-buffer))))
  (move-marker last-input-end (point))
  (let ((process (get-buffer-process (current-buffer))))
    (process-send-region process last-input-start last-input-end)
    (set-marker (process-mark process) (point))))


----cut here---shell.el---
*** /users/src/gnuemacs-18.49/lisp/shell.el	Fri Aug 14 17:55:08 1987
--- /users/wolfgang/emacs18/patches/shell.el	Fri Jan  1 12:31:31 1988
***************
*** 1,3 ****
--- 1,12 ----
+
+
+ 
+ ;;;;
+ ;; this is a patched version of shell.el from gnuemacs 18.49
+ ;; patches are marked with '; wsr patch'
+ ;; -wolfgang rupprecht 9/15/87
+ ;;;;
+ 
  ;; Run subshell under Emacs
  ;; Copyright (C) 1985, 1986, 1987 Free Software Foundation, Inc.
  
***************
*** 84,94 ****
--- 93,109 ----
    (setq last-input-start (make-marker))
    (make-local-variable 'last-input-end)
    (setq last-input-end (make-marker))
+   (make-local-variable 'shell-history-list) ; wsr patch
+   (make-local-variable 'shell-history-index) ; wsr patch
+   (make-local-variable 'shell-last-search) ; wsr patch
    (run-hooks 'shell-mode-hook))
  
  (if shell-mode-map
      nil
    (setq shell-mode-map (make-sparse-keymap))
+   ; sparse keymaps have unordered key documentation in "C-hm" help window
+   ; this should make the C-c map a regular 128 entry map
+   (define-key shell-mode-map "\C-c" (make-keymap))
    (define-key shell-mode-map "\C-m" 'shell-send-input)
    (define-key shell-mode-map "\C-c\C-d" 'shell-send-eof)
    (define-key shell-mode-map "\C-c\C-u" 'kill-shell-input)
***************
*** 99,104 ****
--- 114,120 ----
    (define-key shell-mode-map "\C-c\C-o" 'kill-output-from-shell)
    (define-key shell-mode-map "\C-c\C-r" 'show-output-from-shell)
    (define-key shell-mode-map "\C-c\C-y" 'copy-last-shell-input))
+ 
  
  (defvar explicit-csh-args
    (if (eq system-type 'hpux)
***************
*** 142,148 ****
  	    (let ((symbol (intern-soft (concat "explicit-" name "-args"))))
  	      (if (and symbol (boundp symbol))
  		  (symbol-value symbol)
! 		'("-i")))))))
  
  (defun make-shell (name program &optional startfile &rest switches)
    (let ((buffer (get-buffer-create (concat "*" name "*")))
--- 158,165 ----
  	    (let ((symbol (intern-soft (concat "explicit-" name "-args"))))
  	      (if (and symbol (boundp symbol))
  		  (symbol-value symbol)
! 		'("-i"))))))
!   (if shell-read-history (shell-input-history-file))) ; wsr patch
  
  (defun make-shell (name program &optional startfile &rest switches)
    (let ((buffer (get-buffer-create (concat "*" name "*")))
***************
*** 151,157 ****
      (if proc (setq status (process-status proc)))
      (save-excursion
        (set-buffer buffer)
!       ;;    (setq size (buffer-size))
        (if (memq status '(run stop))
  	  nil
  	(if proc (delete-process proc))
--- 168,174 ----
      (if proc (setq status (process-status proc)))
      (save-excursion
        (set-buffer buffer)
!       (setq size (buffer-size))
        (if (memq status '(run stop))
  	  nil
  	(if proc (delete-process proc))
***************
*** 163,178 ****
  			  "EMACS=t"
  			  "-" program switches))
  	(cond (startfile
! 	       ;;This is guaranteed to wait long enough
! 	       ;;but has bad results if the shell does not prompt at all
! 	       ;;	     (while (= size (buffer-size))
! 	       ;;	       (sleep-for 1))
! 	       ;;I hope 1 second is enough!
! 	       (sleep-for 1)
  	       (goto-char (point-max))
  	       (insert-file-contents startfile)
  	       (setq startfile (buffer-substring (point) (point-max)))
! 	       (delete-region (point) (point-max))
  	       (process-send-string proc startfile)))
  	(setq name (process-name proc)))
        (goto-char (point-max))
--- 180,196 ----
  			  "EMACS=t"
  			  "-" program switches))
  	(cond (startfile
! 		(let ((cnt 3))		; wait up to 3 seconds for prompt
! 		  ; assume that prompt is unset if 3 seconds pass.
! 		  (while (and (= size (buffer-size))
! 			      (< 0 cnt))
! 		    (sit-for 1)
! 		    (setq cnt (1- cnt))))
  	       (goto-char (point-max))
  	       (insert-file-contents startfile)
  	       (setq startfile (buffer-substring (point) (point-max)))
! ; truth in advertising, show input region.
! ;	       (delete-region (point) (point-max)) ; -wsr
  	       (process-send-string proc startfile)))
  	(setq name (process-name proc)))
        (goto-char (point-max))
***************
*** 218,223 ****
--- 236,242 ----
        (error (funcall shell-set-directory-error-hook)))
    (let ((process (get-buffer-process (current-buffer))))
      (process-send-region process last-input-start last-input-end)
+     (shell-save-history)		; wsr patch
      (set-marker (process-mark process) (point))))
  
  ;;;  If this code changes (shell-send-input and shell-set-directory),
***************
*** 325,330 ****
--- 344,350 ----
  (defun kill-shell-input ()
    "Kill all text since last stuff output by the shell or its subjobs."
    (interactive)
+   (goto-char (point-max))		; slight bug fix. -wsr
    (kill-region (process-mark (get-buffer-process (current-buffer)))
  	       (point)))
  
***************
*** 335,340 ****
--- 355,362 ----
    (lisp-mode-commands inferior-lisp-mode-map)
    (define-key inferior-lisp-mode-map "\e\C-x" 'lisp-send-defun))
  
+ (require 'shellext) ; needs shell-mode-map and lisp-mode-map ; wsr patch
+ 
  (defvar inferior-lisp-program "lisp"
    "*Program name for invoking an inferior Lisp with `run-lisp'.")
  
***************
*** 393,398 ****
--- 415,423 ----
    (setq last-input-start (make-marker))
    (make-local-variable 'last-input-end)
    (setq last-input-end (make-marker))
+   (make-local-variable 'shell-history-list) ; wsr patch
+   (make-local-variable 'shell-history-index) ; wsr patch
+   (make-local-variable 'shell-last-search) ; wsr patch
    (run-hooks 'shell-mode-hook 'lisp-mode-hook))
  
  (defun run-lisp ()
Wolfgang Rupprecht	UUCP: mit-eddie!mgm.mit.edu!wolfgang
				(or) mirror!mit-mgm!wolfgang
			ARPA: wolfgang@mgm.mit.edu (IP addr 18.82.0.114)