summary refs log tree commit diff stats
path: root/lib/std/paths.nim
diff options
context:
space:
mode:
authorringabout <43030857+ringabout@users.noreply.github.com>2022-10-22 03:53:44 +0800
committerGitHub <noreply@github.com>2022-10-21 21:53:44 +0200
commit3c12b72168d9fe9916f471fa17d2c24c52b33066 (patch)
tree067a76c67e104b2d4d1e31bc9d0ea4b193824233 /lib/std/paths.nim
parent66cbcaab8474f5ff3480e7a9bc55df249548a90c (diff)
downloadNim-3c12b72168d9fe9916f471fa17d2c24c52b33066.tar.gz
add typesafe `std/paths`, `std/files`, `std/dirs`, `std/symlinks` (#20582)
* split std/os; add typesafe std/paths
* add more files, dirs, paths
* add documentation
* add testcase
* remove tryRemoveFile
* clean up
* Delete test.nim
* apply changes
* add `add` and fixes
Diffstat (limited to 'lib/std/paths.nim')
-rw-r--r--lib/std/paths.nim270
1 files changed, 270 insertions, 0 deletions
diff --git a/lib/std/paths.nim b/lib/std/paths.nim
new file mode 100644
index 000000000..b3cd7de83
--- /dev/null
+++ b/lib/std/paths.nim
@@ -0,0 +1,270 @@
+import std/private/osseps
+export osseps
+
+import pathnorm
+
+from std/private/ospaths2 import  joinPath, splitPath,
+                                  ReadDirEffect, WriteDirEffect,
+                                  isAbsolute, relativePath,
+                                  normalizePathEnd, isRelativeTo, parentDir,
+                                  tailDir, isRootDir, parentDirs, `/../`,
+                                  extractFilename, lastPathPart,
+                                  changeFileExt, addFileExt, cmpPaths, splitFile,
+                                  unixToNativePath, absolutePath, normalizeExe,
+                                  normalizePath
+export ReadDirEffect, WriteDirEffect
+
+type
+  Path* = distinct string
+
+template endsWith(a: string, b: set[char]): bool =
+  a.len > 0 and a[^1] in b
+
+func add(x: var string, tail: string) =
+  var state = 0
+  let trailingSep = tail.endsWith({DirSep, AltSep}) or tail.len == 0 and x.endsWith({DirSep, AltSep})
+  normalizePathEnd(x, trailingSep=false)
+  addNormalizePath(tail, x, state, DirSep)
+  normalizePathEnd(x, trailingSep=trailingSep)
+
+func add*(x: var Path, y: Path) {.borrow.}
+
+func `/`*(head, tail: Path): Path {.inline.} =
+  ## Joins two directory names to one.
+  ##
+  ## returns normalized path concatenation of `head` and `tail`, preserving
+  ## whether or not `tail` has a trailing slash (or, if tail if empty, whether
+  ## head has one).
+  ##
+  ## See also:
+  ## * `splitPath proc`_
+  ## * `uri.combine proc <uri.html#combine,Uri,Uri>`_
+  ## * `uri./ proc <uri.html#/,Uri,string>`_
+  Path(joinPath(head.string, tail.string))
+
+func splitPath*(path: Path): tuple[head, tail: Path] {.inline.} =
+  ## Splits a directory into `(head, tail)` tuple, so that
+  ## ``head / tail == path`` (except for edge cases like "/usr").
+  ##
+  ## See also:
+  ## * `add proc`_
+  ## * `/ proc`_
+  ## * `/../ proc`_
+  ## * `relativePath proc`_
+  let res = splitPath(path.string)
+  result = (Path(res.head), Path(res.tail))
+
+func splitFile*(path: Path): tuple[dir, name: Path, ext: string] {.inline.} =
+  ## Splits a filename into `(dir, name, extension)` tuple.
+  ##
+  ## `dir` does not end in DirSep_ unless it's `/`.
+  ## `extension` includes the leading dot.
+  ##
+  ## If `path` has no extension, `ext` is the empty string.
+  ## If `path` has no directory component, `dir` is the empty string.
+  ## If `path` has no filename component, `name` and `ext` are empty strings.
+  ##
+  ## See also:
+  ## * `extractFilename proc`_
+  ## * `lastPathPart proc`_
+  ## * `changeFileExt proc`_
+  ## * `addFileExt proc`_
+  let res = splitFile(path.string)
+  result = (Path(res.dir), Path(res.name), res.ext)
+
+func isAbsolute*(path: Path): bool {.inline, raises: [].} =
+  ## Checks whether a given `path` is absolute.
+  ##
+  ## On Windows, network paths are considered absolute too.
+  result = isAbsolute(path.string)
+
+proc relativePath*(path, base: Path, sep = DirSep): Path {.inline.} =
+  ## Converts `path` to a path relative to `base`.
+  ##
+  ## The `sep` (default: DirSep_) is used for the path normalizations,
+  ## this can be useful to ensure the relative path only contains `'/'`
+  ## so that it can be used for URL constructions.
+  ##
+  ## On Windows, if a root of `path` and a root of `base` are different,
+  ## returns `path` as is because it is impossible to make a relative path.
+  ## That means an absolute path can be returned.
+  ##
+  ## See also:
+  ## * `splitPath proc`_
+  ## * `parentDir proc`_
+  ## * `tailDir proc`_
+  result = Path(relativePath(path.string, base.string, sep))
+
+proc isRelativeTo*(path: Path, base: Path): bool {.inline.} =
+  ## Returns true if `path` is relative to `base`.
+  result = isRelativeTo(path.string, base.string)
+
+
+func parentDir*(path: Path): Path {.inline.} =
+  ## Returns the parent directory of `path`.
+  ##
+  ## This is similar to ``splitPath(path).head`` when ``path`` doesn't end
+  ## in a dir separator, but also takes care of path normalizations.
+  ## The remainder can be obtained with `lastPathPart(path) proc`_.
+  ##
+  ## See also:
+  ## * `relativePath proc`_
+  ## * `splitPath proc`_
+  ## * `tailDir proc`_
+  ## * `parentDirs iterator`_
+  result = Path(parentDir(path.string))
+
+func tailDir*(path: Path): Path {.inline.} =
+  ## Returns the tail part of `path`.
+  ##
+  ## See also:
+  ## * `relativePath proc`_
+  ## * `splitPath proc`_
+  ## * `parentDir proc`_
+  result = Path(tailDir(path.string))
+
+func isRootDir*(path: Path): bool {.inline.} =
+  ## Checks whether a given `path` is a root directory.
+  result = isRootDir(path.string)
+
+iterator parentDirs*(path: Path, fromRoot=false, inclusive=true): Path =
+  ## Walks over all parent directories of a given `path`.
+  ##
+  ## If `fromRoot` is true (default: false), the traversal will start from
+  ## the file system root directory.
+  ## If `inclusive` is true (default), the original argument will be included
+  ## in the traversal.
+  ##
+  ## Relative paths won't be expanded by this iterator. Instead, it will traverse
+  ## only the directories appearing in the relative path.
+  ##
+  ## See also:
+  ## * `parentDir proc`_
+  ##
+  for p in parentDirs(path.string, fromRoot, inclusive):
+    yield Path(p)
+
+func `/../`*(head, tail: Path): Path {.inline.} =
+  ## The same as ``parentDir(head) / tail``, unless there is no parent
+  ## directory. Then ``head / tail`` is performed instead.
+  ##
+  ## See also:
+  ## * `/ proc`_
+  ## * `parentDir proc`_
+  Path(`/../`(head.string, tail.string))
+
+func extractFilename*(path: Path): Path {.inline.} =
+  ## Extracts the filename of a given `path`.
+  ##
+  ## This is the same as ``name & ext`` from `splitFile(path) proc`_.
+  ##
+  ## See also:
+  ## * `splitFile proc`_
+  ## * `lastPathPart proc`_
+  ## * `changeFileExt proc`_
+  ## * `addFileExt proc`_
+  result = Path(extractFilename(path.string))
+
+func lastPathPart*(path: Path): Path {.inline.} =
+  ## Like `extractFilename proc`_, but ignores
+  ## trailing dir separator; aka: `baseName`:idx: in some other languages.
+  ##
+  ## See also:
+  ## * `splitFile proc`_
+  ## * `extractFilename proc`_
+  ## * `changeFileExt proc`_
+  ## * `addFileExt proc`_
+  result = Path(lastPathPart(path.string))
+
+func changeFileExt*(filename: Path, ext: string): Path {.inline.} =
+  ## Changes the file extension to `ext`.
+  ##
+  ## If the `filename` has no extension, `ext` will be added.
+  ## If `ext` == "" then any extension is removed.
+  ##
+  ## `Ext` should be given without the leading `'.'`, because some
+  ## filesystems may use a different character. (Although I know
+  ## of none such beast.)
+  ##
+  ## See also:
+  ## * `splitFile proc`_
+  ## * `extractFilename proc`_
+  ## * `lastPathPart proc`_
+  ## * `addFileExt proc`_
+  result = Path(changeFileExt(filename.string, ext))
+
+func addFileExt*(filename: Path, ext: string): Path {.inline.} =
+  ## Adds the file extension `ext` to `filename`, unless
+  ## `filename` already has an extension.
+  ##
+  ## `Ext` should be given without the leading `'.'`, because some
+  ## filesystems may use a different character.
+  ## (Although I know of none such beast.)
+  ##
+  ## See also:
+  ## * `splitFile proc`_
+  ## * `extractFilename proc`_
+  ## * `lastPathPart proc`_
+  ## * `changeFileExt proc`_
+  result = Path(addFileExt(filename.string, ext))
+
+func cmpPaths*(pathA, pathB: Path): int {.inline.} =
+  ## Compares two paths.
+  ##
+  ## On a case-sensitive filesystem this is done
+  ## case-sensitively otherwise case-insensitively. Returns:
+  ##
+  ## | 0 if pathA == pathB
+  ## | < 0 if pathA < pathB
+  ## | > 0 if pathA > pathB
+  result = cmpPaths(pathA.string, pathB.string)
+
+func unixToNativePath*(path: Path, drive=Path("")): Path {.inline.} =
+  ## Converts an UNIX-like path to a native one.
+  ##
+  ## On an UNIX system this does nothing. Else it converts
+  ## `'/'`, `'.'`, `'..'` to the appropriate things.
+  ##
+  ## On systems with a concept of "drives", `drive` is used to determine
+  ## which drive label to use during absolute path conversion.
+  ## `drive` defaults to the drive of the current working directory, and is
+  ## ignored on systems that do not have a concept of "drives".
+  result = Path(unixToNativePath(path.string, drive.string))
+
+proc getCurrentDir*(): Path {.inline, tags: [].} =
+  ## Returns the `current working directory`:idx: i.e. where the built
+  ## binary is run.
+  ##
+  ## So the path returned by this proc is determined at run time.
+  ##
+  ## See also:
+  ## * `getHomeDir proc`_
+  ## * `getConfigDir proc`_
+  ## * `getTempDir proc`_
+  ## * `setCurrentDir proc`_
+  ## * `currentSourcePath template <system.html#currentSourcePath.t>`_
+  ## * `getProjectPath proc <macros.html#getProjectPath>`_
+  result = Path(ospaths2.getCurrentDir())
+
+proc setCurrentDir*(newDir: Path) {.inline, tags: [].} =
+  ## Sets the `current working directory`:idx:; `OSError`
+  ## is raised if `newDir` cannot been set.
+  ##
+  ## See also:
+  ## * `getCurrentDir proc`_
+  ospaths2.setCurrentDir(newDir.string)
+
+proc normalizeExe*(file: var Path) {.borrow.}
+
+proc normalizePath*(path: var Path) {.borrow.}
+
+proc normalizePathEnd*(path: var Path, trailingSep = false) {.borrow.}
+
+proc absolutePath*(path: Path, root = getCurrentDir()): Path =
+  ## Returns the absolute path of `path`, rooted at `root` (which must be absolute;
+  ## default: current directory).
+  ## If `path` is absolute, return it, ignoring `root`.
+  ##
+  ## See also:
+  ## * `normalizePath proc`_
+  result = Path(absolutePath(path.string, root.string))