summary refs log tree commit diff stats
path: root/compiler/pathutils.nim
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2018-09-07 01:53:09 +0200
committerAraq <rumpf_a@web.de>2018-09-07 19:21:16 +0200
commit86556ebfdbbd4b8e9edc9d3085ea21d6c0bae2e2 (patch)
tree9f8b4b752ed728ceff82dceef2f5605cb2a846a0 /compiler/pathutils.nim
parentb5730ec01f02e542eb06161430aa5a7c2ede775f (diff)
downloadNim-86556ebfdbbd4b8e9edc9d3085ea21d6c0bae2e2.tar.gz
compiler refactoring; use typesafe path handing; docgen: render symbols between modules
Diffstat (limited to 'compiler/pathutils.nim')
-rw-r--r--compiler/pathutils.nim254
1 files changed, 254 insertions, 0 deletions
diff --git a/compiler/pathutils.nim b/compiler/pathutils.nim
new file mode 100644
index 000000000..f84d964bb
--- /dev/null
+++ b/compiler/pathutils.nim
@@ -0,0 +1,254 @@
+#
+#
+#           The Nim Compiler
+#        (c) Copyright 2018 Andreas Rumpf
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## Path handling utilities for Nim. Strictly typed code in order
+## to avoid the never ending time sink in getting path handling right.
+## Might be a candidate for the stdlib later.
+
+import os, strutils
+
+type
+  AbsoluteFile* = distinct string
+  AbsoluteDir* = distinct string
+  RelativeFile* = distinct string
+  RelativeDir* = distinct string
+
+proc isEmpty*(x: AbsoluteFile): bool {.inline.} = x.string.len == 0
+proc isEmpty*(x: AbsoluteDir): bool {.inline.} = x.string.len == 0
+proc isEmpty*(x: RelativeFile): bool {.inline.} = x.string.len == 0
+proc isEmpty*(x: RelativeDir): bool {.inline.} = x.string.len == 0
+
+proc copyFile*(source, dest: AbsoluteFile) =
+  os.copyFile(source.string, dest.string)
+
+proc removeFile*(x: AbsoluteFile) {.borrow.}
+
+proc splitFile*(x: AbsoluteFile): tuple[dir: AbsoluteDir, name, ext: string] =
+  let (a, b, c) = splitFile(x.string)
+  result = (dir: AbsoluteDir(a), name: b, ext: c)
+
+proc extractFilename*(x: AbsoluteFile): string {.borrow.}
+
+proc fileExists*(x: AbsoluteFile): bool {.borrow.}
+proc dirExists*(x: AbsoluteDir): bool {.borrow.}
+
+proc quoteShell*(x: AbsoluteFile): string {.borrow.}
+proc quoteShell*(x: AbsoluteDir): string {.borrow.}
+
+proc cmpPaths*(x, y: AbsoluteDir): int {.borrow.}
+
+proc createDir*(x: AbsoluteDir) {.borrow.}
+
+type
+  PathIter = object
+    i, prev: int
+    notFirst: bool
+
+proc hasNext(it: PathIter; x: string): bool =
+  it.i < x.len
+
+proc next(it: var PathIter; x: string): (int, int) =
+  it.prev = it.i
+  if not it.notFirst and x[it.i] in {DirSep, AltSep}:
+    # absolute path:
+    inc it.i
+  else:
+    while it.i < x.len and x[it.i] notin {DirSep, AltSep}: inc it.i
+  if it.i > it.prev:
+    result = (it.prev, it.i-1)
+  elif hasNext(it, x):
+    result = next(it, x)
+
+  # skip all separators:
+  while it.i < x.len and x[it.i] in {DirSep, AltSep}: inc it.i
+  it.notFirst = true
+
+iterator dirs(x: string): (int, int) =
+  var it: PathIter
+  while hasNext(it, x): yield next(it, x)
+
+when false:
+  iterator dirs(x: string): (int, int) =
+    var i = 0
+    var first = true
+    while i < x.len:
+      let prev = i
+      if first and x[i] in {DirSep, AltSep}:
+        # absolute path:
+        inc i
+      else:
+        while i < x.len and x[i] notin {DirSep, AltSep}: inc i
+      if i > prev:
+        yield (prev, i-1)
+      first = false
+      # skip all separators:
+      while i < x.len and x[i] in {DirSep, AltSep}: inc i
+
+proc isDot(x: string; bounds: (int, int)): bool =
+  bounds[1] == bounds[0] and x[bounds[0]] == '.'
+
+proc isDotDot(x: string; bounds: (int, int)): bool =
+  bounds[1] == bounds[0] + 1 and x[bounds[0]] == '.' and x[bounds[0]+1] == '.'
+
+proc isSlash(x: string; bounds: (int, int)): bool =
+  bounds[1] == bounds[0] and x[bounds[0]] in {DirSep, AltSep}
+
+proc canon(x: string; result: var string; state: var int) =
+  # state: 0th bit set if isAbsolute path. Other bits count
+  # the number of path components.
+  for b in dirs(x):
+    if (state shr 1 == 0) and isSlash(x, b):
+      result.add DirSep
+      state = state or 1
+    elif result.len > (state and 1) and isDotDot(x, b):
+      var d = result.len
+      # f/..
+      while d > (state and 1) and result[d-1] != DirSep:
+        dec d
+      setLen(result, d)
+    elif isDot(x, b):
+      discard "discard the dot"
+    else:
+      if result.len > (state and 1): result.add DirSep
+      result.add substr(x, b[0], b[1])
+    inc state, 2
+
+proc canon(x: string): string =
+  # - Turn multiple slashes into single slashes.
+  # - Resolve '/foo/../bar' to '/bar'.
+  # - Remove './' from the path.
+  result = newStringOfCap(x.len)
+  var state = 0
+  canon(x, result, state)
+
+when FileSystemCaseSensitive:
+  template `!=?`(a, b: char): bool = toLowerAscii(a) != toLowerAscii(b)
+else:
+  template `!=?`(a, b: char): bool = a != b
+
+proc relativeTo(full, base: string; sep = DirSep): string =
+  if full.len == 0: return ""
+  var f, b: PathIter
+  var ff = (0, -1)
+  var bb = (0, -1) # (int, int)
+  result = newStringOfCap(full.len)
+  # skip the common prefix:
+  while f.hasNext(full) and b.hasNext(base):
+    ff = next(f, full)
+    bb = next(b, base)
+    let diff = ff[1] - ff[0]
+    if diff != bb[1] - bb[0]: break
+    var same = true
+    for i in 0..diff:
+      if full[i + ff[0]] !=? base[i + bb[0]]:
+        same = false
+        break
+    if not same: break
+    ff = (0, -1)
+    bb = (0, -1)
+  #  for i in 0..diff:
+  #    result.add base[i + bb[0]]
+
+  # /foo/bar/xxx/ -- base
+  # /foo/bar/baz  -- full path
+  #   ../baz
+  # every directory that is in 'base', needs to add '..'
+  while true:
+    if bb[1] >= bb[0]:
+      if result.len > 0 and result[^1] != sep:
+        result.add sep
+      result.add ".."
+    if not b.hasNext(base): break
+    bb = b.next(base)
+
+  # add the rest of 'full':
+  while true:
+    if ff[1] >= ff[0]:
+      if result.len > 0 and result[^1] != sep:
+        result.add sep
+      for i in 0..ff[1] - ff[0]:
+        result.add full[i + ff[0]]
+    if not f.hasNext(full): break
+    ff = f.next(full)
+
+when true:
+  proc eqImpl(x, y: string): bool =
+    when FileSystemCaseSensitive:
+      result = toLowerAscii(canon x) == toLowerAscii(canon y)
+    else:
+      result = canon(x) == canon(y)
+
+  proc `==`*(x, y: AbsoluteFile): bool = eqImpl(x.string, y.string)
+  proc `==`*(x, y: AbsoluteDir): bool = eqImpl(x.string, y.string)
+  proc `==`*(x, y: RelativeFile): bool = eqImpl(x.string, y.string)
+  proc `==`*(x, y: RelativeDir): bool = eqImpl(x.string, y.string)
+
+  proc `/`*(base: AbsoluteDir; f: RelativeFile): AbsoluteFile =
+    assert isAbsolute(base.string)
+    assert(not isAbsolute(f.string))
+    result = AbsoluteFile newStringOfCap(base.string.len + f.string.len)
+    var state = 0
+    canon(base.string, result.string, state)
+    canon(f.string, result.string, state)
+
+  proc `/`*(base: AbsoluteDir; f: RelativeDir): AbsoluteDir =
+    assert isAbsolute(base.string)
+    assert(not isAbsolute(f.string))
+    result = AbsoluteDir newStringOfCap(base.string.len + f.string.len)
+    var state = 0
+    canon(base.string, result.string, state)
+    canon(f.string, result.string, state)
+
+  proc relativeTo*(fullPath: AbsoluteFile, baseFilename: AbsoluteDir;
+                   sep = DirSep): RelativeFile =
+    RelativeFile(relativeTo(fullPath.string, baseFilename.string, sep))
+
+  proc toAbsolute*(file: string; base: AbsoluteDir): AbsoluteFile =
+    if isAbsolute(file): result = AbsoluteFile(file)
+    else: result = base / RelativeFile file
+
+  proc changeFileExt*(x: AbsoluteFile; ext: string): AbsoluteFile {.borrow.}
+  proc changeFileExt*(x: RelativeFile; ext: string): RelativeFile {.borrow.}
+
+  proc addFileExt*(x: AbsoluteFile; ext: string): AbsoluteFile {.borrow.}
+  proc addFileExt*(x: RelativeFile; ext: string): RelativeFile {.borrow.}
+
+  proc writeFile*(x: AbsoluteFile; content: string) {.borrow.}
+
+when isMainModule and defined(posix):
+  doAssert canon"/foo/../bar" == "/bar"
+  doAssert canon"foo/../bar" == "bar"
+
+  doAssert canon"/f/../bar///" == "/bar"
+  doAssert canon"f/..////bar" == "bar"
+
+  doAssert canon"../bar" == "../bar"
+  doAssert canon"/../bar" == "/../bar"
+
+  doAssert canon("foo/../../bar/") == "../bar"
+  doAssert canon("./bla/blob/") == "bla/blob"
+  doAssert canon(".hiddenFile") == ".hiddenFile"
+  doAssert canon("./bla/../../blob/./zoo.nim") == "../blob/zoo.nim"
+
+  doAssert canon("C:/file/to/this/long") == "C:/file/to/this/long"
+  doAssert canon("") == ""
+  doAssert canon("foobar") == "foobar"
+  doAssert canon("f/////////") == "f"
+
+  doAssert relativeTo("/foo/bar//baz.nim", "/foo") == "bar/baz.nim"
+
+  doAssert relativeTo("/Users/me/bar/z.nim", "/Users/other/bad") == "../../me/bar/z.nim"
+
+  doAssert relativeTo("/Users/me/bar/z.nim", "/Users/other") == "../me/bar/z.nim"
+  doAssert relativeTo("/Users///me/bar//z.nim", "//Users/") == "me/bar/z.nim"
+  doAssert relativeTo("/Users/me/bar/z.nim", "/Users/me") == "bar/z.nim"
+  doAssert relativeTo("", "/users/moo") == ""
+  doAssert relativeTo("foo", "") == "foo"
+
+  echo string(AbsoluteDir"/Users/me///" / RelativeFile"z.nim")