paths.nim 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. ## This module implements path handling.
  2. ##
  3. ## **See also:**
  4. ## * `files module <files.html>`_ for file access
  5. import std/private/osseps
  6. export osseps
  7. import std/envvars
  8. import std/private/osappdirs
  9. import pathnorm
  10. from std/private/ospaths2 import joinPath, splitPath,
  11. ReadDirEffect, WriteDirEffect,
  12. isAbsolute, relativePath,
  13. normalizePathEnd, isRelativeTo, parentDir,
  14. tailDir, isRootDir, parentDirs, `/../`,
  15. extractFilename, lastPathPart,
  16. changeFileExt, addFileExt, cmpPaths, splitFile,
  17. unixToNativePath, absolutePath, normalizeExe,
  18. normalizePath
  19. export ReadDirEffect, WriteDirEffect
  20. type
  21. Path* = distinct string
  22. func `==`*(x, y: Path): bool {.inline.} =
  23. ## Compares two paths.
  24. ##
  25. ## On a case-sensitive filesystem this is done
  26. ## case-sensitively otherwise case-insensitively.
  27. result = cmpPaths(x.string, y.string) == 0
  28. template endsWith(a: string, b: set[char]): bool =
  29. a.len > 0 and a[^1] in b
  30. func add(x: var string, tail: string) =
  31. var state = 0
  32. let trailingSep = tail.endsWith({DirSep, AltSep}) or tail.len == 0 and x.endsWith({DirSep, AltSep})
  33. normalizePathEnd(x, trailingSep=false)
  34. addNormalizePath(tail, x, state, DirSep)
  35. normalizePathEnd(x, trailingSep=trailingSep)
  36. func add*(x: var Path, y: Path) {.borrow.}
  37. func `/`*(head, tail: Path): Path {.inline.} =
  38. ## Joins two directory names to one.
  39. ##
  40. ## returns normalized path concatenation of `head` and `tail`, preserving
  41. ## whether or not `tail` has a trailing slash (or, if tail if empty, whether
  42. ## head has one).
  43. ##
  44. ## See also:
  45. ## * `splitPath proc`_
  46. ## * `uri.combine proc <uri.html#combine,Uri,Uri>`_
  47. ## * `uri./ proc <uri.html#/,Uri,string>`_
  48. Path(joinPath(head.string, tail.string))
  49. func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} =
  50. ## Splits a directory into `(head, tail)` tuple, so that
  51. ## ``head / tail == path`` (except for edge cases like "/usr").
  52. ##
  53. ## See also:
  54. ## * `add proc`_
  55. ## * `/ proc`_
  56. ## * `/../ proc`_
  57. ## * `relativePath proc`_
  58. let res = splitPath(path.string)
  59. result = (Path(res.head), Path(res.tail))
  60. func splitFile*(path: Path): tuple[dir, name: Path, ext: string] {.inline.} =
  61. ## Splits a filename into `(dir, name, extension)` tuple.
  62. ##
  63. ## `dir` does not end in DirSep unless it's `/`.
  64. ## `extension` includes the leading dot.
  65. ##
  66. ## If `path` has no extension, `ext` is the empty string.
  67. ## If `path` has no directory component, `dir` is the empty string.
  68. ## If `path` has no filename component, `name` and `ext` are empty strings.
  69. ##
  70. ## See also:
  71. ## * `extractFilename proc`_
  72. ## * `lastPathPart proc`_
  73. ## * `changeFileExt proc`_
  74. ## * `addFileExt proc`_
  75. let res = splitFile(path.string)
  76. result = (Path(res.dir), Path(res.name), res.ext)
  77. func isAbsolute*(path: Path): bool {.inline, raises: [].} =
  78. ## Checks whether a given `path` is absolute.
  79. ##
  80. ## On Windows, network paths are considered absolute too.
  81. result = isAbsolute(path.string)
  82. proc relativePath*(path, base: Path, sep = DirSep): Path {.inline.} =
  83. ## Converts `path` to a path relative to `base`.
  84. ##
  85. ## The `sep` (default: DirSep) is used for the path normalizations,
  86. ## this can be useful to ensure the relative path only contains `'/'`
  87. ## so that it can be used for URL constructions.
  88. ##
  89. ## On Windows, if a root of `path` and a root of `base` are different,
  90. ## returns `path` as is because it is impossible to make a relative path.
  91. ## That means an absolute path can be returned.
  92. ##
  93. ## See also:
  94. ## * `splitPath proc`_
  95. ## * `parentDir proc`_
  96. ## * `tailDir proc`_
  97. result = Path(relativePath(path.string, base.string, sep))
  98. proc isRelativeTo*(path: Path, base: Path): bool {.inline.} =
  99. ## Returns true if `path` is relative to `base`.
  100. result = isRelativeTo(path.string, base.string)
  101. func parentDir*(path: Path): Path {.inline.} =
  102. ## Returns the parent directory of `path`.
  103. ##
  104. ## This is similar to ``splitPath(path).head`` when ``path`` doesn't end
  105. ## in a dir separator, but also takes care of path normalizations.
  106. ## The remainder can be obtained with `lastPathPart(path) proc`_.
  107. ##
  108. ## See also:
  109. ## * `relativePath proc`_
  110. ## * `splitPath proc`_
  111. ## * `tailDir proc`_
  112. ## * `parentDirs iterator`_
  113. result = Path(parentDir(path.string))
  114. func tailDir*(path: Path): Path {.inline.} =
  115. ## Returns the tail part of `path`.
  116. ##
  117. ## See also:
  118. ## * `relativePath proc`_
  119. ## * `splitPath proc`_
  120. ## * `parentDir proc`_
  121. result = Path(tailDir(path.string))
  122. func isRootDir*(path: Path): bool {.inline.} =
  123. ## Checks whether a given `path` is a root directory.
  124. result = isRootDir(path.string)
  125. iterator parentDirs*(path: Path, fromRoot=false, inclusive=true): Path =
  126. ## Walks over all parent directories of a given `path`.
  127. ##
  128. ## If `fromRoot` is true (default: false), the traversal will start from
  129. ## the file system root directory.
  130. ## If `inclusive` is true (default), the original argument will be included
  131. ## in the traversal.
  132. ##
  133. ## Relative paths won't be expanded by this iterator. Instead, it will traverse
  134. ## only the directories appearing in the relative path.
  135. ##
  136. ## See also:
  137. ## * `parentDir proc`_
  138. ##
  139. for p in parentDirs(path.string, fromRoot, inclusive):
  140. yield Path(p)
  141. func `/../`*(head, tail: Path): Path {.inline.} =
  142. ## The same as ``parentDir(head) / tail``, unless there is no parent
  143. ## directory. Then ``head / tail`` is performed instead.
  144. ##
  145. ## See also:
  146. ## * `/ proc`_
  147. ## * `parentDir proc`_
  148. Path(`/../`(head.string, tail.string))
  149. func extractFilename*(path: Path): Path {.inline.} =
  150. ## Extracts the filename of a given `path`.
  151. ##
  152. ## This is the same as ``name & ext`` from `splitFile(path) proc`_.
  153. ##
  154. ## See also:
  155. ## * `splitFile proc`_
  156. ## * `lastPathPart proc`_
  157. ## * `changeFileExt proc`_
  158. ## * `addFileExt proc`_
  159. result = Path(extractFilename(path.string))
  160. func lastPathPart*(path: Path): Path {.inline.} =
  161. ## Like `extractFilename proc`_, but ignores
  162. ## trailing dir separator; aka: `baseName`:idx: in some other languages.
  163. ##
  164. ## See also:
  165. ## * `splitFile proc`_
  166. ## * `extractFilename proc`_
  167. ## * `changeFileExt proc`_
  168. ## * `addFileExt proc`_
  169. result = Path(lastPathPart(path.string))
  170. func changeFileExt*(filename: Path, ext: string): Path {.inline.} =
  171. ## Changes the file extension to `ext`.
  172. ##
  173. ## If the `filename` has no extension, `ext` will be added.
  174. ## If `ext` == "" then any extension is removed.
  175. ##
  176. ## `Ext` should be given without the leading `'.'`, because some
  177. ## filesystems may use a different character. (Although I know
  178. ## of none such beast.)
  179. ##
  180. ## See also:
  181. ## * `splitFile proc`_
  182. ## * `extractFilename proc`_
  183. ## * `lastPathPart proc`_
  184. ## * `addFileExt proc`_
  185. result = Path(changeFileExt(filename.string, ext))
  186. func addFileExt*(filename: Path, ext: string): Path {.inline.} =
  187. ## Adds the file extension `ext` to `filename`, unless
  188. ## `filename` already has an extension.
  189. ##
  190. ## `Ext` should be given without the leading `'.'`, because some
  191. ## filesystems may use a different character.
  192. ## (Although I know of none such beast.)
  193. ##
  194. ## See also:
  195. ## * `splitFile proc`_
  196. ## * `extractFilename proc`_
  197. ## * `lastPathPart proc`_
  198. ## * `changeFileExt proc`_
  199. result = Path(addFileExt(filename.string, ext))
  200. func unixToNativePath*(path: Path, drive=Path("")): Path {.inline.} =
  201. ## Converts an UNIX-like path to a native one.
  202. ##
  203. ## On an UNIX system this does nothing. Else it converts
  204. ## `'/'`, `'.'`, `'..'` to the appropriate things.
  205. ##
  206. ## On systems with a concept of "drives", `drive` is used to determine
  207. ## which drive label to use during absolute path conversion.
  208. ## `drive` defaults to the drive of the current working directory, and is
  209. ## ignored on systems that do not have a concept of "drives".
  210. result = Path(unixToNativePath(path.string, drive.string))
  211. proc getCurrentDir*(): Path {.inline, tags: [].} =
  212. ## Returns the `current working directory`:idx: i.e. where the built
  213. ## binary is run.
  214. ##
  215. ## So the path returned by this proc is determined at run time.
  216. ##
  217. ## See also:
  218. ## * `getHomeDir proc <appdirs.html#getHomeDir>`_
  219. ## * `getConfigDir proc <appdirs.html#getConfigDir>`_
  220. ## * `getTempDir proc <appdirs.html#getTempDir>`_
  221. ## * `setCurrentDir proc <dirs.html#setCurrentDir>`_
  222. ## * `currentSourcePath template <system.html#currentSourcePath.t>`_
  223. ## * `getProjectPath proc <macros.html#getProjectPath>`_
  224. result = Path(ospaths2.getCurrentDir())
  225. proc normalizeExe*(file: var Path) {.borrow.}
  226. proc normalizePath*(path: var Path) {.borrow.}
  227. proc normalizePathEnd*(path: var Path, trailingSep = false) {.borrow.}
  228. proc absolutePath*(path: Path, root = getCurrentDir()): Path =
  229. ## Returns the absolute path of `path`, rooted at `root` (which must be absolute;
  230. ## default: current directory).
  231. ## If `path` is absolute, return it, ignoring `root`.
  232. ##
  233. ## See also:
  234. ## * `normalizePath proc`_
  235. result = Path(absolutePath(path.string, root.string))
  236. proc expandTildeImpl(path: string): string {.
  237. tags: [ReadEnvEffect, ReadIOEffect].} =
  238. if len(path) == 0 or path[0] != '~':
  239. result = path
  240. elif len(path) == 1:
  241. result = getHomeDir()
  242. elif (path[1] in {DirSep, AltSep}):
  243. result = joinPath(getHomeDir(), path.substr(2))
  244. else:
  245. # TODO: handle `~bob` and `~bob/` which means home of bob
  246. result = path
  247. proc expandTilde*(path: Path): Path {.inline,
  248. tags: [ReadEnvEffect, ReadIOEffect].} =
  249. ## Expands ``~`` or a path starting with ``~/`` to a full path, replacing
  250. ## ``~`` with `getHomeDir() <appdirs.html#getHomeDir>`_ (otherwise returns ``path`` unmodified).
  251. ##
  252. ## Windows: this is still supported despite the Windows platform not having this
  253. ## convention; also, both ``~/`` and ``~\`` are handled.
  254. runnableExamples:
  255. import std/appdirs
  256. assert expandTilde(Path("~") / Path("appname.cfg")) == getHomeDir() / Path("appname.cfg")
  257. assert expandTilde(Path("~/foo/bar")) == getHomeDir() / Path("foo/bar")
  258. assert expandTilde(Path("/foo/bar")) == Path("/foo/bar")
  259. result = Path(expandTildeImpl(path.string))