racket/racket/collects/racket/path.rkt
Matthew Flatt e22a5da06c find-relative-path: case-normalize for comparison by default
This change affects programs only on Windows. For example, `C:\a\b`
relative to `c:\A\c` is `..\b`, instead of not relative.

Closes #1603
2017-03-24 20:00:03 -06:00

273 lines
11 KiB
Racket

#lang racket/base
(provide find-relative-path
simple-form-path
normalize-path
path-has-extension?
path-get-extension
filename-extension
file-name-from-path
path-only
some-system-path->string
string->some-system-path
path-element?
shrink-path-wrt)
(define (simple-form-path p)
(unless (path-string? p)
(raise-argument-error 'simple-form-path "path-string?" p))
(simplify-path (path->complete-path p)))
;; 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 found\n path: ~a" 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 found\n path: ~a" 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 (and (path-string? wrt) (complete-path? wrt))
(raise-argument-error 'normalize-path "(and/c path-string? complete-path?)" wrt))
(do-normalize-path orig-path wrt)])]
[error-not-a-dir
(lambda (path)
(error 'normalize-path
"element within the input path is not a directory or does not exist\n element: ~a"
path))]
[do-normalize-path
(lambda (orig-path wrt)
(let normalize ([path (cleanse-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\n root path: ~a"
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)))])]
[(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 (cleanse-path (resolve path))])
(cond
[(relative-path? resolved)
(normalize (build-path base resolved))]
[(complete-path? resolved)
resolved]
[else (path->complete-path resolved base)]))]))))])
normalize-path))
(define (do-explode-path who orig-path)
(define l (explode-path orig-path))
(for ([p (in-list l)])
(when (not (path-for-some-system? p))
(raise-argument-error who
"(and/c path-for-some-system? simple-form?)"
orig-path)))
l)
;; Arguments must be in simple form
(define (find-relative-path directory filename
#:more-than-root? [more-than-root? #f]
#:normalize-case? [normalize-case? #t])
(let ([dir (do-explode-path 'find-relative-path directory)]
[file (do-explode-path 'find-relative-path filename)]
[normalize (lambda (p)
(if normalize-case?
(normal-case-path p)
p))])
(if (and (equal? (normalize (car dir)) (normalize (car file)))
(or (not more-than-root?)
(not (eq? 'unix (path-convention-type directory)))
(null? (cdr dir))
(null? (cdr file))
(equal? (normalize (cadr dir)) (normalize (cadr 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/convention-type
(if (string? filename)
(system-path-convention-type)
(path-convention-type filename))
(map (lambda (x) 'up) dir))]
[(equal? (normalize (car dir)) (normalize (car file)))
(loop (cdr dir) (cdr file))]
[else
(apply build-path (append (map (lambda (x) 'up) dir) file))]))
filename)))
(define (file-name who name dir-ok?)
(unless (or (path-string? name)
(path-for-some-system? name))
(raise-argument-error who "(or/c path-string? path-for-some-system?)" name))
(let-values ([(base file dir?) (split-path name)])
(and (or dir-ok? (not dir?))
(path-for-some-system? file) file)))
(define (file-name-from-path name)
(file-name 'file-name-from-path name #f))
(define (path-only name)
(unless (or (path-string? name)
(path-for-some-system? name))
(raise-argument-error 'path-only "(or/c path-string? path-for-some-system?)" name))
(let-values ([(base file dir?) (split-path name)])
(cond [dir? (if (string? name) (string->path name) name)]
[(path-for-some-system? base) base]
[else #f])))
(define (path-has-extension? name sfx)
(unless (path-string? name)
(raise-argument-error 'path-extension=? "path-string?" name))
(unless (or (bytes? sfx) (string? sfx))
(raise-argument-error 'path-extension=? "(or/c bytes? string?)" name))
(let-values ([(base file dir?) (split-path name)])
(and base
(path? file)
(let* ([bs (path-element->bytes file)]
[sfx (if (bytes? sfx) sfx (string->bytes/utf-8 sfx))]
[len (bytes-length bs)]
[slen (bytes-length sfx)])
(and (len . > . slen)
(bytes=? sfx (subbytes bs (- len slen))))))))
(define (path-get-extension name)
(let* ([name (file-name 'path-get-extension name #t)]
[name (and name (path->bytes name))])
(cond [(and name (regexp-match #rx#"(?<=.)([.][^.]+)$" name)) => cadr]
[else #f])))
;; This old variant doesn't correctly handle filenames that start with ".":
(define (filename-extension name)
(let* ([name (file-name 'filename-extension name #f)]
[name (and name (path->bytes name))])
(cond [(and name (regexp-match #rx#"[.]([^.]+)$" name)) => cadr]
[else #f])))
(define (some-system-path->string path)
(unless (path-for-some-system? path)
(raise-argument-error 'some-system-path->string "path-for-some-system?" path))
(bytes->string/utf-8 (path->bytes path)))
(define (string->some-system-path path kind)
(unless (string? path)
(raise-argument-error 'string->some-system-path "string?" path))
(unless (or (eq? kind 'unix)
(eq? kind 'windows))
(raise-argument-error 'string->some-system-path "(or/c 'unix 'windows)" kind))
(bytes->path (string->bytes/utf-8 path) kind))
(define (path-element? path)
(and (path-for-some-system? path)
(let-values ([(base name d?) (split-path path)])
(and (eq? base 'relative)
(path-for-some-system? name)))))
(define (shrink-path-wrt fn other-fns)
(unless (path? fn)
(raise-argument-error
'shrink-path-wrt
"path?"
0 fn other-fns))
(unless (and (list? other-fns) (andmap path? other-fns))
(raise-argument-error
'shrink-path-wrt
"(listof path?)"
1 fn other-fns))
(define exp (reverse (explode-path fn)))
(define other-exps
(filter
(λ (x) (not (equal? exp x)))
(map (λ (fn) (reverse (explode-path fn)))
other-fns)))
(cond
[(null? other-exps) #f]
[else
(define size
(let loop ([other-exps other-exps]
[size 1])
(cond
[(null? other-exps) size]
[else (let ([new-size (find-exp-diff (car other-exps) exp)])
(loop (cdr other-exps)
(max new-size size)))])))
(apply build-path (reverse (take-n size exp)))]))
(define (take-n n lst)
(let loop ([n n]
[lst lst])
(cond
[(zero? n) null]
[(null? lst) null]
[else (cons (car lst) (loop (- n 1) (cdr lst)))])))
(define (find-exp-diff p1 p2)
(let loop ([p1 p1]
[p2 p2]
[i 1])
(cond
[(or (null? p1) (null? p2)) i]
[else (let ([f1 (car p1)]
[f2 (car p2)])
(if (equal? f1 f2)
(loop (cdr p1) (cdr p2) (+ i 1))
i))])))