;;; Read from/Write to files ;;; ;;; Copyright (C) 1997-2000 by Sam Steingold ;;; This is Free Software, covered by the GNU GPL (v2) ;;; See http://www.gnu.org/copyleft/gpl.html ;;; ;;; $Id: fileio.lisp,v 1.24 2002/01/26 19:30:32 sds Exp $ ;;; $Source: /cvsroot/clocc/clocc/src/cllib/fileio.lisp,v $ (eval-when (compile load eval) (require :cllib-base (translate-logical-pathname "clocc:src;cllib;base")) ;; `index-t' (require :cllib-withtype (translate-logical-pathname "cllib:withtype")) ;; `+kwd+' (require :cllib-symb (translate-logical-pathname "cllib:symb")) ;; `with-timing', `mesg' (require :cllib-log (translate-logical-pathname "cllib:log"))) (in-package :cllib) (export '(file-size-t file-size rename-files save-restore count-sexps code-complexity load-compile-maybe write-list-to-stream write-list-to-file read-list-from-stream read-list-from-file pr write-to-file read-from-file read-from-stream append-to-file read-trim skip-to-line skip-search skip-blanks read-non-blanks)) ;;; ;;; {{{ file misc ;;; (eval-when (compile load eval) ; ACL (deftype file-size-t () '(unsigned-byte 32)) ) (declaim (ftype (function (t) file-size-t) file-size)) ;;;###autoload (defun file-size (fn) "Return the size of file named FN." (with-open-file (str fn :direction :input) (file-length str))) ;;;###autoload (defun rename-files (from to) "Rename file from wildcard FROM to wildcard TO. file:/usr/doc/lisp/HyperSpec/Body/fun_translate-pathname.html" (dolist (file (directory from)) (let ((new (translate-pathname file from to))) (format t "~a --> ~a~%" file new) (rename-file file new)))) ;;; ;;; }}}{{{ `code-complexity' ;;; ;;;###autoload (defun count-sexps (form) "Count the SEXPs in the form, quoted forms counting as 1." (if (or (atom form) (eq 'quote (car form))) 0 (do ((ff form (cdr ff)) (cc 1 (+ cc (count-sexps (car ff))))) ((atom ff) cc) (declare (type index-t cc))))) ;;;###autoload (defun code-complexity (&optional file) "Count the sexps in the file." (let ((errorp nil)) (typecase file ((or pathname string) (with-open-file (str file :direction :input) (values (loop :for form = (handler-case (read str nil +eof+) (error (err) (format t " *** problem: ~a~%" err) (setq errorp t) (return cc))) :until (eq form +eof+) :finally (return cc) :sum (count-sexps form) :into cc :of-type index-t) (file-length str) errorp))) (cons (loop :for fl :in file :with cc :of-type index-t = 0 :and cs :of-type file-size-t = 0 :and err :of-type boolean = nil :finally (format t " *** Total~20t~8:d forms, ~9:d bytes, ~7,2f bytes/form~%" tc ts (/ ts tc)) :do (setf (values cc cs err) (code-complexity fl)) (format t " * ~a~20t~8:d forms, ~9:d bytes, ~7,2f bytes/form~%" fl cc cs (/ cs cc)) (setq errorp (or errorp err)) :sum cc :into tc :of-type index-t :sum cs :into ts :of-type file-size-t :finally (return (values tc ts errorp)))) (t (error 'case-error :proc 'code-complexity :args (list 'file file 'pathname 'string 'cons)))))) ;;; ;;; }}}{{{ Read/Write list ;;; (declaim (ftype (function (list stream &optional function) (values fixnum)) write-list-to-stream)) (defun write-list-to-stream (lst stream &optional (print-function #'prin1)) "Write the list into the stream, printing the elements one per line. PRINT-FUNCTION should take (at least) 2 arguments: a record and a stream. PRIN1 is the default. Returns the length of the list." (declare (list lst) (stream stream) (type (function (t stream) t) print-function)) (do ((ll lst (cdr ll)) (len 0 (1+ len))) ((null ll) len) (declare (type index-t len)) (funcall print-function (car ll) stream) (terpri stream))) (declaim (ftype (function (list t &optional function) (values fixnum file-size-t)) write-list-to-file)) (defun write-list-to-file (lst fout &optional (print-function #'prin1)) "Write the list into the file, printing the elements one per line. Calls `write-list-to-stream', which see. Returns the number of records and the file size." (declare (list lst) (type (function (t stream) t) print-function)) (with-timing () (format t "~&Writing `~a'..." fout) (force-output) (with-open-file (stout fout :direction :output :if-exists :supersede) (let ((len (write-list-to-stream lst stout print-function)) (size (file-length stout))) (declare (fixnum len) (type file-size-t size)) (format t "done [~:d record~:p] [~:d byte~:p]" len size) (values len size))))) (defun read-list-from-stream (stream read-function &key args (package +kwd+) (read-ahead-function #'read) (eof +eof+) (readtable *readtable*)) "Read the input STREAM into the list, each line becomes a list element. READ-FUNCTION should take a stream and a read-ahead object as its arguments and return two values: the record read and a read-ahead object or EOF for end of file. ARGS are just passed to READ-FUNCTION. EOF is passed to `read' and checked against with `eq'. It defaults to `+eof+'. Return three values - the list read, its length, and the last element. `*package*' is bound to PACKAGE, which defaults to the KEYWORD package. If PACKAGE is not a package, a package with a generated name is created and deleted at the end of the reading. The first read-ahead object is read with READ-AHEAD-FUNCTION, which defaults to `read' and is called with 3 arguments - STREAM, NIL and EOF. `*readtable*' is bound to READTABLE (defaults to `*readtable*')." (declare (stream stream) (readtable readtable) (type (function (stream t t) (values t t)) read-function)) (let ((pack (if (packagep package) package (make-package (gensym "RLFS-PACK-"))))) (unwind-protect (do* ((*package* pack) (*readtable* readtable) (ra (funcall read-ahead-function stream nil eof)) new lst (len 0 (1+ len))) ((eq ra eof) (values (nreverse lst) len new)) (declare (type index-t len)) (setf (values new ra) (apply read-function stream ra args)) (push new lst)) (unless (packagep package) (delete-package pack))))) (defun read-list-from-file (fin read-function &rest args) "Read the file into the list. Calls `read-list-from-stream'. (read-list-from-file FILE-IN READ-FUNCTION EOF &rest ARGS)" (declare (type (function (stream t t) (values t t)) read-function) (type (or simple-string pathname) fin)) (with-timing () (multiple-value-bind (lst len last) (with-open-file (stin fin :direction :input :if-does-not-exist nil) (unless stin (return-from read-list-from-file (format t "~& *** Cannot open file `~a' for reading~%" fin))) (format t "~&Reading `~a' [~:d bytes]..." fin (file-length stin)) (force-output) (apply #'read-list-from-stream stin read-function args)) (format t "done [~:d record~:p]" len) (values lst len last)))) ;;; ;;; }}}{{{ Read/Write object ;;; ;;;###autoload (defun pr (obj &optional (str *standard-output*) (nice t)) "Print the OBJECT readably to the STREAM (default `*standard-output*'). Set `*print-circle*' and `*print-pretty*' to the third argument NICE (default T). Uses `with-standard-io-syntax'." (declare (stream str)) (with-standard-io-syntax (write obj :stream str :case :downcase :circle nice :pretty nice)) (values)) ;;;###autoload (defun write-to-file (obj file &optional (nice t) &rest comments) "Write the object to the file, readably. The optional third argument is passed to `pr'. Return the size of the file." (declare (type (or simple-string pathname) file)) (format t "Writing `~a'..." file) (force-output) (with-timing () (with-open-file (str file :direction :output :if-exists :supersede) (declare (stream str)) (format str ";; File: <~a - " file) (current-time str) (format str " ~a>~%;; Created by: ~a [~a] ;; *print-circle* = *print-pretty* = ~:[false~;true~]~%~{~a~}~2%" (getenv "USER") (lisp-implementation-type) (lisp-implementation-version) nice comments) (pr obj str nice) (format str "~2%;; file written [") (current-time str) (format str "]~%") (format t "done [~:d byte~:p]" (file-length str)) (file-length str)))) ;;;###autoload (defun read-from-stream (str &key (repeat t)) "Read from stream. The REPEAT keyword argument tells how many objects to read. If NIL, read once and return the object read; if a number, read that many times and return a list of objects read, if T (default), read until end of file and return a list of objects read." (declare (stream str)) (cond ((null repeat) (read str)) ((numberp repeat) (loop :repeat repeat :collect (read str))) ((loop :for obj = (read str nil +eof+) :until (eq obj +eof+) :collect obj)))) ;;;###autoload (defun read-from-file (file &key (readtable *readtable*) repeat (out *standard-output*)) "Read an object or several objects from a file. The READTABLE keyword argument (default `*readtable*') specifies the readtable to use. Passes REPEAT (default NIL) keyword argument to `read-from-stream'." (declare (type (or simple-string pathname) file)) (with-timing (:done t :out out) (with-open-file (str file :direction :input) (when out (format out "~&Reading `~a' [~:d bytes]..." file (file-length str)) (force-output out)) (with-standard-io-syntax (let ((*readtable* readtable)) (read-from-stream str :repeat repeat)))))) (defun append-to-file (file fmt &rest fmt-args) "Append to the file the formatted output." (declare (type (or simple-string pathname) file) (simple-string fmt)) (let ((*print-pretty* nil) (*print-length* nil)) (with-open-file (str file :direction :output :if-exists :append :if-does-not-exist :create) (declare (stream str)) (apply #'format str fmt fmt-args)))) ;;; ;;; }}}{{{ line-based input ;;; (declaim (ftype (function (stream) (values simple-string)) read-trim skip-blanks read-non-blanks)) (defsubst read-trim (stream) "Read a line from stream and trim it." (declare (type stream stream)) (string-trim +whitespace+ (read-line stream nil "."))) (defun skip-blanks (stream) "Read from STREAM first non-blank string is found." (declare (type stream stream)) (do ((st (read-trim stream) (read-trim stream))) ((/= 0 (length st)) st) (declare (simple-string st)))) (defun read-non-blanks (stream) "Read from STREAM through the first blank string." (declare (type stream stream)) (do* ((st (read-trim stream) (read-trim stream)) (res st (concatenate 'string res " " st))) ((zerop (length st)) res) (declare (simple-string st res)))) (declaim (ftype (function (stream simple-string &optional t) (values (or null simple-string))) skip-to-line skip-search)) (defun skip-to-line (st ln &optional out) "Read from stream ST until a line starting with LN. The optional third argument specifies where the message should go. By default nothing is printed." (declare (stream st) (simple-string ln)) (mesg :head out " +++ `skip-to-line' --> `~a'~%" ln) (do ((len (length ln)) (rr (read-line st) (read-line st))) ((and (>= (length rr) len) (string-equal ln rr :end2 len)) (subseq rr len)) (declare (fixnum len) (simple-string rr)))) (defun skip-search (stream string &optional out) "Read from STREAM until STRING is found by `search.'" (declare (stream stream) (simple-string string)) (mesg :head out " +++ `skip-search' --> `~a'~%" string) (do ((st (read-line stream nil nil) (read-line stream nil nil))) ((or (null st) (search string st :test #'char-equal)) st) (declare (type (or null simple-string) st)))) ;;; ;;; }}}{{{ `save-restore' ;;; (declaim (ftype (function (&optional integer) (values simple-string)) timestamp)) (defun timestamp (&optional (time (get-universal-time))) "Return the current time as a string without blanks." (declare (integer time)) (multiple-value-bind (se mi ho da mo ye) (decode-universal-time time) (format nil "~4,'0d-~2,'0d-~2,'0d_~2,'0d:~2,'0d:~2,'0d" ye mo da ho mi se))) (defun file-newer (f0 f1) "Return T if the first arg is newer than the second. Non-existent files are assumed to be VERY old." (flet ((fwd (ff) (or (ignore-errors (file-write-date ff)) 0))) (> (fwd f0) (fwd f1)))) ;;;###autoload (defun load-compile-maybe (file &key load-only-if-compiled force) "Compile the file if newer than the compiled and load it." (if force (load (compile-file file)) (let ((cf (compile-file-pathname file))) (if (file-newer file cf) (load (compile-file file)) (unless load-only-if-compiled (load cf)))))) (defsubst file-newest (f0 f1) "Returnt the newest of the two existing files." (if (> (file-write-date f0) (file-write-date f1)) f0 f1)) (defun latest-file (path &optional (nth 0)) "Return the latest file matching PATH, which should be wild. The optional second argument NTH (default - 0) specifies the which newest file to return. When it is NIL, return a list of conses (FILE . FILE-WRITE-DATE)" (let ((ll (sort (mapcan (lambda (ff) (let ((fwd (file-write-date ff))) (when fwd (list (cons ff fwd))))) (directory path)) #'> :key #'cdr))) (when ll (if nth (car (nth nth ll)) ll)))) (defun save-restore (what &key (name "~a") pre-save post-read var (voidp #'null) (basedir *datadir*) (readtable *readtable*)) "Save or read VAR into/from file. NAME is the name template, and should contain one `~a' format instruction if you want the filename to contain the timestamp; VAR is a symbol whose value is being saved; VOIDP is a predicate called on value of VAR (default - NULL) to determine whether to save or restore: if WHAT is non-NIL or VAR is VOIDP, then READ, otherwise - WRITE. When WHAT is non-NIL, it can determine the file to be read: as a pathname designator, it specifies the path, as a number, the n-th latest file (see `latest-file'), when neither - the latest file matching basedir/name* will be read. PRE-SAVE and POST-READ are functions called before save and after reading, they should be non-destructive and return the value to be written to the file and assigned to the variable respectively; READTABLE is passed to `read-from-file'. BASEDIR is the pathname relative to which NAME is expanded (`*datadir*')." (declare (simple-string name) (symbol var) (pathname basedir) (type (or function null) pre-save post-read) (function voidp)) (let ((val (symbol-value var))) (if (or what (funcall voidp val)) (setf (symbol-value var) (let ((vv (read-from-file (latest-file (if (or (stringp what) (pathnamep what)) what (merge-pathnames (format nil name "*") basedir)) (if (numberp what) what 0)) :readtable readtable))) (if post-read (funcall post-read vv) vv))) (write-to-file (if pre-save (funcall pre-save val) val) (merge-pathnames (format nil name (timestamp)) basedir))))) ;;; }}} (provide :cllib-fileio) ;;; file fileio.lisp ends here