;;AUTOLOAD: dofile
;;AUTODOC: dofile "process each argument file recursively"
;; (dofile (name-var stats-var dir-var file [keywords]) statements...)
;; (dofile (name-var stats-var dir-var '(files...) [keywords]) statements...)
;;
;; dofile recursively sets name-var to the file name, and stats-var to
;; the results of (file-stats name-var), and executes the statements in 
;; this context in a "progn" mode. By default, if file is a directory,
;; recurses into it.
;; name-var is the base name of the file (without its path), dir-var is set 
;; to the path leading to the current file from the directory where
;; dofile was called (which can be relative), and can be "" at top level.
;; Like dolist, name-var, stats-var, dir-var and statements are quoted, but 
;; file and keywords are evaluated.
;;
;; keywords can be:
;; :no-recurse t                   do not recurse in dirs (recurse is default)
;; :process-dirs t                 execute statements on dirs also (default
;;                                 only on files)
;; :do-not-follow-symbolic-links t do not follow symbolic links
;; :process-links t                execute statements on symlinks too 
;;                                 (default is to follow them, or to ignore
;;                                 them if above option is set)
;; :errors-as-nil t                for files we cannot open, default is to 
;;                                 print a message on stderr and skip it. This
;;                                 prints nothing and sets stats-var to nil
;; :errors-abort t                 This aborts in case of open errors
;; :sort-filter function           will call the function in each dir to sort
;;                                 files, with argument a list of pairs
;;                                 (filename stats) to be sorted in place
;;
;; Examples:
;; ;; find biggest file in home dir
;; (setqn biggest-size 0 biggest-file "NONE" biggest-dir "")
;; (dofile (name stats dir "~" :do-not-follow-symbolic-links t)
;;     (if (> (get stats 'size 0) biggest-size) 
;;         (setqn biggest-size (getn stats 'size) biggest-file name
;;             biggest-dir dir)
;; ))
;; (print-format "Biggest file in homedir: %0, %1 bytes\n"
;;     (concat-paths biggest-dir biggest-file) biggest-size)

(defunq dofile (dofile:vars &rest dofile:body &aux
    name-var stats-var dir-var 
    (files (list))
    no-recurse process-dirs do-not-follow-symbolic-links process-links
    errors-as-nil errors-abort sort-filter
  )
  ;;option parsing
  (if (not (and
	(typep dofile:vars List)
	(>= (length dofile:vars) 4)
	(typep (setq name-var (0 dofile:vars)) Atom)
	(typep (setq stats-var (1 dofile:vars)) Atom)
	(typep (setq dir-var (2 dofile:vars)) Atom)
    ))
    (error 'Errors:BadDoSyntax)
  )
  (setq files (eval (3 dofile:vars)))
  (if (not (typep files List)) (setq files (list files)))
  (dohash (key val (subseq dofile:vars 4))
    (if 
      (= :no-recurse key) (setq no-recurse (eval val))
      (= :process-dirs key) (setq process-dirs (eval val))
      (= :do-not-follow-symbolic-links key) (setq
	do-not-follow-symbolic-links (eval val))
      (= :process-links key) (setq process-links (eval val))
      (= :errors-as-nil key) (setq errors-as-nil (eval val))
      (= :errors-abort key) (setq errors-abort (eval val))
      (= :sort-filter key) (setq sort-filter (eval val))
      t
      (error 'Errors:BadDoSyntax)
  ))

  ;;now pre-"compile" a fast inner loop
  (setq dofile:do-file 
    (eval `(lambda (,name-var ,stats-var ,dir-var)
	,@dofile:body
  )))
  (setq dofile:do (if (and do-not-follow-symbolic-links (not process-links))
      dofile:do-testlinks dofile:do-notestlinks
  ))
  (dolist (file files)
    (if (= "" file)			;curdir
      (dofile:do-dir file () "")
      (dofile:do file (file-stats file do-not-follow-symbolic-links) "")
  ))
)

;; process a file or dir: dispatcher
(defun dofile:do-testlinks (file stats dir)
  ;;(PV "dofile:do" file stats)
  (if (= S_IFDIR (logand S_IFMT (get stats 'mode 0))) ;dir
    (dofile:do-dir file stats dir)
    stats (if (/= S_IFLNK (logand S_IFMT (get stats 'mode 0)))
      (dofile:do-file file stats dir)
    )
    ;; otherwise ignore links
    (dofile:error file dir "cannot get info")
))

(defun dofile:do-notestlinks (file stats dir)
  ;;(PV "dofile:do" file stats)
  (if (= S_IFDIR (logand S_IFMT (get stats 'mode 0))) ;dir
    (dofile:do-dir file stats dir)
    stats				;existing file
    (dofile:do-file file stats dir)
    ;; otherwise ignore links
    (dofile:error file dir "cannot get info")
))

(defun dofile:do (file stats dir &aux
  )
  ;;(PV "dofile:do" file stats)
  (if (= S_IFDIR (logand S_IFMT (get stats 'mode 0))) ;dir
    (dofile:do-dir file stats dir)
    stats				;existing file
    (if (or 
	(not do-not-follow-symbolic-links)
	(/= S_IFLNK (logand S_IFMT (get stats 'mode 0)))
	process-links
      )
      (dofile:do-file file stats dir)
      ;; otherwise ignore links
    )
    (dofile:error file dir "cannot get info")
))

;; cannot operate on file
(defun dofile:error (file dir reason)
  (if					;non-existing file or dir
    errors-abort
    (error 'Errors:ErrorOpeningFile file :read reason)
    errors-as-nil
    (dofile:do-file file () dir)
    (print-format *standard-error* 
      "dofile: error with file %0 in dir %1, %2\n" file dir reason)
))

;; process a dir and recurse in it
(defun dofile:do-dir (file stats dir &aux
    (curdir (if (= "" file) "" 
	(if (= "" dir) file (+ dir "/" file)))
    )
    (*current-directory* *current-directory*)
    (to-dir (if (= file "") "." file))
    pos
  )
  ;;(PV "dofile:do-dir" file stats dir)
  (if process-dirs
    (dofile:do-file file stats dir)
  )
  (if no-recurse ()
    (if (trap-error 'Errors:BadDirectory (setq *current-directory* to-dir))
      (if sort-filter
	(with (contents (list))
	  (dolist (f (directory)) 
	    (lappend contents (list f 
		(file-stats f do-not-follow-symbolic-links)))
	  )
	  (apply sort-filter (list contents))
	  (dolist (fs contents)
	    (dofile:do (0 fs) (1 fs) curdir)
	))
	(dolist (f (directory)) (dofile:do f 
	    (file-stats f do-not-follow-symbolic-links) curdir))
      )
      (dofile:error to-dir dir "cannot enter dir")	;error getting in dir
)))

