pathutils.nim 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. #
  2. #
  3. # The Nim Compiler
  4. # (c) Copyright 2018 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## Path handling utilities for Nim. Strictly typed code in order
  10. ## to avoid the never ending time sink in getting path handling right.
  11. ## Might be a candidate for the stdlib later.
  12. import os, strutils
  13. type
  14. AbsoluteFile* = distinct string
  15. AbsoluteDir* = distinct string
  16. RelativeFile* = distinct string
  17. RelativeDir* = distinct string
  18. proc isEmpty*(x: AbsoluteFile): bool {.inline.} = x.string.len == 0
  19. proc isEmpty*(x: AbsoluteDir): bool {.inline.} = x.string.len == 0
  20. proc isEmpty*(x: RelativeFile): bool {.inline.} = x.string.len == 0
  21. proc isEmpty*(x: RelativeDir): bool {.inline.} = x.string.len == 0
  22. proc copyFile*(source, dest: AbsoluteFile) =
  23. os.copyFile(source.string, dest.string)
  24. proc removeFile*(x: AbsoluteFile) {.borrow.}
  25. proc splitFile*(x: AbsoluteFile): tuple[dir: AbsoluteDir, name, ext: string] =
  26. let (a, b, c) = splitFile(x.string)
  27. result = (dir: AbsoluteDir(a), name: b, ext: c)
  28. proc extractFilename*(x: AbsoluteFile): string {.borrow.}
  29. proc fileExists*(x: AbsoluteFile): bool {.borrow.}
  30. proc dirExists*(x: AbsoluteDir): bool {.borrow.}
  31. proc quoteShell*(x: AbsoluteFile): string {.borrow.}
  32. proc quoteShell*(x: AbsoluteDir): string {.borrow.}
  33. proc cmpPaths*(x, y: AbsoluteDir): int {.borrow.}
  34. proc createDir*(x: AbsoluteDir) {.borrow.}
  35. type
  36. PathIter = object
  37. i, prev: int
  38. notFirst: bool
  39. proc hasNext(it: PathIter; x: string): bool =
  40. it.i < x.len
  41. proc next(it: var PathIter; x: string): (int, int) =
  42. it.prev = it.i
  43. if not it.notFirst and x[it.i] in {DirSep, AltSep}:
  44. # absolute path:
  45. inc it.i
  46. else:
  47. while it.i < x.len and x[it.i] notin {DirSep, AltSep}: inc it.i
  48. if it.i > it.prev:
  49. result = (it.prev, it.i-1)
  50. elif hasNext(it, x):
  51. result = next(it, x)
  52. # skip all separators:
  53. while it.i < x.len and x[it.i] in {DirSep, AltSep}: inc it.i
  54. it.notFirst = true
  55. iterator dirs(x: string): (int, int) =
  56. var it: PathIter
  57. while hasNext(it, x): yield next(it, x)
  58. when false:
  59. iterator dirs(x: string): (int, int) =
  60. var i = 0
  61. var first = true
  62. while i < x.len:
  63. let prev = i
  64. if first and x[i] in {DirSep, AltSep}:
  65. # absolute path:
  66. inc i
  67. else:
  68. while i < x.len and x[i] notin {DirSep, AltSep}: inc i
  69. if i > prev:
  70. yield (prev, i-1)
  71. first = false
  72. # skip all separators:
  73. while i < x.len and x[i] in {DirSep, AltSep}: inc i
  74. proc isDot(x: string; bounds: (int, int)): bool =
  75. bounds[1] == bounds[0] and x[bounds[0]] == '.'
  76. proc isDotDot(x: string; bounds: (int, int)): bool =
  77. bounds[1] == bounds[0] + 1 and x[bounds[0]] == '.' and x[bounds[0]+1] == '.'
  78. proc isSlash(x: string; bounds: (int, int)): bool =
  79. bounds[1] == bounds[0] and x[bounds[0]] in {DirSep, AltSep}
  80. proc canon(x: string; result: var string; state: var int) =
  81. # state: 0th bit set if isAbsolute path. Other bits count
  82. # the number of path components.
  83. for b in dirs(x):
  84. if (state shr 1 == 0) and isSlash(x, b):
  85. result.add DirSep
  86. state = state or 1
  87. elif result.len > (state and 1) and isDotDot(x, b):
  88. var d = result.len
  89. # f/..
  90. while d > (state and 1) and result[d-1] != DirSep:
  91. dec d
  92. if d > 0: setLen(result, d-1)
  93. elif isDot(x, b):
  94. discard "discard the dot"
  95. elif b[1] >= b[0]:
  96. if result.len > 0 and result[^1] != DirSep:
  97. result.add DirSep
  98. result.add substr(x, b[0], b[1])
  99. inc state, 2
  100. proc canon(x: string): string =
  101. # - Turn multiple slashes into single slashes.
  102. # - Resolve '/foo/../bar' to '/bar'.
  103. # - Remove './' from the path.
  104. result = newStringOfCap(x.len)
  105. var state = 0
  106. canon(x, result, state)
  107. when FileSystemCaseSensitive:
  108. template `!=?`(a, b: char): bool = toLowerAscii(a) != toLowerAscii(b)
  109. else:
  110. template `!=?`(a, b: char): bool = a != b
  111. proc relativeTo(full, base: string; sep = DirSep): string =
  112. if full.len == 0: return ""
  113. var f, b: PathIter
  114. var ff = (0, -1)
  115. var bb = (0, -1) # (int, int)
  116. result = newStringOfCap(full.len)
  117. # skip the common prefix:
  118. while f.hasNext(full) and b.hasNext(base):
  119. ff = next(f, full)
  120. bb = next(b, base)
  121. let diff = ff[1] - ff[0]
  122. if diff != bb[1] - bb[0]: break
  123. var same = true
  124. for i in 0..diff:
  125. if full[i + ff[0]] !=? base[i + bb[0]]:
  126. same = false
  127. break
  128. if not same: break
  129. ff = (0, -1)
  130. bb = (0, -1)
  131. # for i in 0..diff:
  132. # result.add base[i + bb[0]]
  133. # /foo/bar/xxx/ -- base
  134. # /foo/bar/baz -- full path
  135. # ../baz
  136. # every directory that is in 'base', needs to add '..'
  137. while true:
  138. if bb[1] >= bb[0]:
  139. if result.len > 0 and result[^1] != sep:
  140. result.add sep
  141. result.add ".."
  142. if not b.hasNext(base): break
  143. bb = b.next(base)
  144. # add the rest of 'full':
  145. while true:
  146. if ff[1] >= ff[0]:
  147. if result.len > 0 and result[^1] != sep:
  148. result.add sep
  149. for i in 0..ff[1] - ff[0]:
  150. result.add full[i + ff[0]]
  151. if not f.hasNext(full): break
  152. ff = f.next(full)
  153. when true:
  154. proc eqImpl(x, y: string): bool =
  155. when FileSystemCaseSensitive:
  156. result = cmpIgnoreCase(canon x, canon y) == 0
  157. else:
  158. result = canon(x) == canon(y)
  159. proc `==`*(x, y: AbsoluteFile): bool = eqImpl(x.string, y.string)
  160. proc `==`*(x, y: AbsoluteDir): bool = eqImpl(x.string, y.string)
  161. proc `==`*(x, y: RelativeFile): bool = eqImpl(x.string, y.string)
  162. proc `==`*(x, y: RelativeDir): bool = eqImpl(x.string, y.string)
  163. proc `/`*(base: AbsoluteDir; f: RelativeFile): AbsoluteFile =
  164. #assert isAbsolute(base.string)
  165. assert(not isAbsolute(f.string))
  166. result = AbsoluteFile newStringOfCap(base.string.len + f.string.len)
  167. var state = 0
  168. canon(base.string, result.string, state)
  169. canon(f.string, result.string, state)
  170. proc `/`*(base: AbsoluteDir; f: RelativeDir): AbsoluteDir =
  171. #assert isAbsolute(base.string)
  172. assert(not isAbsolute(f.string))
  173. result = AbsoluteDir newStringOfCap(base.string.len + f.string.len)
  174. var state = 0
  175. canon(base.string, result.string, state)
  176. canon(f.string, result.string, state)
  177. proc relativeTo*(fullPath: AbsoluteFile, baseFilename: AbsoluteDir;
  178. sep = DirSep): RelativeFile =
  179. RelativeFile(relativeTo(fullPath.string, baseFilename.string, sep))
  180. proc toAbsolute*(file: string; base: AbsoluteDir): AbsoluteFile =
  181. if isAbsolute(file): result = AbsoluteFile(file)
  182. else: result = base / RelativeFile file
  183. proc changeFileExt*(x: AbsoluteFile; ext: string): AbsoluteFile {.borrow.}
  184. proc changeFileExt*(x: RelativeFile; ext: string): RelativeFile {.borrow.}
  185. proc addFileExt*(x: AbsoluteFile; ext: string): AbsoluteFile {.borrow.}
  186. proc addFileExt*(x: RelativeFile; ext: string): RelativeFile {.borrow.}
  187. proc writeFile*(x: AbsoluteFile; content: string) {.borrow.}
  188. when isMainModule and defined(posix):
  189. doAssert canon"/foo/../bar" == "/bar"
  190. doAssert canon"foo/../bar" == "bar"
  191. doAssert canon"/f/../bar///" == "/bar"
  192. doAssert canon"f/..////bar" == "bar"
  193. doAssert canon"../bar" == "../bar"
  194. doAssert canon"/../bar" == "/../bar"
  195. doAssert canon("foo/../../bar/") == "../bar"
  196. doAssert canon("./bla/blob/") == "bla/blob"
  197. doAssert canon(".hiddenFile") == ".hiddenFile"
  198. doAssert canon("./bla/../../blob/./zoo.nim") == "../blob/zoo.nim"
  199. doAssert canon("C:/file/to/this/long") == "C:/file/to/this/long"
  200. doAssert canon("") == ""
  201. doAssert canon("foobar") == "foobar"
  202. doAssert canon("f/////////") == "f"
  203. doAssert relativeTo("/foo/bar//baz.nim", "/foo") == "bar/baz.nim"
  204. doAssert relativeTo("/Users/me/bar/z.nim", "/Users/other/bad") == "../../me/bar/z.nim"
  205. doAssert relativeTo("/Users/me/bar/z.nim", "/Users/other") == "../me/bar/z.nim"
  206. doAssert relativeTo("/Users///me/bar//z.nim", "//Users/") == "me/bar/z.nim"
  207. doAssert relativeTo("/Users/me/bar/z.nim", "/Users/me") == "bar/z.nim"
  208. doAssert relativeTo("", "/users/moo") == ""
  209. doAssert relativeTo("foo", "") == "foo"
  210. doAssert AbsoluteDir"/Users/me///" / RelativeFile"z.nim" == AbsoluteFile"/Users/me/z.nim"
  211. doAssert relativeTo("/foo/bar.nim", "/foo/") == "bar.nim"
  212. when isMainModule and defined(windows):
  213. let nasty = string(AbsoluteDir(r"C:\Users\rumpf\projects\nim\tests\nimble\nimbleDir\linkedPkgs\pkgB-#head\../../simplePkgs/pkgB-#head/") / RelativeFile"pkgA/module.nim")
  214. doAssert nasty == r"C:\Users\rumpf\projects\nim\tests\nimble\nimbleDir\simplePkgs\pkgB-#head\pkgA\module.nim"