(module file mzscheme (provide find-relative-path explode-path normalize-path build-absolute-path build-relative-path filename-extension file-name-from-path path-only delete-directory/files copy-directory/files make-directory* make-temporary-file find-library get-preference put-preferences call-with-input-file* call-with-output-file* fold-files find-files) (require "list.ss" "etc.ss") (define build-relative-path (lambda (p . args) (if (relative-path? p) (apply build-path p args) (error 'build-relative-path "base path ~s is absolute" p)))) (define build-absolute-path (lambda (p . args) (if (relative-path? p) (error 'build-absolute-path "base path ~s is relative" p) (apply build-path p args)))) ;; Note that normalize-path does not normalize the case (define normalize-path (letrec ([resolve-all (lambda (path wrt) (let ([orig-path (if (and wrt (not (complete-path? path))) (path->complete-path path wrt) path)]) (let loop ([full-path orig-path][seen-paths (list orig-path)]) (let ([resolved (resolve-path full-path)]) (if (equal? resolved full-path) (do-normalize-path resolved #f) (let ([path (if (relative-path? resolved) (build-path (let-values ([(base name dir?) (split-path full-path)]) base) resolved) resolved)]) (if (member path seen-paths) (error 'normalize-path "circular reference at ~s" path) (let ([spath ;; Use simplify-path to get rid of ..s, which can ;; allow the path to grow indefinitely in a cycle. ;; An exception must mean a cycle of links. (with-handlers ([exn:fail:filesystem? (lambda (x) (error 'normalize-path "circular reference at ~s" path))]) (simplify-path path))]) (loop spath (cons path seen-paths))))))))))] [resolve (lambda (path) (if (equal? path (resolve-path path)) path (resolve-all path #f)))] [normalize-path (case-lambda [(orig-path) (do-normalize-path orig-path (current-directory))] [(orig-path wrt) (unless (complete-path? wrt) (raise-type-error 'normalize-path "complete path" wrt)) (do-normalize-path orig-path wrt)])] [error-not-a-dir (lambda (path) (error 'normalize-path "~s (within the input path) is not a directory or does not exist" path))] [do-normalize-path (lambda (orig-path wrt) (let normalize ([path (expand-path orig-path)]) (let-values ([(base name dir?) (split-path path)]) (cond [(eq? name 'up) (let up ([base (if (eq? base 'relative) wrt (resolve-all base wrt))]) (if (directory-exists? base) (let-values ([(prev name dir?) (split-path base)]) (cond [(not prev) (error 'normalize-path "root has no parent directory: ~s" orig-path)] [else (let ([prev (if (eq? prev 'relative) wrt (normalize prev))]) (cond [(eq? name 'same) (up prev)] [(eq? name 'up) (up (up prev))] [else prev]))])) (error-not-a-dir base)))] [(eq? name 'same) (cond [(eq? base 'relative) wrt] [else (let ([n (normalize base)]) (if (directory-exists? n) n (error-not-a-dir n)))])] [else (cond [(not base) (path->complete-path path)] [else (let* ([base (if (eq? base 'relative) (normalize wrt) (normalize base))] [path (if (directory-exists? base) (build-path base name) (error-not-a-dir base))] [resolved (expand-path (resolve path))]) (cond [(relative-path? resolved) (normalize (build-path base resolved))] [(complete-path? resolved) resolved] [else (path->complete-path resolved base)]))])]))))]) normalize-path)) ;; Argument must be in normal form (define do-explode-path (lambda (who orig-path) (let loop ([path orig-path][rest '()]) (let-values ([(base name dir?) (split-path path)]) (when (or (and base (not (path? base))) (not (path? name))) (raise-type-error who "path in normal form" orig-path)) (if base (loop base (cons name rest)) (cons name rest)))))) (define explode-path (lambda (orig-path) (unless (path-string? orig-path) (raise-type-error 'explode-path "path or string" orig-path)) (do-explode-path 'explode-path orig-path))) ;; Arguments must be in normal form (define find-relative-path (lambda (directory filename) (let ([dir (do-explode-path 'find-relative-path directory)] [file (do-explode-path 'find-relative-path filename)]) (if (equal? (car dir) (car file)) (let loop ([dir (cdr dir)] [file (cdr file)]) (cond [(null? dir) (if (null? file) filename (apply build-path file))] [(null? file) (apply build-path (map (lambda (x) 'up) dir))] [(equal? (car dir) (car file)) (loop (cdr dir) (cdr file))] [else (apply build-path (append (map (lambda (x) 'up) dir) file))])) filename)))) (define file-name-from-path (lambda (name) (unless (path-string? name) (raise-type-error 'file-name-from-path "path or string" name)) (let-values ([(base file dir?) (split-path name)]) (if (and (not dir?) (path? file)) file #f)))) (define path-only (lambda (name) (unless (path-string? name) (raise-type-error 'path-only "path or string" name)) (let-values ([(base file dir?) (split-path name)]) (cond [dir? name] [(path? base) base] [else #f])))) ;; name can be any string; we just look for a dot (define filename-extension (lambda (name) (unless (path-string? name) (raise-type-error 'filename-extension "path or string" name)) (let ([name (if (path? name) (path->bytes name) name)]) (let ([m (regexp-match #rx#"[.]([^.]+)$" name)]) (and m (cadr m)))))) (define (delete-directory/files path) (unless (path-string? path) (raise-type-error 'delete-directory/files "path or string" path)) (cond [(or (link-exists? path) (file-exists? path)) (delete-file path)] [(directory-exists? path) (for-each (lambda (e) (delete-directory/files (build-path path e))) (directory-list path)) (delete-directory path)] [else (error 'delete-directory/files "encountered ~a, neither a file nor a directory" path)])) (define (copy-directory/files src dest) (cond [(file-exists? src) (copy-file src dest)] [(directory-exists? src) (make-directory dest) (for-each (lambda (e) (copy-directory/files (build-path src e) (build-path dest e))) (directory-list src))] [else (error 'copy-directory/files "encountered ~a, neither a file nor a directory" src)])) (define (make-directory* dir) (let-values ([(base name dir?) (split-path dir)]) (when (and (path? base) (not (directory-exists? base))) (make-directory* base)) (unless (directory-exists? dir) (make-directory dir)))) (define make-temporary-file (case-lambda [(template copy-from base-dir) (with-handlers ([exn:fail:contract? (lambda (x) (raise-type-error 'make-temporary-file "format string for 1 argument" template))]) (format template void)) (unless (or (not copy-from) (path-string? copy-from)) (raise-type-error 'make-temporary-file "path, valid-path string, or #f" copy-from)) (unless (or (not base-dir) (path-string? base-dir)) (raise-type-error 'make-temporary-file "path, valid-path, string, or #f" base-dir)) (let ([tmpdir (find-system-path 'temp-dir)]) (let loop ([s (current-seconds)][ms (current-milliseconds)]) (let ([name (let ([n (format template (format "~a~a" s ms))]) (cond [base-dir (build-path base-dir n)] [(relative-path? n) (build-path tmpdir n)] [else n]))]) (with-handlers ([exn:fail:filesystem:exists? (lambda (x) ;; try again with a new name (loop (- s (random 10)) (+ ms (random 10))))]) (if copy-from (copy-file copy-from name) (close-output-port (open-output-file name))) name))))] [(template copy-from) (make-temporary-file template copy-from #f)] [(template) (make-temporary-file template #f #f)] [() (make-temporary-file "mztmp~a" #f #f)])) (define find-library (case-lambda [(name) (find-library name "mzlib")] [(name collection . cp) (let ([dir (with-handlers ([exn:fail:filesystem? (lambda (exn) #f)]) (apply collection-path collection cp))]) (if dir (let ([file (build-path dir name)]) (if (file-exists? file) file #f)) #f))])) (define (with-pref-params thunk) (parameterize ((read-case-sensitive #f) (read-square-bracket-as-paren #t) (read-curly-brace-as-paren #t) (read-accept-box #t) (read-accept-compiled #f) (read-accept-bar-quote #t) (read-accept-graph #t) (read-decimal-as-inexact #t) (read-accept-dot #t) (read-accept-quasiquote #t) (read-accept-reader #f) (print-struct #f) (print-graph #t) (print-box #t) (print-vector-length #t) (current-readtable #f)) (thunk))) (define pref-box (make-weak-box #f)) ; non-weak box => need to save (define (get-prefs flush? filename) (let ([f (and (not flush?) (not filename) (weak-box-value pref-box))]) (or f (let ([f (let ([v (with-handlers ([exn:fail:filesystem? (lambda (x) null)]) (let ([pref-file (or filename (let ([f (find-system-path 'pref-file)]) (if (file-exists? f) ;; Using `file-exists?' means there's technically ;; a race condition, but something ;; has gone really wrong if the file disappears. f ;; Error here bails out through above `with-handlers' (build-path (collection-path "defaults") "plt-prefs.ss"))))]) (with-pref-params (lambda () (with-input-from-file pref-file read)))))]) ;; Make sure file content had the right shape: (if (and (list? v) (andmap (lambda (x) (and (pair? x) (pair? (cdr x)) (null? (cddr x)))) v)) v null))]) (unless filename (set! pref-box (make-weak-box f))) f)))) (define get-preference (case-lambda [(name fail-thunk refresh-cache? filename) (unless (symbol? name) (raise-type-error 'get-preference "symbol" name)) (unless (and (procedure? fail-thunk) (procedure-arity-includes? fail-thunk 0)) (raise-type-error 'get-preference "procedure (arity 0)" fail-thunk)) (let ([f (get-prefs refresh-cache? filename)]) (let ([m (assq name f)]) (if m (cadr m) (fail-thunk))))] [(name fail-thunk refresh-cache?) (get-preference name fail-thunk refresh-cache? #f)] [(name fail-thunk) (get-preference name fail-thunk #t #f)] [(name) (get-preference name (lambda () #f) #t #f)])) (define put-preferences (case-lambda [(names vals lock-there filename) (unless (and (list? names) (andmap symbol? names)) (raise-type-error 'put-preferences "list of symbols" names)) (unless (list? vals) (raise-type-error 'put-preferences "list" vals)) (unless (= (length names) (length vals)) (raise-mismatch-error 'put-preferences (format "the size of the name list (~a) does not match the size of the value list (~a): " (length names) (length vals)) vals)) (let-values ([(pref-file lock-file pref-dir) (let ([filename (or filename (find-system-path 'pref-file))]) (let-values ([(base name dir?) (split-path filename)]) (let ([dir (if (symbol? base) (current-directory) base)]) (unless (directory-exists? dir) (make-directory* dir)) (values filename (build-path dir (bytes->path (bytes-append (if (eq? 'windows (system-type)) #"_" #".") #"LOCK" (path->bytes name)))) dir))))]) (with-handlers ([exn:fail:filesystem:exists? (lambda (x) (lock-there lock-file))]) ;; Grab lock: (close-output-port (open-output-file lock-file 'error)) (dynamic-wind void (lambda () (let ([f (get-prefs #t filename)]) (for-each (lambda (name val) (let ([m (assq name f)]) (if m (set-car! (cdr m) val) (set! f (cons (list name val) f))))) names vals) (unless filename (set! pref-box (make-weak-box f))) ;; To write the file, copy the old one to a temporary name ;; (preserves permissions, etc), write to the temp file, ;; then move (atomicly) the temp file to the normal name. (let* ([tmp-file (make-temporary-file "TMPPREF~a" (and (file-exists? pref-file) pref-file) pref-dir)]) ;; If something goes wrong, try to delete the temp file. (with-handlers ([exn:fail? (lambda (exn) (with-handlers ([exn:fail:filesystem? void]) (delete-file tmp-file)) (raise exn))]) ;; Write to temp file... (with-output-to-file tmp-file (lambda () (with-pref-params (lambda () ;; If a pref value turns out to be unreadable, raise ;; an exception instead of creating a bad pref file. (parameterize ([print-unreadable #f]) ;; Poor man's pretty-print: one line per entry. (printf "(~n") (for-each (lambda (a) (if (list? (cadr a)) (begin (printf " (~s~n (~n" (car a)) (for-each (lambda (i) (printf " ~s~n" i)) (cadr a)) (printf " ))~n")) (printf " ~s~n" a))) f) (printf ")~n"))))) 'truncate/replace) (rename-file-or-directory tmp-file pref-file #t))))) (lambda () ;; Release lock: (delete-file lock-file)))))] [(names vals lock-there) (put-preferences names vals lock-there #f)] [(names vals) (put-preferences names vals (lambda (lock-file) (error 'put-preferences "some other process has the preference-file lock, as indicated by the existence of the lock file: ~e" lock-file)))])) (define call-with-input-file* (lambda (file thunk . flags) (let ([p (apply open-input-file file flags)]) (dynamic-wind void (lambda () (thunk p)) (lambda () (close-input-port p)))))) (define call-with-output-file* (lambda (file thunk . flags) (let ([p (apply open-output-file file flags)]) (dynamic-wind void (lambda () (thunk p)) (lambda () (close-output-port p)))))) ;; fold-files : (pathname sym alpha -> alpha) alpha pathname/#f -> alpha (define fold-files (opt-lambda (f init [path #f] [follow-links? #t]) ;; traverse-dir : string[directory] (listof string[file/directory]) -> (listof string[file/directory]) (define (traverse-dir dir base acc) (let loop ([subs (directory-list dir)] [acc acc]) (cond [(null? subs) acc] [else (loop (cdr subs) (let ([path (if base (build-path base (car subs)) (car subs))]) (traverse-file/dir path path acc)))]))) ;; traverse-file/dir : string[file/directory] (listof string[file/directory]) -> (listof string[file/directory]) (define (traverse-file/dir file/dir base acc) (cond [(and (not follow-links?) (link-exists? file/dir)) (f file/dir 'link acc)] [(directory-exists? file/dir) (traverse-dir file/dir base (if base (f file/dir 'dir acc) acc))] [else (f file/dir 'file acc)])) (traverse-file/dir (or path (current-directory)) path init))) (define find-files (opt-lambda (f [path #f]) (fold-files (lambda (path kind acc) (if (f path) (cons path acc) acc)) null path))))