;; fmf.el --- Find Multiple Files

;; Author : Robert E. Wyrick, Dec 2, 1998

;; This file is _NOT_ part of GNU Emacs, but should be.
;; I hereby release this code under the GPL.

;; Description:
;;   Allows you to load many files at once, try:
;;     ESC-x find-multiple-files *.cpp RET
;;
;;   On newer version of GNU Emacs, the find-file-wildcards variable exists
;;   and takes care of this functionality, but since I move between emacs
;;   version (old and new) a lot, I wrote this code so that I'd always have
;;   this ability.  This code works very hard to try to find a way to expand
;;   wildcards, regardless of what system you are on.  If you've found a
;;   system that this code doesn't work on, please let me know!  Better yet
;;   fix it and send me the patches.

;; Commentary:
;;   All ways to get a file list have been tested on:
;;          1. Windows 95 / NT Emacs 20.6
;;          2. AIX / GNU Emacs 20.2.1
;; This code is not longer maintained since newer versions of emacs have
;; this functionality built-in.

;; History:
;;   1998/12/02 - First version.
;;   2000/09/28 - Major overhaul... should work anywhere now.


;; Code:

(defvar fmf-is-windows (string-equal
                        (file-name-nondirectory shell-file-name)
                        "cmdproxy.exe")
  "Are we running on windows?")

(defvar fmf-shell-file-name (if fmf-is-windows
                                shell-file-name
                              "/bin/sh")
  "The name of the shell to use.")

(defvar fmf-override-shell t
  "Set to t to force the shell to be /bin/sh on unix (sometimes this is faster).
If nil, use shell-file-name")

(defvar fmf-directory-opts (if fmf-is-windows
                        "/b /a-d /on" ;; /n doesn't work on 95
                        "-1a")  ;; -d has given me problems!!
  "Options for directory program")

(defvar fmf-directory-command (if fmf-is-windows
                                  "dir"
                                "/bin/ls")
  "The command that will return a list of files one per line with no extra info.")

(defvar fmf-ignore-files-re (if fmf-is-windows
                                nil
                              "/$")
  "The regex to match output from fmf-directory-command to discard.")

(defun fmf-get-not-found-pattern ()
  "Get the pattern that the system returns when no file is found"
  (interactive)
  (let* ((buff (get-buffer-create "*fmf-shell-output-buffer*"))
         (notthere " fmf-this-file-should-never-exist1234")
         (command (concat fmf-directory-command " " fmf-directory-opts " " notthere)))
    (save-excursion
      (set-buffer buff)
      (erase-buffer)
      (call-process (if fmf-override-shell
                        fmf-shell-file-name
                      shell-file-name) nil t nil shell-command-switch command)
      (let ((str (buffer-string)))
        ;; Split the output at new lines
        (if (string-match notthere str)
          (setq str   (substring str (match-end 0))))
        (kill-buffer buff)
        (if (> (length str) 0)
            str
          nil)))
))

(defvar fmf-no-files-pattern (fmf-get-not-found-pattern)
  "The output when no files are found.")

(defun fmf-string-to-list (str)
  "Split the string into a list (split on newline)"
  (interactive)
  (let (item output-list)
    ;; Split the str at new lines
    (while (string-match "\n" str)
      (setq item  (substring str 0 (match-beginning 0))
            str   (substring str (match-end 0)))
      (if (and (not (null item))
               (> (length item) 0))
          (setq output-list (append output-list (list item)))))
    output-list))

(defun fmf-get-buffer-as-list ()
  "Get the current buffer as a list (split on newline)"
  (interactive)
  (fmf-string-to-list (buffer-string)))

(defun fmf-shell-command-to-list (command &optional dir the-shell the-opts)
  "Execute a command and put it's output into a list."
  (interactive)
  (if (or (null dir) (<= (length dir) 0))
      (setq dir default-directory))
  (or the-shell (setq the-shell (if fmf-override-shell
                                    fmf-shell-file-name
                                  shell-file-name)))
  (or the-opts (setq the-opts shell-command-switch))
  (cond ((fboundp 'shell-command-to-string)
         (let ((save-dir                default-directory)
               (shell-file-name         the-shell)
               (shell-command-to-string the-opts)
               list)
           (cd dir)
           (setq list (fmf-string-to-list (shell-command-to-string command)))
           (cd save-dir)
           list
         ))
        ((fboundp 'call-process)
         (let ((buff (get-buffer-create "*fmf-shell-output-buffer*"))
               error
               list)
           (save-excursion
             (set-buffer buff)
             (erase-buffer)
             (cd dir)
             (condition-case nil
                 (call-process the-shell nil t nil the-opts command)
               ((error quit exit)
                (setq error t)))
             (or error (setq list (fmf-get-buffer-as-list)))
             (kill-buffer buff))
           list))
  )
)

(defun fmf-pattern-extract (list &optional keep-re remove-re)
  "Apply keep-re to each item in list.  If it matches, keep it.
Apply remove-re to each item in list.  If it matches, discard it."
  (interactive)
  (let (output-list)
    (while list
      (let ((item (car list)))
           (if (and item
                    (or (null keep-re) (string-match keep-re item))
                    (or (null remove-re) (not (string-match remove-re item))))
               (setq output-list (append output-list (list item)))))
      (setq list (cdr list)))
    output-list
  )
)

(defun fmf-get-directory-via-insert-directory (pattern)
  "Get the directory that matches the pattern and return it in a list"
  (interactive)
  (let ((buff (get-buffer-create "*fmf-shell-output-buffer*"))
        (thedir (file-name-directory pattern))
        (shell-file-name (if fmf-override-shell
                             fmf-shell-file-name
                           shell-file-name))
        list
        error)
    (save-excursion
      (set-buffer buff)
      (erase-buffer)
      (condition-case nil
          (let ((insert-directory-program fmf-directory-command))
            (insert-directory pattern fmf-directory-opts t))
        ((error quit exit)
         (setq error t)))
      (or error (setq list (fmf-get-buffer-as-list))))
    (let ((case-fold-search t))
      (if (string-match "^total[ 	]+[0-9]+$" (car list))
          (setq list (cdr list))))
      (kill-buffer buff)
    (mapcar (lambda (arg) (concat thedir arg)) list))
)

(defun fmf-get-directory-via-shell-command (dirname pattern)
  "Get the directory that matches the pattern and return it in a list"
  (interactive)
  (let* ((command (concat fmf-directory-command " " fmf-directory-opts " " pattern))
         (list (fmf-shell-command-to-list command dirname (if fmf-override-shell
                                                              fmf-shell-file-name
                                                            shell-file-name))))
    (if (and fmf-no-files-pattern
             (string-match fmf-no-files-pattern (car list)))
        nil
      (mapcar (lambda (arg) (concat dirname arg)) list))))

(defun fmf-expand-wildcard (wildcard)
;;  The following is a good idea, if it worked.
;;  On Gnu Emacs 20.2.1, it returned a regular expression that also matched . (dot) files
;;  when given an argument of "*".  I want this function to return an equivalent regex
;;  that would behave _EXACTLY_ as the shell does (e.g. 'echo *' doesn't list dot files)
;;  (if (fboundp 'wildcard-to-regexp)
;;      (wildcard-to-regexp wildcard)
    (let ((first t))
      (concat "^"
              (mapconcat (lambda (y)
                           (let* ((x (char-to-string y))
                                  (y (cond ((string-equal x "*")
                                            (if first
                                                "[^.]*"
                                              ".*"))
                                           ((string-equal x ".") "\\.")
                                           ((string-equal x "?") 
                                            (if first
                                                "[^.]"
                                              "."))
                                           ((string-equal x "\\") "\\\\")
                                           (t x)
                                     )))
                             (setq first nil)
                             y
                           )
                         )
                         (string-to-list wildcard) "")
              "$")))

(defun fmf-get-directory-via-shell-command-2 (dirname pattern)
  "Get the directory that matches the pattern and return it in a list"
  (interactive)
  (let* ((command (concat fmf-directory-command " " fmf-directory-opts))
         (re-pattern (fmf-expand-wildcard pattern))
         (list (fmf-shell-command-to-list command dirname (if fmf-override-shell
                                                              fmf-shell-file-name
                                                            shell-file-name))))
    (if (and fmf-no-files-pattern
             (string-match fmf-no-files-pattern (car list)))
        (setq list nil)
      (setq list (fmf-pattern-extract list re-pattern fmf-ignore-files-re)))
    (mapcar (lambda (arg) (concat dirname arg)) list)))

(defun fmf-get-file-list (fname)
  "Return a list of files in or matching fname, a la 'ls'."
  (setq fname (expand-file-name fname))
  (let* ((dirname (file-name-directory fname))
         (pattern (file-name-nondirectory fname))
         (list
          (cond ((fboundp 'file-expand-wildcards)
                 (file-expand-wildcards fname t))
                ((fboundp 'directory-files)
                 (directory-files dirname t (fmf-expand-wildcard pattern)))
                ((and (not fmf-is-windows) ;; insert-directory is ugly on windows 95
                      (fboundp 'insert-directory))
                 (fmf-get-directory-via-insert-directory fname))
                (t
                 (fmf-get-directory-via-shell-command dirname fname))
                (t ;; never gets here
                 (fmf-get-directory-via-shell-command-2 dirname fname))
          )
         ))
    (mapcar 'expand-file-name list)
  )
)

(defun find-multiple-files (fname)
  "Open multiple files using shell-like wildcards.  Directories will be skipped."
  (interactive "FFind file(s): ")
  (let ((list (fmf-get-file-list fname))
        (first nil)
        (count 0))
    (if (and (null list)
             (not (string-match "[[*?]" fname)))
        ;; If no files were found to match and we didn't use a wildcard
        ;; Then we must be creating a new file....
        (find-file fname)
      ;; else
      (mapcar (lambda (arg)
                (if (not (file-directory-p arg))
                    (progn
                      (find-file arg)
                      (setq count (1+ count))
                      (or first (setq first (buffer-name))))))
              list)
      (if first
          (progn
            (switch-to-buffer first)
            (message "Opened %d file%s." count (if (> count 1) "s" ""))
          )
        (message "No matching files.")
      )
    )
  )
)

(provide 'fmf)
