;;;; igrep.el --- An improved interface to `grep`.

;;; Description:
;;; 
;;; Define the \[igrep] command, which is like \[grep] except that it
;;; takes three required arguments (PROGRAM, EXPRESSION, and FILES) and
;;; an optional argument (OPTIONS) instead of just one argument (COMMAND).
;;; Also define the analogous \[egrep] and \[fgrep] commands for convenience.
;;; 
;;; Define the \[igrep-find] command, which is like \[igrep] except that
;;; it uses `find` to recursively `grep` a directory.  Also define the
;;; analogous \[egrep-find] and \[fgrep-find] commands for convenience.
;;; 
;;; \[igrep] and \[igrep-find] (and their analogues) provide defaults
;;; for the EXPRESSION and FILES arguments when called interactively,
;;; and there are global variables that control the syntax of the `grep`
;;; and `find` shell commands that are executed.  A user option controls
;;; whether the corresponding GNU (gzip) "zPROGRAM" script is used, to
;;; `grep` compressed files.
;;; 
;;; \[agrep] and \[agrep-find] are also defined as convenient interfaces
;;; to the approximate `grep` utility, which is distributed with the
;;; `glimpse' indexing and query tool (available from
;;; <URL:http://glimpse.cs.arizona.edu:1994/>).
;;; 
;;; \[grep] itself is advised to provide the \[igrep] interface when
;;; called interactively (when called programmatically, it still uses
;;; the original argument list).  \[grep-find] is defined as an alias
;;; for \[igrep-find].
;;; 
;;; When run interactively from Dired mode, the various \[igrep]
;;; commands provide defaults for the EXPRESSION and FILES arguments
;;; that are based on the visited directory (including any inserted
;;; subdirectories) and the current file.  In addition, the
;;; \[dired-do-igrep] and \[dired-do-igrep-find] commands are defined
;;; that respect the `dired-do-*' command calling conventions: a prefix
;;; argument is interpreted as the number of succeeding files to `grep`,
;;; otherwise all the marked files are `grep`ed.  \[dired-do-grep] and
;;; \[dired-do-grep-find] are defined as aliases for \[dired-do-igrep]
;;; and \[dired-do-igrep-find].

;;; Copyright:
;;; 
;;; Copyright © 1994,1995,1996,1997 Kevin Rodgers
;;; 
;;; 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., 675 Mass Ave, Cambridge, MA 02139, USA.
;;; 
;;; Neither my former nor current employer (Martin Marietta and
;;; Information Handling Services, respectively) has disclaimed any
;;; copyright interest in igrep.el.
;;; 
;;; Kevin Rodgers <kevinr@ihs.com>		Project Engineer
;;; Information Handling Services		Electronic Systems Development
;;; 15 Inverness Way East, M/S A201		
;;; Englewood CO 80112 USA			(303)397-2807[voice]/-2779[fax]

;;; Installation:
;;; 
;;; 1. Put this file in a directory that is a member of load-path, and
;;;    byte-compile it (e.g. with `M-x byte-compile-file') for better
;;;    performance.  You can ignore any warnings about references to free
;;;    variables and "not known to be defined" functions.
;;; 2. Put these forms in default.el or ~/.emacs:
;;;    (autoload (function igrep) "igrep"
;;;       "*Run `grep` PROGRAM to match EXPRESSION in FILES..." t)
;;;    (autoload (function igrep-find) "igrep"
;;;       "*Run `grep` via `find`..." t)
;;;    (autoload (function dired-do-igrep) "igrep"
;;;       "*Run `grep` on the marked (or next prefix ARG) files." t)
;;;    (autoload (function dired-do-igrep-find) "igrep"
;;;       "*Run `grep` via `find` on the marked (or next prefix ARG) directories." t)
;;; 2. a. For completeness, you can add these forms as well:
;;;    (autoload (function grep) "igrep"
;;;       "*Run `grep` PROGRAM to match EXPRESSION in FILES..." t)
;;;    (autoload (function egrep) "igrep"
;;;       "*Run `egrep`..." t)
;;;    (autoload (function fgrep) "igrep"
;;;       "*Run `fgrep`..." t)
;;;    (autoload (function agrep) "igrep"
;;;       "*Run `agrep`..." t)
;;;    (autoload (function grep-find) "igrep"
;;;       "*Run `grep` via `find`..." t)
;;;    (autoload (function egrep-find) "igrep"
;;;       "*Run `egrep` via `find`..." t)
;;;    (autoload (function fgrep-find) "igrep"
;;;       "*Run `fgrep` via `find`..." t)
;;;    (autoload (function agrep-find) "igrep"
;;;       "*Run `agrep` via `find`..." t)
;;;    (autoload (function dired-do-grep) "igrep"
;;;       "*Run `grep` on the marked (or next prefix ARG) files." t)
;;;    (autoload (function dired-do-grep-find) "igrep"
;;;       "*Run `grep` via `find` on the marked (or next prefix ARG) directories." t)
;;; 3. If you are running Windows 95/NT, you should install findutils
;;;    and grep from release 17.1 (or higher) of the Cygnus GNU-Win32
;;;    distribution.  See <URL:http://www.cygnus.com/misc/gnu-win32/>.

;;; Usage:
;;; 
;;; M-x igrep			M-x igrep-find
;;; M-x  grep			M-x  grep-find
;;; M-x egrep			M-x egrep-find
;;; M-x fgrep			M-x fgrep-find
;;; M-x agrep			M-x agrep-find
;;; M-x dired-do-igrep		M-x dired-do-igrep-find
;;; M-x  dired-do-grep		M-x  dired-do-grep-find
;;; (Each of the 10 igrep commands accepts 1, 2, or 3 `C-u' prefix arguments.
;;; The 2 Dired commands interpret a prefix argument like the regular `dired-do'
;;; commands.)
;;; C-x ` (M-x next-error)	C-c C-c (M-x compile-goto-error) [in *igrep*]

;;; Customization examples:
;;; 
;;; To ignore case by default:
;;; 	(setq igrep-options "-i")
;;; To search subdirectories by default:
;;; 	(setq igrep-find t)
;;; To search files with the GNU (gzip) zgrep script:
;;; 	(setq igrep-use-zgrep t)
;;; or define new igrep commands (this works for zegrep and zfgrep as well):
;;; 	(igrep-define zgrep)		; M-x zgrep
;;; 	(igrep-find-define zgrep)	; M-x zgrep-find
;;; To avoid exceeding some shells' limit on command argument length
;;; (this only works when grep'ing files in the current directory):
;;; 	(setq igrep-find t
;;; 	      igrep-find-prune-option "\\! -name .")

;;; To do:
;;; 
;;; 1. Delete the *-program variables (except for igrep-program) and
;;;    replace igrep-options with a table that maps igrep-program to the
;;;    appropriate options.
;;; 2. Generalize support for the -prune find clause (e.g. -fstype nfs).
;;; 3. Provide support for `glimpse`.
;;; 4. Add a menu interface.

;;; LCD Archive Entry:
;;; 
;;; igrep|Kevin Rodgers|kevinr@ihs.com|
;;; An improved interface to grep/egrep/fgrep; plus recursive `find` versions.|
;;; 97/02/10|2.56|~/misc/igrep.el.Z|


;;; Package interface:

(provide 'igrep)

(require 'backquote)			; igrep-with-default-in-history
(require 'compile)			; compile-internal and grep-regexp-
					; alist

(eval-when-compile
  (require 'dired))

(defconst igrep-version "2.56"
  "Version of igrep.el")


;;; User options:

(defgroup igrep nil
  "An improved interface to `grep'."
  :group 'processes)

(defcustom igrep-options nil
  "*The options passed by \\[igrep] to `igrep-program', or nil.

`-n' will automatically be passed to `igrep-program', to generate the
output expected by \\[next-error] and \\[compile-goto-error].
`-e' will automatically be passed to `igrep-program', if it supports
that option."
  :type '(repeat (string :tag "Option"))
  :group 'igrep)

(defcustom igrep-read-options nil
  "*If non-nil, `\\[igrep]' always prompts for options;
otherwise, it only prompts when 1 or 3 `C-u's are given as a prefix arg."
  :type 'boolean
  :group 'igrep)

(defcustom igrep-read-multiple-files nil
  "*If non-nil, `\\[igrep]' always prompts for multiple-files;
otherwise, it only prompts when 2 or 3 `C-u's are given as a prefix arg."
  :type 'boolean
  :group 'igrep)

(defcustom igrep-verbose-prompts t
  "*If t, \\[igrep] prompts for arguments verbosely;
if not t but non-nil, \\[igrep] prompts for arguments semi-verbosely;
if nil, \\[igrep] prompts for arguments tersely."
  :type 'boolean
  :group 'igrep)

(defcustom igrep-save-buffers 'query
  "*If t, \\[igrep] first saves each modified file buffer;
if not t but non-nil, \\[igrep] offers to save each modified file buffer."
  :type '(choice (const :tag "Save" t)
		 (const :tag "Dont Save" nil)
		 (const :tag "Query" query))
  :group 'igrep)

(defvar igrep-program-table		; referenced by igrep-use-zgrep
  (let ((exec-directories exec-path)
	(program-obarray (make-vector 11 0)))
    (while exec-directories
      (if (and (car exec-directories)
	       (file-directory-p (car exec-directories)))
	  (let ((grep-programs
		 (directory-files (car exec-directories)
				  nil "grep\\(\\.exe\\)?\\'")))
	    (while grep-programs
	      ;; Check `(file-executable-p (car grep-programs))'?
	      (if (save-match-data
		    (string-match "\\.exe\\'" (car grep-programs)))
		  (intern (substring (car grep-programs) 0 -4) program-obarray)
		(intern (car grep-programs) program-obarray))
	      (setq grep-programs (cdr grep-programs)))))
      (setq exec-directories (cdr exec-directories)))
    program-obarray)
  "An obarray of available `grep` programs, passed by `igrep-read-program'
to `completing-read' when `igrep-program' is nil.")

(defvar igrep-use-zgrep
  (if (intern-soft "zgrep" igrep-program-table)
      'files)
  "If t, \\[igrep] searches files using the GNU (gzip) `zPROGRAM` script;
If not t but non-nil, \\[igrep] searches compressed FILES using `zPROGRAM`;
if nil, \\[igrep] searches files with `PROGRAM`.")


;;; User variables:

(defvar igrep-program "grep"
  "The default shell program run by \\[igrep] and \\[igrep-find].
It must take a `grep` expression argument and one or more file names.
If nil, \\[igrep] prompts for the program to run.")

(defvar igrep-expression-quote-char
  (if (memq system-type '(ms-dos windows-95 windows-nt emx))
      ?\"
    ?')
  "The character used to delimit the EXPRESSION argument to \\[igrep],
to protect it from shell filename expansion.")

(defvar igrep-expression-option
  (if (or (eq system-type 'berkeley-unix)
	  (save-match-data
	    (string-match "-sco" system-configuration)))
      "-e")
  "If non-nil, the option used to specify the EXPRESSION argument to \\[igrep],
to protect an initial `-' from option processing.")

(defvar igrep-parenthesis-escape-char
  (if (memq system-type '(ms-dos windows-95 windows-nt emx))
      nil
    ?\\)
  "If non-nil, the character used by \\[igrep] to escape parentheses,
to protect them from shell interpretation.")

(defvar igrep-find nil
  "If non-nil, \\[igrep] searches directories using `find`.
See \\[igrep-find].")

(defvar igrep-find-prune-options
  (if (not (save-match-data
	     (string-match "-sco" system-configuration)))
      "-name SCCS -o -name RCS")
  "The `find` clause used to prune directories, or nil;
see \\[igrep-find].")

(defvar igrep-find-file-options "-type f -o -type l"
  "The `find` clause used to filter files passed to `grep`, or nil;
see \\[igrep-find].")

(defvar igrep-find-use-xargs
  (if (equal (call-process "find" nil nil nil
			   (if (boundp 'grep-null-device)
			       grep-null-device
			     "/dev/null")
			   "-print0")
	     0)
      'gnu)
  "If `gnu', \\[igrep-find] executes
	`find ... -print0 | xargs -0 -e grep ...`;
if not `gnu' but non-nil, \\[igrep-find] executes
	`find ... -print | xargs -e grep ...`;
if nil, \\[igrep-find] executes
	`find ... -exec grep ...`.")

(defvar igrep-program-default nil
  "The default `grep` program, passed by `igrep-read-program'
to `completing-read' when `igrep-program' is nil.")

(defvar igrep-expression-history '()
  "The minibuffer history list for \\[igrep]'s EXPRESSION argument.")

(defvar igrep-files-history '()
  "The minibuffer history list for \\[igrep]'s FILES argument.")


;;; Commands:

;; This used to be evaluated at top level.  We consider it evil, so it
;; goes into a user-callable function.    --hniksic
;;;###autoload
(defun igrep-insinuate ()
  "Replace the `grep' functions with `igrep'."
  (defadvice grep (around igrep-interface first (&rest grep-args) activate)
    "If called interactively, use the \\[igrep] interface instead,
where GREP-ARGS is (PROGRAM EXPRESSION FILES OPTIONS);
if called programmatically, GREP-ARGS is still (COMMAND)."
    (interactive (igrep-read-args))
    (if (interactive-p)
	(apply (function igrep) grep-args)
      ad-do-it))
  (defalias 'grep-find 'igrep-find)
  (defalias 'dired-do-grep 'dired-do-igrep)
  (defalias 'dired-do-grep-find 'dired-do-igrep-find))


(defvar win32-quote-process-args)	; XEmacs

;;;###autoload
(defun igrep (program expression files &optional options)
  "*Run `grep` PROGRAM to match EXPRESSION in FILES.
The output is displayed in the *igrep* buffer, which \\[next-error] and
\\[compile-goto-error] parse to find each line of matched text.

PROGRAM may be nil, in which case it defaults to `igrep-program'.

EXPRESSION is automatically delimited by `igrep-expression-quote-char'.

FILES is either a file name pattern (expanded by the shell named by
`shell-file-name') or a list of file name patterns.

Optional OPTIONS is also passed to PROGRAM; it defaults to `igrep-options'.

If a prefix argument \
\(\\[universal-argument]\) \
is given when called interactively,
or if `igrep-read-options' is set, OPTIONS is read from the minibuffer.

If two prefix arguments \
\(\\[universal-argument] \\[universal-argument]\) \
are given when called interactively,
or if `igrep-read-multiple-files' is set, FILES is read from the minibuffer
multiple times.

If three prefix arguments \
\(\\[universal-argument] \\[universal-argument] \\[universal-argument]\) \
are given when called interactively,
or if `igrep-read-options' and `igrep-read-multiple-files' are set,
OPTIONS is read and FILES is read multiple times.

If `igrep-find' is non-nil, the directory or directories
containing FILES is recursively searched for files whose name matches
the file name component of FILES \(and whose contents match
EXPRESSION\)."
  (interactive
   (igrep-read-args))
  (if (null program)
      (setq program (or igrep-program "grep")))
  (if (null options)
      (setq options igrep-options))
  (if (not (listp files))		; (stringp files)
      (setq files (list files)))
  (if (string-match "^[rj]?sh$" (file-name-nondirectory shell-file-name))
      ;; (restricted, job-control, or standard) Bourne shell doesn't expand ~:
      (setq files
	    (mapcar 'expand-file-name files)))
  (let* ((win32-quote-process-args nil)	; work around NT Emacs hack
	 (use-zgrep (cond ((eq igrep-use-zgrep t))
			  (igrep-use-zgrep
			   (let ((files files)
				 (compressed-p nil))
			     (while (and files (not compressed-p))
			       (if (save-match-data
				     (string-match "\\.g?[zZ]\\'" (car files)))
				   (setq compressed-p t))
			       (setq files (cdr files)))
			     compressed-p))
			  (t nil)))
	 (command (format "%s -n %s %s %c%s%c %s %s"
			  (if (and use-zgrep
				   (save-match-data
				     (not (string-match "\\`z" program))))
			      (setq program (concat "z" program))
			    program)
			  (or options "")
			  (or igrep-expression-option
			      (progn
				(if (save-match-data
				      (string-match "\\`-" expression))
				    (setq expression (concat "\\" expression)))
				""))
			  igrep-expression-quote-char
			  expression
			  igrep-expression-quote-char
			  (if igrep-find
			      (if igrep-find-use-xargs
				  ""
				"\"{}\"")
			    (mapconcat (function identity) files " "))
			  (if (boundp 'grep-null-device)
			      grep-null-device
			    "/dev/null"))))
    (if igrep-find
	(setq command
	      (igrep-format-find-command command files)))
    (cond ((eq igrep-save-buffers t) (save-some-buffers t))
	  (igrep-save-buffers (save-some-buffers)))
    (compile-internal command
		      (format "No more %c%s%c matches"
			      igrep-expression-quote-char
			      program
			      igrep-expression-quote-char)
		      "igrep" nil grep-regexp-alist)))

;; Analogue commands:

;;;###autoload
(defmacro igrep-define (analogue-command &rest igrep-bindings)
  "Define ANALOGUE-COMMAND as an `igrep' analogue command.
Optional (VARIABLE VALUE) arguments specify temporary bindings for the command."
;;;  (interactive "SCommand: ") ; C-u => read bindings?
  (let ((analogue-program (symbol-name analogue-command)))
    (` (defun (, analogue-command) (&rest igrep-args)
	 (, (format "*Run `%s` via \\[igrep].
All arguments \(including prefix arguments, when called interactively\)
are handled by `igrep'."
		    analogue-program))
	 (interactive
	  (let ((igrep-program (if igrep-program (, analogue-program)))
		(igrep-program-default (, analogue-program)))
	    (igrep-read-args)))
	 (let ( (,@ igrep-bindings))
	   (apply (function igrep)
		  (cond ((interactive-p) (car igrep-args))
			((car igrep-args))
			(t (, analogue-program)))
		  (cdr igrep-args)))))))

(igrep-define egrep)
(igrep-define fgrep)
(igrep-define agrep
  (igrep-use-zgrep nil)
  (igrep-expression-option "-e"))


;; Recursive (`find`) commands:

;;;###autoload
(defun igrep-find (&rest igrep-args)
  "*Run `grep` via `find`; see \\[igrep] and `igrep-find'.
All arguments \(including prefix arguments, when called interactively\)
are handled by `igrep'."
  (interactive
   (let ((igrep-find t))
     (igrep-read-args)))
  (let ((igrep-find t))
    (apply (function igrep) igrep-args)))

;; Analogue recursive (`find`) commands:

;;;###autoload
(defmacro igrep-find-define (analogue-command &rest igrep-bindings)
  "Define ANALOGUE-COMMAND-find as an `igrep' analogue `find` command.
Optional (VARIABLE VALUE) arguments specify temporary bindings for the command."
;;;  (interactive "SCommand: ") ; C-u => read bindings?
  (let ((analogue-program (symbol-name analogue-command)))
    (setq analogue-command
	  (intern (format "%s-find" analogue-command)))
    (` (defun (, analogue-command) (&rest igrep-args)
	 (, (format "*Run `%s` via \\[igrep-find].
All arguments \(including prefix arguments, when called interactively\)
are handled by `igrep'."
		    analogue-program))
	 (interactive
	  (let ((igrep-program (if igrep-program (, analogue-program)))
		(igrep-program-default (, analogue-program))
		(igrep-find t))
	    (igrep-read-args)))
	 (let ( (,@ igrep-bindings))
	   (apply (function igrep-find)
		  (cond ((interactive-p) (car igrep-args))
			((car igrep-args))
			(t (, analogue-program)))
		  (cdr igrep-args)))))))

(igrep-find-define egrep)
(igrep-find-define fgrep)
(igrep-find-define agrep
  (igrep-use-zgrep nil)
  (igrep-expression-option "-e"))


;; Dired commands:

;;;###autoload
(defun dired-do-igrep (program expression &optional options arg)
  "*Run `grep` PROGRAM to match EXPRESSION (with optional OPTIONS)
on the marked (or next prefix ARG) files."
  (interactive
   (let* ((current-prefix-arg nil)
	  (igrep-args (igrep-read-args t)))
     ;; Delete FILES:
     (setcdr (nthcdr 1 igrep-args) (nthcdr 3 igrep-args))
     ;; Append ARG:
     (nconc igrep-args (list current-prefix-arg))))
  (igrep program
	 expression
	 (funcall (cond ((fboundp 'dired-get-marked-files) ; GNU Emacs
			 'dired-get-marked-files)
			((fboundp 'dired-mark-get-files) ; XEmacs
			 'dired-mark-get-files))
		  t arg)
	 options))

;;;###autoload

;; Dired recursive (`find`) commands:

;;;###autoload
(defun dired-do-igrep-find (program expression &optional options arg)
  "*Run `grep` PROGRAM to match EXPRESSION (with optional OPTIONS)
on the marked (or next prefix ARG) directories."
  (interactive
   (let* ((current-prefix-arg nil)
	  (igrep-find t)
	  (igrep-args (igrep-read-args t)))
     ;; Delete FILES:
     (setcdr (nthcdr 1 igrep-args) (nthcdr 3 igrep-args))
     ;; Append ARG:
     (nconc igrep-args (list current-prefix-arg))))
  (let ((igrep-find t))
    (dired-do-igrep program expression options arg)))


;;; Utilities:

(defsubst igrep-file-directory (name)
  ;; Return the directory component of NAME, or "." if it has no
  ;; directory component.
  (directory-file-name (or (file-name-directory name)
			   (file-name-as-directory "."))))

(defsubst igrep-file-pattern (name)
  ;; Return the file component of NAME, or "*" if it has no file
  ;; component.
  (let ((pattern (file-name-nondirectory name)))
       (if (string= pattern "")
	   "*"
	 pattern)))

(defun igrep-format-find-command (command files)
  ;; Format `grep` COMMAND to be invoked via `find` on FILES.
  (let ((directories '())
	(patterns '()))
    (while files
      (let ((dir (igrep-file-directory (car files)))
	    (pat (igrep-file-pattern (car files))))
	(if (and (not (string= dir "."))
		 (file-symlink-p dir))
	    (setq dir (concat dir "/.")))
	(if (not (member dir directories))
	    (setq directories (cons dir directories)))
	(cond ((equal pat "*")
	       (setq patterns t))
	      ((and (listp patterns)
		    (not (member pat patterns)))
	       (setq patterns (cons pat patterns)))))
      (setq files (cdr files)))
    (format (cond ((eq igrep-find-use-xargs 'gnu)
		   ;; | \\\n
		   "find %s %s %s %s -print0 | xargs -0 -e %s")
		  (igrep-find-use-xargs
		   ;; | \\\n
		   "find %s %s %s %s -print | xargs -e %s")
;;;		  ((memq system-type '(ms-dos windows-95 windows-nt emx))
;;;		   "find %s %s %s %s -exec %s ;")
		  (t
		   "find %s %s %s %s -exec %s \\;"))
	    (mapconcat (function identity) (nreverse directories)
		       " ")
	    (if igrep-find-prune-options
		(format "-type d %c( %s %c) -prune -o"
			(or igrep-parenthesis-escape-char ? )
			igrep-find-prune-options
			(or igrep-parenthesis-escape-char ? ))
	      "")
	    (if igrep-find-file-options
		(format "%c( %s %c)"
			(or igrep-parenthesis-escape-char ? )
			igrep-find-file-options
			(or igrep-parenthesis-escape-char ? ))
	      "")
	    (if (listp patterns)
		(if (cdr patterns)	; (> (length patterns) 1)
		    (format "%c( %s %c)"
			    (or igrep-parenthesis-escape-char " ")
			    (mapconcat (function (lambda (pat)
						   (format "-name \"%s\"" pat)))
				       (nreverse patterns)
				       " -o ")
			    (or igrep-parenthesis-escape-char " "))
		  (format "-name \"%s\"" (car patterns)))
	      "")
	    command)))

(defun igrep-read-args (&optional no-files)
  ;; Read and return a list: (PROGRAM EXPRESSION FILES OPTIONS).
  ;; If NO-FILES is non-nil, then FILES is not read and nil is returned
  ;; in its place.
  (let* ((program (igrep-read-program (if igrep-verbose-prompts
					  (if igrep-find
					      "[find] "))))
	 (prompt-prefix (if igrep-verbose-prompts
			    (apply (function concat)
				   (if igrep-find
				       "[find] ")
				   (if (eq igrep-verbose-prompts t)
				       (list program " ")))))
	 (options (igrep-read-options prompt-prefix)))
    (if (eq igrep-verbose-prompts t)
	(setq prompt-prefix
	      (concat prompt-prefix options " ")))
    (list program
	  (igrep-read-expression prompt-prefix)
	  (if (not no-files)
	      (igrep-read-files prompt-prefix))
	  options)))

(defsubst igrep-prefix (prefix string)
  ;; If PREFIX is non-nil, concatenate it and STRING; otherwise, return STRING.
  (if prefix
      (concat prefix string)
    string))

(defun igrep-read-program (&optional prompt-prefix)
  ;; If igrep-program is nil, read and return a program name from the
  ;; minibuffer; otherwise, return igrep-program.
  ;; Optional PROMPT-PREFIX is prepended to the "Program: " prompt.
  (or igrep-program
      (let ((prompt "Program: "))
	(completing-read (igrep-prefix prompt-prefix prompt) igrep-program-table
			 nil t (or igrep-program-default "grep")))))

(defun igrep-read-options (&optional prompt-prefix)
  ;; If current-prefix-arg is '(4) or '(64), read and return an options
  ;; string from the minibuffer; otherwise, return igrep-options.
  ;; Optional PROMPT-PREFIX is prepended to the "Options: " prompt.
  (if (or igrep-read-options
	  (and (consp current-prefix-arg)
	       (memq (prefix-numeric-value current-prefix-arg)
		     '(4 64))))
      (let ((prompt "Options: "))
	(read-string (igrep-prefix prompt-prefix prompt)
		     (or igrep-options "-")))
    igrep-options))

(defun igrep-read-expression (&optional prompt-prefix)
  ;; Read and return a `grep` expression string from the minibuffer.
  ;; Optional PROMPT-PREFIX is prepended to the "Expression: " prompt.
  (let ((default-expression (igrep-default-expression)))
    (if (string= default-expression "")
	(read-from-minibuffer (igrep-prefix prompt-prefix "Expression: ")
			      nil nil nil 'igrep-expression-history)
      (let ((expression
	     (igrep-read-string-with-default-in-history
	      (igrep-prefix prompt-prefix (format "Expression (default %s): "
						  default-expression))
	      default-expression
	      'igrep-expression-history)))
	(if (string= expression "")
	    default-expression
	  expression)))))

(defun igrep-default-expression ()
  (if (eq major-mode 'dired-mode)
      (let ((dired-file (dired-get-filename nil t)))
	(save-excursion
	  (set-buffer (or (and dired-file (get-file-buffer dired-file))
			  (other-buffer (current-buffer) t)))
	  (current-word)))
    (current-word)))

(defsubst igrep-default-key ()
  ;; Return the key bound to `exit-minibuffer', preferably "\r".
  (if (eq (lookup-key minibuffer-local-completion-map "\r")
	  (function exit-minibuffer))
      "\r"
    (where-is-internal (function exit-minibuffer)
		       minibuffer-local-completion-map
		       t)))

(defun igrep-read-files (&optional prompt-prefix)
  ;; Read and return a file name pattern from the minibuffer.  If
  ;; current-prefix-arg is '(16) or '(64), read multiple file name
  ;; patterns and return them in a list.  Optional PROMPT-PREFIX is
  ;; prepended to the "File(s): " prompt.
  (let* ((dired-subdirectory (if (eq major-mode 'dired-mode)
				 (dired-current-directory t)))
	 (default-files (concat dired-subdirectory
				(igrep-default-file-pattern)))
	 (prompt (format "File(s) (default %s): " default-files))
	 (insert-default-directory nil)	; use relative path names
	 (file (igrep-read-file-name-with-default-in-history
		(igrep-prefix prompt-prefix prompt)
		default-files
		nil
		'igrep-files-history)))
    (if (or igrep-read-multiple-files
	    (and (consp current-prefix-arg)
		 (memq (prefix-numeric-value current-prefix-arg)
		       '(16 64))))
	(let ((files (list file)))
	  (setq prompt
		(igrep-prefix prompt-prefix
			      (if igrep-verbose-prompts
				  (format "File(s): [Type `%s' when done] "
					  (key-description (igrep-default-key)))
				"File(s): ")))
	  (while (not (string= (setq file
				     (igrep-read-file-name prompt
							   nil "" nil nil
							   'igrep-files-history))
			       ""))
	    (setq files (cons file files)))
	  (nreverse files))
      file)))

(defmacro igrep-with-default-in-history (default history &rest body)
  ;; Temporarily append DEFAULT to HISTORY, and execute BODY forms.
  (` (progn
       ;; Append default to history:
       (set history
	    (cons (, default)
		  (if (boundp (, history))
		      (symbol-value (, history))
		    '())))
       (unwind-protect			; Make sure the history is restored.
	   ;; Execute body:
	   (progn (,@ body))
	 ;; Delete default from history (undo the append above):
	 (setcdr (symbol-value (, history))
		 (nthcdr 2 (symbol-value (, history))))))))

(defun igrep-read-string-with-default-in-history (prompt default history)
  ;; Read a string from the minibuffer, prompting with string PROMPT.
  ;; DEFAULT can be inserted into the minibuffer with `previous-
  ;; history-element'; HISTORY is a symbol whose value (if bound) is a
  ;; list of previous results, most recent first.
  (let ((string (igrep-with-default-in-history default history
		  (read-from-minibuffer prompt nil nil nil history))))
    ;; Replace empty string in history with default:
    (if (string= string "")
	(setcar (symbol-value history) default))
    string))

(defun igrep-read-file-name-with-default-in-history
  (prompt &optional default initial history)
  ;; Read a file name from the minibuffer, prompting with string PROMPT.
  ;; DEFAULT can be inserted into the minibuffer with `previous-
  ;; history-element'; HISTORY is a symbol whose value (if any) is a
  ;; list of previous results, most recent first.
  (igrep-with-default-in-history default history
    (igrep-read-file-name prompt nil default nil initial history)))

(defun igrep-read-file-name (prompt
  &optional directory default existing initial history)
  ;; Just like read-file-name, but with optional HISTORY.
  ;; Also: convert DIRECTORY to DIRECTORY/* file name pattern.
  (let ((file-name
	 (if history
	     (let ((file-name-history (symbol-value history)))
	       (prog1 (read-file-name prompt directory default existing initial)
		 (set history file-name-history)))
	   (read-file-name prompt directory default existing initial))))
    (if (and (not (string-equal file-name ""))
	     (file-directory-p file-name))
	(expand-file-name "*" file-name)
      file-name)))

(defun igrep-default-file-pattern ()
  ;; Return a shell file name pattern that matches files with the same
  ;; extension as the file being visited in the current buffer.
  ;; (Based on other-possibly-interesting-files in ~/as-is/unix.el, by
  ;; Wolfgang Rupprecht <wolfgang@mgm.mit.edu>.)
  (if (eq major-mode 'dired-mode)
      (cond ((stringp dired-directory)
	     (if (file-directory-p dired-directory)
		 "*"
	       (file-name-nondirectory dired-directory))) ; wildcard
	    ((consp dired-directory)	; (DIR FILE ...)
	     (mapconcat 'identity (cdr dired-directory) " ")))
    (if buffer-file-name
	(let ((file-name (file-name-nondirectory buffer-file-name)))
	  (concat "*"
		  (save-match-data
		    (if (string-match "\\.[^.]+\\(\\.g?[zZ]\\)?$" file-name)
			(substring file-name
				   (match-beginning 0) (match-end 0))))))
      "*")))

;;; Local Variables:
;;; eval: (put 'igrep-with-default-in-history 'lisp-indent-hook 2)
;;; eval: (put 'igrep-define 'lisp-indent-hook 1)
;;; eval: (put 'igrep-find-define 'lisp-indent-hook 1)
;;; End:

;;;; igrep.el ends here

