Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add relativePath proc and test #8166

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions lib/pure/ospaths.nim
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,275 @@ when defined(windows) or defined(posix) or defined(nintendoswitch):
if i > 0: result.add " "
result.add quoteShell(args[i])

proc isSep(c: char): bool {.noSideEffect.} = c in {DirSep, AltSep}

proc cmpCharInPath(a, b: char): bool {.noSideEffect.} =
when FileSystemCaseSensitive:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if I like FileSystemCaseSensitive as a constant. It is not uncommon to have an ntfs hard drive mounted somewhere in a unix file system (the Windows partition).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how it's done and it works well. There is no way to get this info in a portable way at runtime.

let r = a == b:
else:
let r = toLowerAscii(a) == toLowerAscii(b)
return if r: true else: (a.isSep and b.isSep)

proc sameDrive(a, b: string): bool {.noSideEffect.} =
when doslikeFileSystem:
not (a.len > 1 and a[1] == ':' and isAlphaAscii(a[0]) and b.len > 1 and b[1] == ':' and a[0] != b[0])
else:
true

proc countDir(path: string; start, last: Natural): int {.noSideEffect.} =
if start >= last:
return 0

result = 0
if not path[start].isSep:
inc(result)
for i in (start+1)..<last:
if path[i-1].isSep and not path[i].isSep:
inc(result)

proc skipDirSep(path: string; start, last: Natural = 0): int {.noSideEffect.} =
var p = start
while p < last and path[p].isSep:
inc(p)
return p

proc rSkipDirSep(path: string; start, last: Natural = 0): int {.noSideEffect.} =
var p = start
while p > last and path[p].isSep:
dec(p)
return p

proc countParDir(path: string; start, last: Natural): (int, int) {.noSideEffect.} =
var p = start
var c = 0
while p < last:
if p <= last - ParDir.len and continuesWith(path, ParDir, p):
p += ParDir.len
inc(c)
p = skipDirSep(path, p, last)
else:
break
return (c, p)

proc getRelativePathFromAbsolute(path, baseDir: string): string {.
noSideEffect.} =
## Convert 'path' to a relative path from baseDir.
##
## Both 'path' and 'baseDir' must be absolute paths.
## On DOS like filesystem, when a drive of 'path' is different from 'baseDir',
## this proc just return the 'path' as is because no way to calculate the relative path.
## This proc never read filesystem.
## 'baseDir' is always assumed to be a directory even if that path is actually a file.
##

assert(isAbsolute(path) and isAbsolute(baseDir))

if baseDir.len == 0:
return path

if not sameDrive(path, baseDir):
return path

let alast = path.len
let blast = rSkipDirSep(baseDir, baseDir.len - 1, 0) + 1

var pos = 0
let m = min(alast, blast)
while pos < m:
if not cmpCharInPath(path[pos], baseDir[pos]):
break
inc(pos)

if (pos == blast and (alast == blast or path[blast].isSep)) or (pos == alast and (blast > alast and baseDir[pos].isSep)):
inc(pos)
else:
while pos != 0 and not path[pos-1].isSep:
dec(pos)

let numUp = countDir(baseDir, pos, blast)

if numUp == 0 and pos >= alast:
return $CurDir

result = if numUp > 0: ParDir & (DirSep & ParDir).repeat(numUp-1) else: ""
if pos < path.len:
return result / path.substr(pos)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is way too low level. Just normalize the input paths and strip the common prefix.

Copy link
Member

@timotheecour timotheecour Oct 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw if you're gonna go that route (normalize), one way to do it would be to move os.normalizePath to ospaths.normalizePath (as mentioned in TODO here: #6587 (comment)) (but doing so in a separate PR as it's a useful change by itself)


proc isInRootDir(path: string; last: Natural): bool {.noSideEffect.} =
if last == 0 and path[0].isSep:
return true
when doslikeFileSystem:
if last < 3 and path.len > 1 and
path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':':
return true
return false

proc getRelativePathFromRelative(path, baseDir, curDir: string): string {.
noSideEffect.} =
## Convert 'path' to a path relative to baseDir.
##
## Both 'path' and 'baseDir' must be relative paths from 'curDir'.
## This proc never read filesystem.
## 'baseDir' is always assumed to be a directory even if that path is actually a file.

proc skipCurDir(path: string): int {.noSideEffect.} =
var p = 0
let l = path.len
while p < l:
if p <= l - ParDir.len and continuesWith(path, ParDir, p):
break
if path[p] != CurDir:
break
inc(p)
p = skipDirSep(path, p, l)
return p

assert(not (isAbsolute(path) or isAbsolute(baseDir)))

if baseDir.len == 0:
return path

let alast = path.len
let blast = rSkipDirSep(baseDir, baseDir.len - 1, 0) + 1

let
astart = skipCurDir(path)
bstart = skipCurDir(baseDir)

var
apos = astart
bpos = bstart

while apos < alast and bpos < blast:
if not cmpCharInPath(path[apos], baseDir[bpos]):
break;
inc(apos)
inc(bpos)

if (bpos == blast and (apos == alast or path[apos].isSep)) or
(apos == alast and (bpos == blast or baseDir[bpos].isSep)):
inc(apos)
else:
while apos != astart and not path[apos-1].isSep:
dec(apos)
dec(bpos)

var numPar: int
(numPar, bpos) = countParDir(baseDir, bpos, blast)

let numUp = countDir(baseDir, bpos, blast)

if numPar == 0 and numUp == 0 and apos >= alast:
return $CurDir

result = if numUp > 0: ParDir & (DirSep & ParDir).repeat(numUp-1) else: ""

if numPar > 0:
if curDir.len == 0:
raise newException(ValueError, "parameter `curDir` is required to calculate relative path from given paths")
var cpos = curDir.len-1
for i in countDown(numPar-1, 0):
cpos = rSkipDirSep(curDir, cpos)
if isInRootDir(curDir, cpos) or curDir[cpos] == CurDir:
raise newException(ValueError, "Cannot calculate relative path from given paths")
while cpos > 0 and not curDir[cpos].isSep:
dec(cpos)
if curDir[cpos].isSep:
inc(cpos)
result = result / curDir.substr(cpos)

if apos < path.len:
return result / path.substr(apos)

proc relativePath*(path, baseDir: string; curDir: string = ""): string {.
noSideEffect, rtl, extern: "nos$1".} =
## Convert `path` to a path relative to baseDir.
##
## `path` and `baseDir` must be absolute paths or relative paths from `curDir`.
## When one of `path` and `baseDir` is relative and other one is absolute, `curDir` must be absolute.
##
## On DOS like filesystem, when a drive of `path` is different from `baseDir`,
## this proc just return the `path` as is because no way to calculate the relative path.
##
## Following pseudo code looks like Nim explains requirements of parameters.
##
## .. code-block:: nim
##
## if isAbsolute(path) and isAbsolute(baseDir):
## # `curDir` is ignored
## else not (isAbsolute(path) or isAbsolute(baseDir)):
## # Both `path` and `baseDir` must be relative to a same path.
## # Suppose ".." is only in front of path, not in middle of path.
## let numParDirsInPath = number of ".." in path
## let numParDirsInBaseDir = number of ".." in baseDir
## if numParDirsInBaseDir > numParDirsInPath:
## # `curDir` can be relative or absolute path.
## # Both `path` and `baseDir` must be relative paths from `curDir`.
## # `curDir` must has (numParDirsInBaseDir - numParDirsInPath) directories or raise ValueError.
## else:
## # `curDir` is ignored
## else:
## # `curDir` must be a absolute path.
## # `curDir` is used to convert `path` or `base` to a absolute path.
##
## For example, relativePath("a", "b") returns "../a", but relativePath("a", "..") raise exception.
## Because result of relativePath("a", "..") requires the parent directory name of "a".
##
## This proc never read filesystem.
## `baseDir` is always assumed to be a directory even if that path is actually a file.
##
## You can find more examples in tests/stdlib/tospaths.nim
runnableExamples:
demotomohiro marked this conversation as resolved.
Show resolved Hide resolved
doAssert relativePath("/home/abc".unixToNativePath, "/home/abc/x".unixToNativePath) == "..".unixToNativePath
doAssert relativePath("abc".unixToNativePath, "xyz".unixToNativePath, "".unixToNativePath) == "../abc".unixToNativePath
doAssert relativePath(".".unixToNativePath, "..".unixToNativePath, "/abc".unixToNativePath) == "abc".unixToNativePath
doAssert relativePath("/home/xyz/d".unixToNativePath, "xyz".unixToNativePath, "/home".unixToNativePath) == "d".unixToNativePath
doAssert relativePath("../d".unixToNativePath, "/usr".unixToNativePath, "/home/xyz".unixToNativePath) == "../home/d".unixToNativePath

proc parentDirPos(path: string; start: Natural): int {.noSideEffect.} =
let q = rSkipDirSep(path, start)
if isInRootDir(path, q):
return -1
for i in countdown(q, 0):
if path[i].isSep: return i
return -1

proc nParentDirPos(path: string; n: Natural): int {.noSideEffect.} =
var p = path.len-1
for i in 0..<n:
p = parentDirPos(path, p)
if p < 0:
return p
return p

proc mergePath(head, tail: string): string {.noSideEffect.} =
var
numPar: int
p: int
(numPar, p) = countParDir(tail, 0, tail.len)
if numPar == 0:
return head / tail
let q = nParentDirPos(head, numPar)
if q < 0:
raise newException(ValueError, "Cannot calculate relative path from given paths")
return head.substr(0, q) / tail.substr(p)

let
isAbsp = isAbsolute(path)
isAbsb = isAbsolute(baseDir)
if isAbsp and isAbsb:
return getRelativePathFromAbsolute(path, baseDir)
elif not (isAbsp or isAbsb):
return getRelativePathFromRelative(path, baseDir, curDir)

if not isAbsolute(curDir):
raise newException(ValueError, "Cannot calculate relative path from given paths")

if isAbsp:
return getRelativePathFromAbsolute(path, mergePath(curDir, baseDir))
else:
return getRelativePathFromAbsolute(mergePath(curDir, path), baseDir)

when isMainModule:
assert quoteShellWindows("aaa") == "aaa"
assert quoteShellWindows("aaa\"") == "aaa\\\""
Expand Down
85 changes: 85 additions & 0 deletions tests/stdlib/tospaths.nim
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,88 @@ block lastPathPartTest:
when doslikeFileSystem:
doAssert lastPathPart(r"foo\bar.txt") == "bar.txt"
doAssert lastPathPart(r"foo\") == "foo"

block:
demotomohiro marked this conversation as resolved.
Show resolved Hide resolved
proc testRelativePath(path, baseDir, curDir = "", res: string): bool {.noSideEffect.} =
let r =
relativePath(
path.unixToNativePath("a"),
baseDir.unixToNativePath("a"),
curDir.unixToNativePath("a"))
result = (r == res.unixToNativePath)
if not result:
debugEcho "relativePath(", path, ", ", baseDir, ", ", curDir, ") returned ", r, " not ", res

proc testRelativePathRaise(path, baseDir, curDir = "") {.noSideEffect.} =
try:
discard testRelativePath(path, baseDir, curDir, "")
doAssert false, "Should raise ValueError"
except ValueError:
discard

#Absolute path and absolute path
doAssert testRelativePath("/", "/", res = ".")
doAssert testRelativePath("/b", "/a", res = "../b")
doAssert testRelativePath("/ab", "/a", res = "../ab")
doAssert testRelativePath("/a", "/ab", res = "../a")
doAssert testRelativePath("/x/a", "/x/a", res = ".")
doAssert testRelativePath("/x/a", "/x/a/y", res = "..")
doAssert testRelativePath("/x/a", "/x/a/y/z", res = "../..")
doAssert testRelativePath("/x/a", "/x/ab/c", res = "../../a")
doAssert testRelativePath("/x/a/bc", "/x/a", res = "bc")
doAssert testRelativePath("/x/a/bc/d", "/x/a", res = "bc/d")
doAssert testRelativePath("/x/ab", "/x/a/", res = "../ab")
doAssert testRelativePath("/x/ab", "/x/a", res = "../ab")
doAssert testRelativePath("/x/y/z/", "/u/v/w", res = "../../../x/y/z/")

when doslikeFileSystem:
doAssert relativePath("a:\\a", "b:\\") == "a:\\a"

#Relative path and Relative path
proc testRelativePathFromRelative(path, baseDir, curDir = "", res: string) =
for i in ["", "./"]:
for j in ["", "/"]:
for k in ["", "./"]:
for l in ["", "/"]:
for m in ["", "/"]:
let r = if path == "." or res == "." or res == "..": res else: res & j
doAssert testRelativePath(i & path & j, k & baseDir & l, m & curDir, r)

testRelativePathFromRelative("a", "a", "", ".")
testRelativePathFromRelative("a", "b", "", "../a")
testRelativePathFromRelative("a", "ab", "", "../a")
testRelativePathFromRelative("ab", "a", "", "../ab")
testRelativePathFromRelative("a", "a/b", "", "..")
testRelativePathFromRelative("a/b", "a", "", "b")
testRelativePathFromRelative("a/a", "ab", "", "../a/a")
testRelativePathFromRelative("a/ab", "a/a", "", "../ab")
testRelativePathFromRelative("a/a", "a/ab", "", "../a")
testRelativePathFromRelative(".", "..", "a", "a")
testRelativePathFromRelative("..", "..", "a", ".")
testRelativePathFromRelative("../a", "../b", "a", "../a")
testRelativePathFromRelative("../c", "b", "a", "../../c")
testRelativePathFromRelative("../d", "./", "a", "../d")
testRelativePathFromRelative("b", "..", "a", "a/b")
testRelativePathFromRelative("b", "../b", "a", "../a/b")
testRelativePathFromRelative("b", "../bb", "a", "../a/b")
testRelativePathFromRelative("x", "../..", "a/b", "a/b/x")

testRelativePathRaise("a", "..", "")

#Relative and absolute
doAssert testRelativePath("a", "/a", "/", ".")
doAssert testRelativePath("/a", "a", "/", ".")
doAssert testRelativePath("/a", "a", "/x", "../../a")
doAssert testRelativePath("a", "/a", "/x", "../x/a")
doAssert testRelativePath("..", "/a", "/x", "..")
doAssert testRelativePath("../a", "/a", "/x", ".")
doAssert testRelativePath("/a", "..", "/x", "a")
doAssert testRelativePath("/a", "../b", "/x", "../a")
doAssert testRelativePath("/a", "../a", "/x", ".")
doAssert testRelativePath("/a", "../../", "/x/y", "a")

testRelativePathRaise("a", "/a", "")
testRelativePathRaise("/", "..", "/")
testRelativePathRaise("..", "/", "/")
testRelativePathRaise("/", "../../", "/x")
testRelativePathRaise("../../", "/", "/x")