From a5aea64a51fa6e478fd81627fc9ddbfc669705e2 Mon Sep 17 00:00:00 2001 From: demotomohiro Date: Sun, 1 Jul 2018 04:51:10 +0900 Subject: [PATCH 1/7] Add getRelPathFromAbs proc and test --- lib/pure/ospaths.nim | 79 +++++++++++++++++++++++++++++++++++++++ tests/stdlib/tospaths.nim | 14 +++++++ 2 files changed, 93 insertions(+) diff --git a/lib/pure/ospaths.nim b/lib/pure/ospaths.nim index 7f7f9a425099d..f50307f19892a 100644 --- a/lib/pure/ospaths.nim +++ b/lib/pure/ospaths.nim @@ -711,3 +711,82 @@ when isMainModule: doAssert r"D:\".normalizePathEnd == r"D:" doAssert r"E:/".normalizePathEnd(trailingSep = true) == r"E:\" doAssert "/".normalizePathEnd == r"\" + +proc isSep(c: char): bool {.noSideEffect.} = c in {DirSep, AltSep} + +proc cmpCharInPath(a, b: char): bool {.noSideEffect.} = + when FileSystemCaseSensitive: + 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 and path[p].isSep: + dec(p) + return p + +proc getRelPathFromAbs*(path, baseDir: string): string {. + noSideEffect, rtl, extern: "nos$1".} = + ## 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. + ## + runnableExamples: + doAssert getRelPathFromAbs("/home/abc".unixToNativePath, "/".unixToNativePath) == "home/abc".unixToNativePath + doAssert getRelPathFromAbs("/home/abc".unixToNativePath, "/home/abc/x".unixToNativePath) == "../".unixToNativePath + doAssert getRelPathFromAbs("/home/abc/xyz".unixToNativePath, "/home/abc/x".unixToNativePath) == "../xyz".unixToNativePath + + 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): + 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).repeat(numUp) else: "" + result.add path.substr(pos) diff --git a/tests/stdlib/tospaths.nim b/tests/stdlib/tospaths.nim index 9e2a5605c3519..e365f15897c86 100644 --- a/tests/stdlib/tospaths.nim +++ b/tests/stdlib/tospaths.nim @@ -62,3 +62,17 @@ block lastPathPartTest: when doslikeFileSystem: doAssert lastPathPart(r"foo\bar.txt") == "bar.txt" doAssert lastPathPart(r"foo\") == "foo" + +block: + proc testGetRelPathFromAbs(path, baseDir, res: string): bool {.noSideEffect.} = + getRelPathFromAbs(path.unixToNativePath("a"), baseDir.unixToNativePath("a")) == res.unixToNativePath + doAssert testGetRelPathFromAbs("/", "/", ".") + doAssert testGetRelPathFromAbs("/b", "/a", "../b") + doAssert testGetRelPathFromAbs("/ab", "/a", "../ab") + doAssert testGetRelPathFromAbs("/a", "/ab", "../a") + doAssert testGetRelPathFromAbs("/x/a", "/x/a", ".") + doAssert testGetRelPathFromAbs("/x/a", "/x/ab/c", "../../a") + doAssert testGetRelPathFromAbs("/x/a/bc", "/x/a", "bc") + doAssert testGetRelPathFromAbs("/x/ab", "/x/a/", "../ab") + doAssert testGetRelPathFromAbs("/x/ab", "/x/a", "../ab") + doAssert testGetRelPathFromAbs("/x/y/z/", "/u/v/w", "../../../x/y/z/") From 8895d144e19ed8a14e29406fe9f41afdbb7a200d Mon Sep 17 00:00:00 2001 From: demotomohiro Date: Fri, 27 Jul 2018 08:50:03 +0900 Subject: [PATCH 2/7] Fixed indent --- lib/pure/ospaths.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pure/ospaths.nim b/lib/pure/ospaths.nim index f50307f19892a..e3bf4410ba482 100644 --- a/lib/pure/ospaths.nim +++ b/lib/pure/ospaths.nim @@ -765,7 +765,7 @@ proc getRelPathFromAbs*(path, baseDir: string): string {. return path if not sameDrive(path, baseDir): - return path + return path let alast = path.len let blast = rSkipDirSep(baseDir, baseDir.len - 1, 0) + 1 From e31c6d578b10ac47e06165e7075d7897547c0b14 Mon Sep 17 00:00:00 2001 From: demotomohiro Date: Tue, 16 Oct 2018 12:50:02 +0900 Subject: [PATCH 3/7] Rebase against upstream devel --- lib/pure/ospaths.nim | 254 +++++++++++++++++++++++++++++++------- tests/stdlib/tospaths.nim | 95 ++++++++++++-- 2 files changed, 294 insertions(+), 55 deletions(-) diff --git a/lib/pure/ospaths.nim b/lib/pure/ospaths.nim index e3bf4410ba482..03c62c337d3d9 100644 --- a/lib/pure/ospaths.nim +++ b/lib/pure/ospaths.nim @@ -678,40 +678,6 @@ when defined(windows) or defined(posix) or defined(nintendoswitch): if i > 0: result.add " " result.add quoteShell(args[i]) -when isMainModule: - assert quoteShellWindows("aaa") == "aaa" - assert quoteShellWindows("aaa\"") == "aaa\\\"" - assert quoteShellWindows("") == "\"\"" - - assert quoteShellPosix("aaa") == "aaa" - assert quoteShellPosix("aaa a") == "'aaa a'" - assert quoteShellPosix("") == "''" - assert quoteShellPosix("a'a") == "'a'\"'\"'a'" - - when defined(posix): - assert quoteShell("") == "''" - - block normalizePathEndTest: - # handle edge cases correctly: shouldn't affect whether path is - # absolute/relative - doAssert "".normalizePathEnd(true) == "" - doAssert "".normalizePathEnd(false) == "" - doAssert "/".normalizePathEnd(true) == $DirSep - doAssert "/".normalizePathEnd(false) == $DirSep - - when defined(posix): - doAssert "//".normalizePathEnd(false) == "/" - doAssert "foo.bar//".normalizePathEnd == "foo.bar" - doAssert "bar//".normalizePathEnd(trailingSep = true) == "bar/" - when defined(Windows): - doAssert r"C:\foo\\".normalizePathEnd == r"C:\foo" - doAssert r"C:\foo".normalizePathEnd(trailingSep = true) == r"C:\foo\" - # this one is controversial: we could argue for returning `D:\` instead, - # but this is simplest. - doAssert r"D:\".normalizePathEnd == r"D:" - doAssert r"E:/".normalizePathEnd(trailingSep = true) == r"E:\" - doAssert "/".normalizePathEnd == r"\" - proc isSep(c: char): bool {.noSideEffect.} = c in {DirSep, AltSep} proc cmpCharInPath(a, b: char): bool {.noSideEffect.} = @@ -738,14 +704,32 @@ proc countDir(path: string; start, last: Natural): int {.noSideEffect.} = 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 getRelPathFromAbs*(path, baseDir: string): string {. - noSideEffect, rtl, extern: "nos$1".} = +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. @@ -754,10 +738,6 @@ proc getRelPathFromAbs*(path, baseDir: string): string {. ## This proc never read filesystem. ## 'baseDir' is always assumed to be a directory even if that path is actually a file. ## - runnableExamples: - doAssert getRelPathFromAbs("/home/abc".unixToNativePath, "/".unixToNativePath) == "home/abc".unixToNativePath - doAssert getRelPathFromAbs("/home/abc".unixToNativePath, "/home/abc/x".unixToNativePath) == "../".unixToNativePath - doAssert getRelPathFromAbs("/home/abc/xyz".unixToNativePath, "/home/abc/x".unixToNativePath) == "../xyz".unixToNativePath assert(isAbsolute(path) and isAbsolute(baseDir)) @@ -777,7 +757,7 @@ proc getRelPathFromAbs*(path, baseDir: string): string {. break inc(pos) - if pos == blast and (alast == blast or path[blast].isSep): + 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: @@ -788,5 +768,193 @@ proc getRelPathFromAbs*(path, baseDir: string): string {. if numUp == 0 and pos >= alast: return $CurDir - result = if numUp > 0: (ParDir & DirSep).repeat(numUp) else: "" - result.add path.substr(pos) + result = if numUp > 0: ParDir & (DirSep & ParDir).repeat(numUp-1) else: "" + if pos < path.len: + return result / path.substr(pos) + +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. + ## + ## This proc never read filesystem. + ## `baseDir` is always assumed to be a directory even if that path is actually a file. + runnableExamples: + doAssert relativePath("/home/abc".unixToNativePath, "/".unixToNativePath) == "home/abc".unixToNativePath + doAssert relativePath("/home/abc".unixToNativePath, "/home/abc/x".unixToNativePath) == "..".unixToNativePath + doAssert relativePath("/home/abc/xyz".unixToNativePath, "/home/abc/x".unixToNativePath) == "../xyz".unixToNativePath + doAssert relativePath("home/xyz/d".unixToNativePath, "home/xyz".unixToNativePath, "".unixToNativePath) == "d".unixToNativePath + doAssert relativePath("abc".unixToNativePath, "xyz".unixToNativePath, "".unixToNativePath) == "../abc".unixToNativePath + doAssert relativePath(".".unixToNativePath, "..".unixToNativePath, "/abc".unixToNativePath) == "abc".unixToNativePath + doAssert relativePath("xyz/d".unixToNativePath, "/home/xyz".unixToNativePath, "/home".unixToNativePath) == "d".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.. Date: Tue, 16 Oct 2018 13:22:11 +0900 Subject: [PATCH 4/7] Remove some runnableExamples of relativePath --- lib/pure/ospaths.nim | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/pure/ospaths.nim b/lib/pure/ospaths.nim index 03c62c337d3d9..fabc30cf5a8a0 100644 --- a/lib/pure/ospaths.nim +++ b/lib/pure/ospaths.nim @@ -870,14 +870,12 @@ proc relativePath*(path, baseDir: string; curDir: string = ""): string {. ## ## 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: - doAssert relativePath("/home/abc".unixToNativePath, "/".unixToNativePath) == "home/abc".unixToNativePath doAssert relativePath("/home/abc".unixToNativePath, "/home/abc/x".unixToNativePath) == "..".unixToNativePath - doAssert relativePath("/home/abc/xyz".unixToNativePath, "/home/abc/x".unixToNativePath) == "../xyz".unixToNativePath - doAssert relativePath("home/xyz/d".unixToNativePath, "home/xyz".unixToNativePath, "".unixToNativePath) == "d".unixToNativePath doAssert relativePath("abc".unixToNativePath, "xyz".unixToNativePath, "".unixToNativePath) == "../abc".unixToNativePath doAssert relativePath(".".unixToNativePath, "..".unixToNativePath, "/abc".unixToNativePath) == "abc".unixToNativePath - doAssert relativePath("xyz/d".unixToNativePath, "/home/xyz".unixToNativePath, "/home".unixToNativePath) == "d".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 From d6d619a112ee37602e0c03094ccd2bc235e2a913 Mon Sep 17 00:00:00 2001 From: demotomohiro Date: Thu, 18 Oct 2018 13:10:10 +0900 Subject: [PATCH 5/7] Add comment to relativePath that explains requirements of parameters --- lib/pure/ospaths.nim | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/pure/ospaths.nim b/lib/pure/ospaths.nim index fabc30cf5a8a0..bac37e2117c26 100644 --- a/lib/pure/ospaths.nim +++ b/lib/pure/ospaths.nim @@ -868,6 +868,30 @@ proc relativePath*(path, baseDir: string; curDir: string = ""): string {. ## 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. ## From 14c21c39e34b8310430b96d990313885f774d3fb Mon Sep 17 00:00:00 2001 From: demotomohiro Date: Thu, 18 Oct 2018 13:50:41 +0900 Subject: [PATCH 6/7] Replace debuging comments for relativePath proc in tospaths.nim --- tests/stdlib/tospaths.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/stdlib/tospaths.nim b/tests/stdlib/tospaths.nim index 76a645be6f958..4734d9514c407 100644 --- a/tests/stdlib/tospaths.nim +++ b/tests/stdlib/tospaths.nim @@ -65,14 +65,14 @@ block lastPathPartTest: block: proc testRelativePath(path, baseDir, curDir = "", res: string): bool {.noSideEffect.} = - #debugEcho path, ", ", baseDir, ", ", curDir, ", ", res let r = relativePath( path.unixToNativePath("a"), baseDir.unixToNativePath("a"), curDir.unixToNativePath("a")) - #debugEcho r - return r == res.unixToNativePath + result = (r == res.unixToNativePath) + if not result: + debugEcho "relativePath(", path, ", ", baseDir, ", ", curDir, ") returned ", r, " not ", res proc testRelativePathRaise(path, baseDir, curDir = "") {.noSideEffect.} = try: From 7276d0f7e4321205aec8601db86795af5904d504 Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Thu, 18 Oct 2018 20:37:02 +0900 Subject: [PATCH 7/7] Add block name to the block that tests relativePath --- tests/stdlib/tospaths.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stdlib/tospaths.nim b/tests/stdlib/tospaths.nim index 4734d9514c407..c576bdb93c364 100644 --- a/tests/stdlib/tospaths.nim +++ b/tests/stdlib/tospaths.nim @@ -63,7 +63,7 @@ block lastPathPartTest: doAssert lastPathPart(r"foo\bar.txt") == "bar.txt" doAssert lastPathPart(r"foo\") == "foo" -block: +block relativePathTest: proc testRelativePath(path, baseDir, curDir = "", res: string): bool {.noSideEffect.} = let r = relativePath(