mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-29 09:24:36 +00:00
* docgen: implement cross-document links Fully implements https://github.com/nim-lang/RFCs/issues/125 Follow-up of: https://github.com/nim-lang/Nim/pull/18642 (for internal links) and https://github.com/nim-lang/Nim/issues/20127. Overview -------- Explicit import-like directive is required, called `.. importdoc::`. (the syntax is % RST, Markdown will use it for a while). Then one can reference any symbols/headings/anchors, as if they were in the local file (but they will be prefixed with a module name or markup document in link text). It's possible to reference anything from anywhere (any direction in `.nim`/`.md`/`.rst` files). See `doc/docgen.md` for full description. Working is based on `.idx` files, hence one needs to generate all `.idx` beforehand. A dedicated option `--index:only` is introduced (and a separate stage for `--index:only` is added to `kochdocs.nim`). Performance note ---------------- Full run for `./koch docs` now takes 185% of the time before this PR. (After: 315 s, before: 170 s on my PC). All the time seems to be spent on `--index:only` run, which takes almost as much (85%) of normal doc run -- it seems that most time is spent on file parsing, turning off HTML generation phase has not helped much. (One could avoid it by specifying list of files that can be referenced and pre-processing only them. But it can become error-prone and I assume that these linke will be **everywhere** in the repository anyway, especially considering https://github.com/nim-lang/RFCs/issues/478. So every `.nim`/`.md` file is processed for `.idx` first). But that's all without significant part of repository converted to cross-module auto links. To estimate impact I checked the time for `doc`ing a few files (after all indexes have been generated), and everywhere difference was **negligible**. E.g. for `lib/std/private/osfiles.nim` that `importdoc`s large `os.idx` and hence should have been a case with relatively large performance impact, but: * After: 0.59 s. * Before: 0.59 s. So Nim compiler works so slow that doc part basically does not matter :-) Testing ------- 1) added `extlinks` test to `nimdoc/` 2) checked that `theindex.html` is still correct 2) fixed broken auto-links for modules that were derived from `os.nim` by adding appropriate ``importdoc`` Implementation note ------------------- Parsing and formating of `.idx` entries is moved into a dedicated `rstidx.nim` module from `rstgen.nim`. `.idx` file format changed: * fields are not escaped in most cases because we need original strings for referencing, not HTML ones (the exception is linkTitle for titles and headings). Escaping happens later -- on the stage of `rstgen` buildIndex, etc. * all lines have fixed number of columns 6 * added discriminator tag as a first column, it always allows distinguish Nim/markup entries, titles/headings, etc. `rstgen` does not rely any more (in most cases) on ad-hoc logic to determine what type each entry is. * there is now always a title entry added at the first line. * add a line number as 6th column * linkTitle (4th) column has a different format: before it was like `module: funcName()`, now it's `proc funcName()`. (This format is also propagated to `theindex.html` and search results, I kept it that way since I like it more though it's discussible.) This column is what used for Nim symbols resolution. * also changed details on column format for headings and titles: "keyword" is original, "linkTitle" is HTML one * fix paths on Windows + more clear code * Update compiler/docgen.nim Co-authored-by: Andreas Rumpf <rumpf_a@web.de> * Handle .md and .nim paths uniformly in findRefFile * handle titles better + more comments * don't allow markup overwrite index title for .nim files Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
1031 lines
32 KiB
Nim
1031 lines
32 KiB
Nim
include system/inclrtl
|
|
import std/private/since
|
|
|
|
import strutils, pathnorm
|
|
import std/oserrors
|
|
|
|
import oscommon
|
|
export ReadDirEffect, WriteDirEffect
|
|
|
|
when defined(nimPreviewSlimSystem):
|
|
import std/[syncio, assertions, widestrs]
|
|
|
|
## .. importdoc:: osappdirs.nim, osdirs.nim, osseps.nim, os.nim
|
|
|
|
const weirdTarget = defined(nimscript) or defined(js)
|
|
|
|
when weirdTarget:
|
|
discard
|
|
elif defined(windows):
|
|
import winlean
|
|
elif defined(posix):
|
|
import posix, system/ansi_c
|
|
else:
|
|
{.error: "OS module not ported to your operating system!".}
|
|
|
|
when weirdTarget:
|
|
{.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".}
|
|
else:
|
|
{.pragma: noWeirdTarget.}
|
|
|
|
when defined(nimscript):
|
|
# for procs already defined in scriptconfig.nim
|
|
template noNimJs(body): untyped = discard
|
|
elif defined(js):
|
|
{.pragma: noNimJs, error: "this proc is not available on the js target".}
|
|
else:
|
|
{.pragma: noNimJs.}
|
|
|
|
|
|
proc normalizePathAux(path: var string){.inline, raises: [], noSideEffect.}
|
|
|
|
|
|
import std/private/osseps
|
|
export osseps
|
|
|
|
proc absolutePathInternal(path: string): string {.gcsafe.}
|
|
|
|
proc normalizePathEnd*(path: var string, trailingSep = false) =
|
|
## Ensures ``path`` has exactly 0 or 1 trailing `DirSep`, depending on
|
|
## ``trailingSep``, and taking care of edge cases: it preservers whether
|
|
## a path is absolute or relative, and makes sure trailing sep is `DirSep`,
|
|
## not `AltSep`. Trailing `/.` are compressed, see examples.
|
|
if path.len == 0: return
|
|
var i = path.len
|
|
while i >= 1:
|
|
if path[i-1] in {DirSep, AltSep}: dec(i)
|
|
elif path[i-1] == '.' and i >= 2 and path[i-2] in {DirSep, AltSep}: dec(i)
|
|
else: break
|
|
if trailingSep:
|
|
# foo// => foo
|
|
path.setLen(i)
|
|
# foo => foo/
|
|
path.add DirSep
|
|
elif i > 0:
|
|
# foo// => foo
|
|
path.setLen(i)
|
|
else:
|
|
# // => / (empty case was already taken care of)
|
|
path = $DirSep
|
|
|
|
proc normalizePathEnd*(path: string, trailingSep = false): string =
|
|
## outplace overload
|
|
runnableExamples:
|
|
when defined(posix):
|
|
assert normalizePathEnd("/lib//.//", trailingSep = true) == "/lib/"
|
|
assert normalizePathEnd("lib/./.", trailingSep = false) == "lib"
|
|
assert normalizePathEnd(".//./.", trailingSep = false) == "."
|
|
assert normalizePathEnd("", trailingSep = true) == "" # not / !
|
|
assert normalizePathEnd("/", trailingSep = false) == "/" # not "" !
|
|
result = path
|
|
result.normalizePathEnd(trailingSep)
|
|
|
|
template endsWith(a: string, b: set[char]): bool =
|
|
a.len > 0 and a[^1] in b
|
|
|
|
proc joinPathImpl(result: var string, state: var int, tail: string) =
|
|
let trailingSep = tail.endsWith({DirSep, AltSep}) or tail.len == 0 and result.endsWith({DirSep, AltSep})
|
|
normalizePathEnd(result, trailingSep=false)
|
|
addNormalizePath(tail, result, state, DirSep)
|
|
normalizePathEnd(result, trailingSep=trailingSep)
|
|
|
|
proc joinPath*(head, tail: string): string {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## 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:
|
|
## * `joinPath(parts: varargs[string]) proc`_
|
|
## * `/ proc`_
|
|
## * `splitPath proc`_
|
|
## * `uri.combine proc <uri.html#combine,Uri,Uri>`_
|
|
## * `uri./ proc <uri.html#/,Uri,string>`_
|
|
runnableExamples:
|
|
when defined(posix):
|
|
assert joinPath("usr", "lib") == "usr/lib"
|
|
assert joinPath("usr", "lib/") == "usr/lib/"
|
|
assert joinPath("usr", "") == "usr"
|
|
assert joinPath("usr/", "") == "usr/"
|
|
assert joinPath("", "") == ""
|
|
assert joinPath("", "lib") == "lib"
|
|
assert joinPath("", "/lib") == "/lib"
|
|
assert joinPath("usr/", "/lib") == "usr/lib"
|
|
assert joinPath("usr/lib", "../bin") == "usr/bin"
|
|
|
|
result = newStringOfCap(head.len + tail.len)
|
|
var state = 0
|
|
joinPathImpl(result, state, head)
|
|
joinPathImpl(result, state, tail)
|
|
when false:
|
|
if len(head) == 0:
|
|
result = tail
|
|
elif head[len(head)-1] in {DirSep, AltSep}:
|
|
if tail.len > 0 and tail[0] in {DirSep, AltSep}:
|
|
result = head & substr(tail, 1)
|
|
else:
|
|
result = head & tail
|
|
else:
|
|
if tail.len > 0 and tail[0] in {DirSep, AltSep}:
|
|
result = head & tail
|
|
else:
|
|
result = head & DirSep & tail
|
|
|
|
proc joinPath*(parts: varargs[string]): string {.noSideEffect,
|
|
rtl, extern: "nos$1OpenArray".} =
|
|
## The same as `joinPath(head, tail) proc`_,
|
|
## but works with any number of directory parts.
|
|
##
|
|
## You need to pass at least one element or the proc
|
|
## will assert in debug builds and crash on release builds.
|
|
##
|
|
## See also:
|
|
## * `joinPath(head, tail) proc`_
|
|
## * `/ proc`_
|
|
## * `/../ proc`_
|
|
## * `splitPath proc`_
|
|
runnableExamples:
|
|
when defined(posix):
|
|
assert joinPath("a") == "a"
|
|
assert joinPath("a", "b", "c") == "a/b/c"
|
|
assert joinPath("usr/lib", "../../var", "log") == "var/log"
|
|
|
|
var estimatedLen = 0
|
|
for p in parts: estimatedLen += p.len
|
|
result = newStringOfCap(estimatedLen)
|
|
var state = 0
|
|
for i in 0..high(parts):
|
|
joinPathImpl(result, state, parts[i])
|
|
|
|
proc `/`*(head, tail: string): string {.noSideEffect, inline.} =
|
|
## The same as `joinPath(head, tail) proc`_.
|
|
##
|
|
## See also:
|
|
## * `/../ proc`_
|
|
## * `joinPath(head, tail) proc`_
|
|
## * `joinPath(parts: varargs[string]) proc`_
|
|
## * `splitPath proc`_
|
|
## * `uri.combine proc <uri.html#combine,Uri,Uri>`_
|
|
## * `uri./ proc <uri.html#/,Uri,string>`_
|
|
runnableExamples:
|
|
when defined(posix):
|
|
assert "usr" / "" == "usr"
|
|
assert "" / "lib" == "lib"
|
|
assert "" / "/lib" == "/lib"
|
|
assert "usr/" / "/lib/" == "usr/lib/"
|
|
assert "usr" / "lib" / "../bin" == "usr/bin"
|
|
|
|
result = joinPath(head, tail)
|
|
|
|
when doslikeFileSystem:
|
|
import std/private/ntpath
|
|
|
|
proc splitPath*(path: string): tuple[head, tail: string] {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## Splits a directory into `(head, tail)` tuple, so that
|
|
## ``head / tail == path`` (except for edge cases like "/usr").
|
|
##
|
|
## See also:
|
|
## * `joinPath(head, tail) proc`_
|
|
## * `joinPath(parts: varargs[string]) proc`_
|
|
## * `/ proc`_
|
|
## * `/../ proc`_
|
|
## * `relativePath proc`_
|
|
runnableExamples:
|
|
assert splitPath("usr/local/bin") == ("usr/local", "bin")
|
|
assert splitPath("usr/local/bin/") == ("usr/local/bin", "")
|
|
assert splitPath("/bin/") == ("/bin", "")
|
|
when (NimMajor, NimMinor) <= (1, 0):
|
|
assert splitPath("/bin") == ("", "bin")
|
|
else:
|
|
assert splitPath("/bin") == ("/", "bin")
|
|
assert splitPath("bin") == ("", "bin")
|
|
assert splitPath("") == ("", "")
|
|
|
|
when doslikeFileSystem:
|
|
let (drive, splitpath) = splitDrive(path)
|
|
let stop = drive.len
|
|
else:
|
|
const stop = 0
|
|
|
|
var sepPos = -1
|
|
for i in countdown(len(path)-1, stop):
|
|
if path[i] in {DirSep, AltSep}:
|
|
sepPos = i
|
|
break
|
|
if sepPos >= 0:
|
|
result.head = substr(path, 0,
|
|
when (NimMajor, NimMinor) <= (1, 0):
|
|
sepPos-1
|
|
else:
|
|
if likely(sepPos >= 1): sepPos-1 else: 0
|
|
)
|
|
result.tail = substr(path, sepPos+1)
|
|
else:
|
|
when doslikeFileSystem:
|
|
result.head = drive
|
|
result.tail = splitpath
|
|
else:
|
|
result.head = ""
|
|
result.tail = path
|
|
|
|
proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1", raises: [].} =
|
|
## Checks whether a given `path` is absolute.
|
|
##
|
|
## On Windows, network paths are considered absolute too.
|
|
runnableExamples:
|
|
assert not "".isAbsolute
|
|
assert not ".".isAbsolute
|
|
when defined(posix):
|
|
assert "/".isAbsolute
|
|
assert not "a/".isAbsolute
|
|
assert "/a/".isAbsolute
|
|
|
|
if len(path) == 0: return false
|
|
|
|
when doslikeFileSystem:
|
|
var len = len(path)
|
|
result = (path[0] in {'/', '\\'}) or
|
|
(len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
|
|
elif defined(macos):
|
|
# according to https://perldoc.perl.org/File/Spec/Mac.html `:a` is a relative path
|
|
result = path[0] != ':'
|
|
elif defined(RISCOS):
|
|
result = path[0] == '$'
|
|
elif defined(posix) or defined(js):
|
|
# `or defined(js)` wouldn't be needed pending https://github.com/nim-lang/Nim/issues/13469
|
|
# This works around the problem for posix, but Windows is still broken with nim js -d:nodejs
|
|
result = path[0] == '/'
|
|
else:
|
|
doAssert false # if ever hits here, adapt as needed
|
|
|
|
when FileSystemCaseSensitive:
|
|
template `!=?`(a, b: char): bool = a != b
|
|
else:
|
|
template `!=?`(a, b: char): bool = toLowerAscii(a) != toLowerAscii(b)
|
|
|
|
when doslikeFileSystem:
|
|
proc isAbsFromCurrentDrive(path: string): bool {.noSideEffect, raises: [].} =
|
|
## An absolute path from the root of the current drive (e.g. "\foo")
|
|
path.len > 0 and
|
|
(path[0] == AltSep or
|
|
(path[0] == DirSep and
|
|
(path.len == 1 or path[1] notin {DirSep, AltSep, ':'})))
|
|
|
|
proc sameRoot(path1, path2: string): bool {.noSideEffect, raises: [].} =
|
|
## Return true if path1 and path2 have a same root.
|
|
##
|
|
## Detail of Windows path formats:
|
|
## https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
|
|
|
|
assert(isAbsolute(path1))
|
|
assert(isAbsolute(path2))
|
|
|
|
if isAbsFromCurrentDrive(path1) and isAbsFromCurrentDrive(path2):
|
|
result = true
|
|
elif cmpIgnoreCase(splitDrive(path1).drive, splitDrive(path2).drive) == 0:
|
|
result = true
|
|
else:
|
|
result = false
|
|
|
|
proc relativePath*(path, base: string, sep = DirSep): string {.
|
|
rtl, extern: "nos$1".} =
|
|
## 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`_
|
|
runnableExamples:
|
|
assert relativePath("/Users/me/bar/z.nim", "/Users/other/bad", '/') == "../../me/bar/z.nim"
|
|
assert relativePath("/Users/me/bar/z.nim", "/Users/other", '/') == "../me/bar/z.nim"
|
|
when not doslikeFileSystem: # On Windows, UNC-paths start with `//`
|
|
assert relativePath("/Users///me/bar//z.nim", "//Users/", '/') == "me/bar/z.nim"
|
|
assert relativePath("/Users/me/bar/z.nim", "/Users/me", '/') == "bar/z.nim"
|
|
assert relativePath("", "/users/moo", '/') == ""
|
|
assert relativePath("foo", ".", '/') == "foo"
|
|
assert relativePath("foo", "foo", '/') == "."
|
|
|
|
if path.len == 0: return ""
|
|
var base = if base == ".": "" else: base
|
|
var path = path
|
|
path.normalizePathAux
|
|
base.normalizePathAux
|
|
let a1 = isAbsolute(path)
|
|
let a2 = isAbsolute(base)
|
|
if a1 and not a2:
|
|
base = absolutePathInternal(base)
|
|
elif a2 and not a1:
|
|
path = absolutePathInternal(path)
|
|
|
|
when doslikeFileSystem:
|
|
if isAbsolute(path) and isAbsolute(base):
|
|
if not sameRoot(path, base):
|
|
return path
|
|
|
|
var f = default PathIter
|
|
var b = default PathIter
|
|
var ff = (0, -1)
|
|
var bb = (0, -1) # (int, int)
|
|
result = newStringOfCap(path.len)
|
|
# skip the common prefix:
|
|
while f.hasNext(path) and b.hasNext(base):
|
|
ff = next(f, path)
|
|
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 path[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 -- path 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 'path':
|
|
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 path[i + ff[0]]
|
|
if not f.hasNext(path): break
|
|
ff = f.next(path)
|
|
|
|
when not defined(nimOldRelativePathBehavior):
|
|
if result.len == 0: result.add "."
|
|
|
|
proc isRelativeTo*(path: string, base: string): bool {.since: (1, 1).} =
|
|
## Returns true if `path` is relative to `base`.
|
|
runnableExamples:
|
|
doAssert isRelativeTo("./foo//bar", "foo")
|
|
doAssert isRelativeTo("foo/bar", ".")
|
|
doAssert isRelativeTo("/foo/bar.nim", "/foo/bar.nim")
|
|
doAssert not isRelativeTo("foo/bar.nims", "foo/bar.nim")
|
|
let path = path.normalizePath
|
|
let base = base.normalizePath
|
|
let ret = relativePath(path, base)
|
|
result = path.len > 0 and not ret.startsWith ".."
|
|
|
|
proc parentDirPos(path: string): int =
|
|
var q = 1
|
|
if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
|
|
for i in countdown(len(path)-q, 0):
|
|
if path[i] in {DirSep, AltSep}: return i
|
|
result = -1
|
|
|
|
proc parentDir*(path: string): string {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## 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`_
|
|
runnableExamples:
|
|
assert parentDir("") == ""
|
|
when defined(posix):
|
|
assert parentDir("/usr/local/bin") == "/usr/local"
|
|
assert parentDir("foo/bar//") == "foo"
|
|
assert parentDir("//foo//bar//.") == "/foo"
|
|
assert parentDir("./foo") == "."
|
|
assert parentDir("/./foo//./") == "/"
|
|
assert parentDir("a//./") == "."
|
|
assert parentDir("a/b/c/..") == "a"
|
|
result = pathnorm.normalizePath(path)
|
|
when doslikeFileSystem:
|
|
let (drive, splitpath) = splitDrive(result)
|
|
result = splitpath
|
|
var sepPos = parentDirPos(result)
|
|
if sepPos >= 0:
|
|
result = substr(result, 0, sepPos)
|
|
normalizePathEnd(result)
|
|
elif result == ".." or result == "." or result.len == 0 or result[^1] in {DirSep, AltSep}:
|
|
# `.` => `..` and .. => `../..`(etc) would be a sensible alternative
|
|
# `/` => `/` (as done with splitFile) would be a sensible alternative
|
|
result = ""
|
|
else:
|
|
result = "."
|
|
when doslikeFileSystem:
|
|
if result.len == 0:
|
|
discard
|
|
elif drive.len > 0 and result.len == 1 and result[0] in {DirSep, AltSep}:
|
|
result = drive
|
|
else:
|
|
result = drive & result
|
|
|
|
proc tailDir*(path: string): string {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## Returns the tail part of `path`.
|
|
##
|
|
## See also:
|
|
## * `relativePath proc`_
|
|
## * `splitPath proc`_
|
|
## * `parentDir proc`_
|
|
runnableExamples:
|
|
assert tailDir("/bin") == "bin"
|
|
assert tailDir("bin") == ""
|
|
assert tailDir("bin/") == ""
|
|
assert tailDir("/usr/local/bin") == "usr/local/bin"
|
|
assert tailDir("//usr//local//bin//") == "usr//local//bin//"
|
|
assert tailDir("./usr/local/bin") == "usr/local/bin"
|
|
assert tailDir("usr/local/bin") == "local/bin"
|
|
|
|
var i = 0
|
|
when doslikeFileSystem:
|
|
let (drive, splitpath) = path.splitDrive
|
|
if drive != "":
|
|
return splitpath.strip(chars = {DirSep, AltSep}, trailing = false)
|
|
while i < len(path):
|
|
if path[i] in {DirSep, AltSep}:
|
|
while i < len(path) and path[i] in {DirSep, AltSep}: inc i
|
|
return substr(path, i)
|
|
inc i
|
|
result = ""
|
|
|
|
proc isRootDir*(path: string): bool {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## Checks whether a given `path` is a root directory.
|
|
runnableExamples:
|
|
assert isRootDir("")
|
|
assert isRootDir(".")
|
|
assert isRootDir("/")
|
|
assert isRootDir("a")
|
|
assert not isRootDir("/a")
|
|
assert not isRootDir("a/b/c")
|
|
|
|
when doslikeFileSystem:
|
|
if splitDrive(path).path == "":
|
|
return true
|
|
result = parentDirPos(path) < 0
|
|
|
|
iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
|
|
## 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`_
|
|
##
|
|
runnableExamples:
|
|
let g = "a/b/c"
|
|
|
|
for p in g.parentDirs:
|
|
echo p
|
|
# a/b/c
|
|
# a/b
|
|
# a
|
|
|
|
for p in g.parentDirs(fromRoot=true):
|
|
echo p
|
|
# a/
|
|
# a/b/
|
|
# a/b/c
|
|
|
|
for p in g.parentDirs(inclusive=false):
|
|
echo p
|
|
# a/b
|
|
# a
|
|
|
|
if not fromRoot:
|
|
var current = path
|
|
if inclusive: yield path
|
|
while true:
|
|
if current.isRootDir: break
|
|
current = current.parentDir
|
|
yield current
|
|
else:
|
|
when doslikeFileSystem:
|
|
let start = path.splitDrive.drive.len
|
|
else:
|
|
const start = 0
|
|
for i in countup(start, path.len - 2): # ignore the last /
|
|
# deal with non-normalized paths such as /foo//bar//baz
|
|
if path[i] in {DirSep, AltSep} and
|
|
(i == 0 or path[i-1] notin {DirSep, AltSep}):
|
|
yield path.substr(0, i)
|
|
|
|
if inclusive: yield path
|
|
|
|
proc `/../`*(head, tail: string): string {.noSideEffect.} =
|
|
## The same as ``parentDir(head) / tail``, unless there is no parent
|
|
## directory. Then ``head / tail`` is performed instead.
|
|
##
|
|
## See also:
|
|
## * `/ proc`_
|
|
## * `parentDir proc`_
|
|
runnableExamples:
|
|
when defined(posix):
|
|
assert "a/b/c" /../ "d/e" == "a/b/d/e"
|
|
assert "a" /../ "d/e" == "a/d/e"
|
|
|
|
when doslikeFileSystem:
|
|
let (drive, head) = splitDrive(head)
|
|
let sepPos = parentDirPos(head)
|
|
if sepPos >= 0:
|
|
result = substr(head, 0, sepPos-1) / tail
|
|
else:
|
|
result = head / tail
|
|
when doslikeFileSystem:
|
|
result = drive / result
|
|
|
|
proc normExt(ext: string): string =
|
|
if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
|
|
else: result = ExtSep & ext
|
|
|
|
proc searchExtPos*(path: string): int =
|
|
## Returns index of the `'.'` char in `path` if it signifies the beginning
|
|
## of extension. Returns -1 otherwise.
|
|
##
|
|
## See also:
|
|
## * `splitFile proc`_
|
|
## * `extractFilename proc`_
|
|
## * `lastPathPart proc`_
|
|
## * `changeFileExt proc`_
|
|
## * `addFileExt proc`_
|
|
runnableExamples:
|
|
assert searchExtPos("a/b/c") == -1
|
|
assert searchExtPos("c.nim") == 1
|
|
assert searchExtPos("a/b/c.nim") == 5
|
|
assert searchExtPos("a.b.c.nim") == 5
|
|
|
|
# BUGFIX: do not search until 0! .DS_Store is no file extension!
|
|
result = -1
|
|
for i in countdown(len(path)-1, 1):
|
|
if path[i] == ExtSep:
|
|
result = i
|
|
break
|
|
elif path[i] in {DirSep, AltSep}:
|
|
break # do not skip over path
|
|
|
|
proc splitFile*(path: string): tuple[dir, name, ext: string] {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## 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:
|
|
## * `searchExtPos proc`_
|
|
## * `extractFilename proc`_
|
|
## * `lastPathPart proc`_
|
|
## * `changeFileExt proc`_
|
|
## * `addFileExt proc`_
|
|
runnableExamples:
|
|
var (dir, name, ext) = splitFile("usr/local/nimc.html")
|
|
assert dir == "usr/local"
|
|
assert name == "nimc"
|
|
assert ext == ".html"
|
|
(dir, name, ext) = splitFile("/usr/local/os")
|
|
assert dir == "/usr/local"
|
|
assert name == "os"
|
|
assert ext == ""
|
|
(dir, name, ext) = splitFile("/usr/local/")
|
|
assert dir == "/usr/local"
|
|
assert name == ""
|
|
assert ext == ""
|
|
(dir, name, ext) = splitFile("/tmp.txt")
|
|
assert dir == "/"
|
|
assert name == "tmp"
|
|
assert ext == ".txt"
|
|
|
|
var namePos = 0
|
|
var dotPos = 0
|
|
when doslikeFileSystem:
|
|
let (drive, _) = splitDrive(path)
|
|
let stop = len(drive)
|
|
result.dir = drive
|
|
else:
|
|
const stop = 0
|
|
for i in countdown(len(path) - 1, stop):
|
|
if path[i] in {DirSep, AltSep} or i == 0:
|
|
if path[i] in {DirSep, AltSep}:
|
|
result.dir = substr(path, 0, if likely(i >= 1): i - 1 else: 0)
|
|
namePos = i + 1
|
|
if dotPos > i:
|
|
result.name = substr(path, namePos, dotPos - 1)
|
|
result.ext = substr(path, dotPos)
|
|
else:
|
|
result.name = substr(path, namePos)
|
|
break
|
|
elif path[i] == ExtSep and i > 0 and i < len(path) - 1 and
|
|
path[i - 1] notin {DirSep, AltSep} and
|
|
path[i + 1] != ExtSep and dotPos == 0:
|
|
dotPos = i
|
|
|
|
proc extractFilename*(path: string): string {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## Extracts the filename of a given `path`.
|
|
##
|
|
## This is the same as ``name & ext`` from `splitFile(path) proc`_.
|
|
##
|
|
## See also:
|
|
## * `searchExtPos proc`_
|
|
## * `splitFile proc`_
|
|
## * `lastPathPart proc`_
|
|
## * `changeFileExt proc`_
|
|
## * `addFileExt proc`_
|
|
runnableExamples:
|
|
assert extractFilename("foo/bar/") == ""
|
|
assert extractFilename("foo/bar") == "bar"
|
|
assert extractFilename("foo/bar.baz") == "bar.baz"
|
|
|
|
if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
|
|
result = ""
|
|
else:
|
|
result = splitPath(path).tail
|
|
|
|
proc lastPathPart*(path: string): string {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## Like `extractFilename proc`_, but ignores
|
|
## trailing dir separator; aka: `baseName`:idx: in some other languages.
|
|
##
|
|
## See also:
|
|
## * `searchExtPos proc`_
|
|
## * `splitFile proc`_
|
|
## * `extractFilename proc`_
|
|
## * `changeFileExt proc`_
|
|
## * `addFileExt proc`_
|
|
runnableExamples:
|
|
assert lastPathPart("foo/bar/") == "bar"
|
|
assert lastPathPart("foo/bar") == "bar"
|
|
|
|
let path = path.normalizePathEnd(trailingSep = false)
|
|
result = extractFilename(path)
|
|
|
|
proc changeFileExt*(filename, ext: string): string {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## 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:
|
|
## * `searchExtPos proc`_
|
|
## * `splitFile proc`_
|
|
## * `extractFilename proc`_
|
|
## * `lastPathPart proc`_
|
|
## * `addFileExt proc`_
|
|
runnableExamples:
|
|
assert changeFileExt("foo.bar", "baz") == "foo.baz"
|
|
assert changeFileExt("foo.bar", "") == "foo"
|
|
assert changeFileExt("foo", "baz") == "foo.baz"
|
|
|
|
var extPos = searchExtPos(filename)
|
|
if extPos < 0: result = filename & normExt(ext)
|
|
else: result = substr(filename, 0, extPos-1) & normExt(ext)
|
|
|
|
proc addFileExt*(filename, ext: string): string {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## 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:
|
|
## * `searchExtPos proc`_
|
|
## * `splitFile proc`_
|
|
## * `extractFilename proc`_
|
|
## * `lastPathPart proc`_
|
|
## * `changeFileExt proc`_
|
|
runnableExamples:
|
|
assert addFileExt("foo.bar", "baz") == "foo.bar"
|
|
assert addFileExt("foo.bar", "") == "foo.bar"
|
|
assert addFileExt("foo", "baz") == "foo.baz"
|
|
|
|
var extPos = searchExtPos(filename)
|
|
if extPos < 0: result = filename & normExt(ext)
|
|
else: result = filename
|
|
|
|
proc cmpPaths*(pathA, pathB: string): int {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## 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
|
|
runnableExamples:
|
|
when defined(macosx):
|
|
assert cmpPaths("foo", "Foo") == 0
|
|
elif defined(posix):
|
|
assert cmpPaths("foo", "Foo") > 0
|
|
|
|
let a = normalizePath(pathA)
|
|
let b = normalizePath(pathB)
|
|
if FileSystemCaseSensitive:
|
|
result = cmp(a, b)
|
|
else:
|
|
when defined(nimscript):
|
|
result = cmpic(a, b)
|
|
elif defined(nimdoc): discard
|
|
else:
|
|
result = cmpIgnoreCase(a, b)
|
|
|
|
proc unixToNativePath*(path: string, drive=""): string {.
|
|
noSideEffect, rtl, extern: "nos$1".} =
|
|
## 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".
|
|
when defined(unix):
|
|
result = path
|
|
else:
|
|
if path.len == 0: return ""
|
|
|
|
var start: int
|
|
if path[0] == '/':
|
|
# an absolute path
|
|
when doslikeFileSystem:
|
|
if drive != "":
|
|
result = drive & ":" & DirSep
|
|
else:
|
|
result = $DirSep
|
|
elif defined(macos):
|
|
result = "" # must not start with ':'
|
|
else:
|
|
result = $DirSep
|
|
start = 1
|
|
elif path[0] == '.' and (path.len == 1 or path[1] == '/'):
|
|
# current directory
|
|
result = $CurDir
|
|
start = when doslikeFileSystem: 1 else: 2
|
|
else:
|
|
result = ""
|
|
start = 0
|
|
|
|
var i = start
|
|
while i < len(path): # ../../../ --> ::::
|
|
if i+2 < path.len and path[i] == '.' and path[i+1] == '.' and path[i+2] == '/':
|
|
# parent directory
|
|
when defined(macos):
|
|
if result[high(result)] == ':':
|
|
add result, ':'
|
|
else:
|
|
add result, ParDir
|
|
else:
|
|
add result, ParDir & DirSep
|
|
inc(i, 3)
|
|
elif path[i] == '/':
|
|
add result, DirSep
|
|
inc(i)
|
|
else:
|
|
add result, path[i]
|
|
inc(i)
|
|
|
|
|
|
when not defined(nimscript):
|
|
proc getCurrentDir*(): string {.rtl, extern: "nos$1", 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>`_
|
|
when defined(nodejs):
|
|
var ret: cstring
|
|
{.emit: "`ret` = process.cwd();".}
|
|
return $ret
|
|
elif defined(js):
|
|
doAssert false, "use -d:nodejs to have `getCurrentDir` defined"
|
|
elif defined(windows):
|
|
var bufsize = MAX_PATH.int32
|
|
when useWinUnicode:
|
|
var res = newWideCString("", bufsize)
|
|
while true:
|
|
var L = getCurrentDirectoryW(bufsize, res)
|
|
if L == 0'i32:
|
|
raiseOSError(osLastError())
|
|
elif L > bufsize:
|
|
res = newWideCString("", L)
|
|
bufsize = L
|
|
else:
|
|
result = res$L
|
|
break
|
|
else:
|
|
result = newString(bufsize)
|
|
while true:
|
|
var L = getCurrentDirectoryA(bufsize, result)
|
|
if L == 0'i32:
|
|
raiseOSError(osLastError())
|
|
elif L > bufsize:
|
|
result = newString(L)
|
|
bufsize = L
|
|
else:
|
|
setLen(result, L)
|
|
break
|
|
else:
|
|
var bufsize = 1024 # should be enough
|
|
result = newString(bufsize)
|
|
while true:
|
|
if getcwd(result.cstring, bufsize) != nil:
|
|
setLen(result, c_strlen(result.cstring))
|
|
break
|
|
else:
|
|
let err = osLastError()
|
|
if err.int32 == ERANGE:
|
|
bufsize = bufsize shl 1
|
|
doAssert(bufsize >= 0)
|
|
result = newString(bufsize)
|
|
else:
|
|
raiseOSError(osLastError())
|
|
|
|
proc absolutePath*(path: string, root = getCurrentDir()): string =
|
|
## 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:
|
|
## * `normalizedPath proc`_
|
|
## * `normalizePath proc`_
|
|
runnableExamples:
|
|
assert absolutePath("a") == getCurrentDir() / "a"
|
|
|
|
if isAbsolute(path): path
|
|
else:
|
|
if not root.isAbsolute:
|
|
raise newException(ValueError, "The specified root is not absolute: " & root)
|
|
joinPath(root, path)
|
|
|
|
proc absolutePathInternal(path: string): string =
|
|
absolutePath(path, getCurrentDir())
|
|
|
|
|
|
proc normalizePath*(path: var string) {.rtl, extern: "nos$1", tags: [].} =
|
|
## Normalize a path.
|
|
##
|
|
## Consecutive directory separators are collapsed, including an initial double slash.
|
|
##
|
|
## On relative paths, double dot (`..`) sequences are collapsed if possible.
|
|
## On absolute paths they are always collapsed.
|
|
##
|
|
## .. warning:: URL-encoded and Unicode attempts at directory traversal are not detected.
|
|
## Triple dot is not handled.
|
|
##
|
|
## See also:
|
|
## * `absolutePath proc`_
|
|
## * `normalizedPath proc`_ for outplace version
|
|
## * `normalizeExe proc`_
|
|
runnableExamples:
|
|
when defined(posix):
|
|
var a = "a///b//..//c///d"
|
|
a.normalizePath()
|
|
assert a == "a/c/d"
|
|
|
|
path = pathnorm.normalizePath(path)
|
|
when false:
|
|
let isAbs = isAbsolute(path)
|
|
var stack: seq[string] = @[]
|
|
for p in split(path, {DirSep}):
|
|
case p
|
|
of "", ".":
|
|
continue
|
|
of "..":
|
|
if stack.len == 0:
|
|
if isAbs:
|
|
discard # collapse all double dots on absoluta paths
|
|
else:
|
|
stack.add(p)
|
|
elif stack[^1] == "..":
|
|
stack.add(p)
|
|
else:
|
|
discard stack.pop()
|
|
else:
|
|
stack.add(p)
|
|
|
|
if isAbs:
|
|
path = DirSep & join(stack, $DirSep)
|
|
elif stack.len > 0:
|
|
path = join(stack, $DirSep)
|
|
else:
|
|
path = "."
|
|
|
|
proc normalizePathAux(path: var string) = normalizePath(path)
|
|
|
|
proc normalizedPath*(path: string): string {.rtl, extern: "nos$1", tags: [].} =
|
|
## Returns a normalized path for the current OS.
|
|
##
|
|
## See also:
|
|
## * `absolutePath proc`_
|
|
## * `normalizePath proc`_ for the in-place version
|
|
runnableExamples:
|
|
when defined(posix):
|
|
assert normalizedPath("a///b//..//c///d") == "a/c/d"
|
|
result = pathnorm.normalizePath(path)
|
|
|
|
proc normalizeExe*(file: var string) {.since: (1, 3, 5).} =
|
|
## on posix, prepends `./` if `file` doesn't contain `/` and is not `"", ".", ".."`.
|
|
runnableExamples:
|
|
import std/sugar
|
|
when defined(posix):
|
|
doAssert "foo".dup(normalizeExe) == "./foo"
|
|
doAssert "foo/../bar".dup(normalizeExe) == "foo/../bar"
|
|
doAssert "".dup(normalizeExe) == ""
|
|
when defined(posix):
|
|
if file.len > 0 and DirSep notin file and file != "." and file != "..":
|
|
file = "./" & file
|
|
|
|
proc sameFile*(path1, path2: string): bool {.rtl, extern: "nos$1",
|
|
tags: [ReadDirEffect], noWeirdTarget.} =
|
|
## Returns true if both pathname arguments refer to the same physical
|
|
## file or directory.
|
|
##
|
|
## Raises `OSError` if any of the files does not
|
|
## exist or information about it can not be obtained.
|
|
##
|
|
## This proc will return true if given two alternative hard-linked or
|
|
## sym-linked paths to the same file or directory.
|
|
##
|
|
## See also:
|
|
## * `sameFileContent proc`_
|
|
when defined(windows):
|
|
var success = true
|
|
var f1 = openHandle(path1)
|
|
var f2 = openHandle(path2)
|
|
|
|
var lastErr: OSErrorCode
|
|
if f1 != INVALID_HANDLE_VALUE and f2 != INVALID_HANDLE_VALUE:
|
|
var fi1, fi2: BY_HANDLE_FILE_INFORMATION
|
|
|
|
if getFileInformationByHandle(f1, addr(fi1)) != 0 and
|
|
getFileInformationByHandle(f2, addr(fi2)) != 0:
|
|
result = fi1.dwVolumeSerialNumber == fi2.dwVolumeSerialNumber and
|
|
fi1.nFileIndexHigh == fi2.nFileIndexHigh and
|
|
fi1.nFileIndexLow == fi2.nFileIndexLow
|
|
else:
|
|
lastErr = osLastError()
|
|
success = false
|
|
else:
|
|
lastErr = osLastError()
|
|
success = false
|
|
|
|
discard closeHandle(f1)
|
|
discard closeHandle(f2)
|
|
|
|
if not success: raiseOSError(lastErr, $(path1, path2))
|
|
else:
|
|
var a, b: Stat
|
|
if stat(path1, a) < 0'i32 or stat(path2, b) < 0'i32:
|
|
raiseOSError(osLastError(), $(path1, path2))
|
|
else:
|
|
result = a.st_dev == b.st_dev and a.st_ino == b.st_ino
|