1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
|
## This module implements path handling.
##
## **See also:**
## * `files module <files.html>`_ for file access
import std/private/osseps
export osseps
import std/envvars
import std/private/osappdirs
import std/[pathnorm, hashes, sugar, strutils]
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
func hash*(x: Path): Hash =
let x = x.string.dup(normalizePath)
if FileSystemCaseSensitive:
result = x.hash
else:
result = x.toLowerAscii.hash
template `$`*(x: Path): string =
string(x)
func `==`*(x, y: Path): bool {.inline.} =
## Compares two paths.
##
## On a case-sensitive filesystem this is done
## case-sensitively otherwise case-insensitively.
result = cmpPaths(x.string, y.string) == 0
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 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 <appdirs.html#getHomeDir>`_
## * `getConfigDir proc <appdirs.html#getConfigDir>`_
## * `getTempDir proc <appdirs.html#getTempDir>`_
## * `setCurrentDir proc <dirs.html#setCurrentDir>`_
## * `currentSourcePath template <system.html#currentSourcePath.t>`_
## * `getProjectPath proc <macros.html#getProjectPath>`_
result = Path(ospaths2.getCurrentDir())
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))
proc expandTildeImpl(path: string): string {.
tags: [ReadEnvEffect, ReadIOEffect].} =
if len(path) == 0 or path[0] != '~':
result = path
elif len(path) == 1:
result = getHomeDir()
elif (path[1] in {DirSep, AltSep}):
result = joinPath(getHomeDir(), path.substr(2))
else:
# TODO: handle `~bob` and `~bob/` which means home of bob
result = path
proc expandTilde*(path: Path): Path {.inline,
tags: [ReadEnvEffect, ReadIOEffect].} =
## Expands ``~`` or a path starting with ``~/`` to a full path, replacing
## ``~`` with `getHomeDir() <appdirs.html#getHomeDir>`_ (otherwise returns ``path`` unmodified).
##
## Windows: this is still supported despite the Windows platform not having this
## convention; also, both ``~/`` and ``~\`` are handled.
runnableExamples:
import std/appdirs
assert expandTilde(Path("~") / Path("appname.cfg")) == getHomeDir() / Path("appname.cfg")
assert expandTilde(Path("~/foo/bar")) == getHomeDir() / Path("foo/bar")
assert expandTilde(Path("/foo/bar")) == Path("/foo/bar")
result = Path(expandTildeImpl(path.string))
|