From fd4e06368bad37ef76336c7fbb070cbe76f94ef2 Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Sat, 14 Jul 2018 13:03:38 -0700 Subject: [PATCH] fix issue #8268 (joinPaths); add lots of tests --- lib/pure/ospaths.nim | 186 +++++++++++++++++++++----------------- tests/stdlib/tospaths.nim | 44 ++++++--- 2 files changed, 135 insertions(+), 95 deletions(-) diff --git a/lib/pure/ospaths.nim b/lib/pure/ospaths.nim index 470aebad95aa2..a45da9f40ad30 100644 --- a/lib/pure/ospaths.nim +++ b/lib/pure/ospaths.nim @@ -157,55 +157,127 @@ const ## The character which separates the base filename from the extension; ## for example, the '.' in ``os.nim``. +proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} = + ## Returns whether ``path`` is absolute. + ## + ## On Windows, network paths are considered absolute too. + runnableExamples: + doAssert(not "".isAbsolute) + doAssert(not ".".isAbsolute) + doAssert(not "foo".isAbsolute) + when defined(posix): + doAssert "/".isAbsolute + doAssert(not "a/".isAbsolute) + when defined(Windows): + doAssert "C:\\foo".isAbsolute + + if len(path) == 0: return false -proc joinPath*(head, tail: string): string {. + 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): + result = path[0] == '/' + +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`. + if path.len == 0: return + var i = path.len + while i >= 1 and path[i-1] in {DirSep, AltSep}: dec(i) + 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 = + result = path + result.normalizePathEnd(trailingSep) + +const absOverridesDefault = false + +proc joinPath*(head, tail: string, absOverrides = absOverridesDefault): string {. noSideEffect, rtl, extern: "nos$1".} = - ## Joins two directory names to one. + ## Concatenates paths ``head`` and ``tail``. ## - ## If head is the empty string, tail is returned. If tail is the empty - ## string, head is returned with a trailing path separator. If tail starts - ## with a path separator it will be removed when concatenated to head. Other - ## path separators not located on boundaries won't be modified. + ## If ``tail`` is absolute and ``absOverrides`` is true, or ``head`` is empty, + ## returns ``tail``. If ``tail`` is empty returns ``head``. Else, returns the + ## concatenation with normalized spearator between ``head`` and ``tail``. runnableExamples: when defined(posix): doAssert joinPath("usr", "lib") == "usr/lib" doAssert joinPath("usr", "") == "usr/" doAssert joinPath("", "lib") == "lib" - doAssert joinPath("", "/lib") == "/lib" doAssert joinPath("usr/", "/lib") == "usr/lib" + doAssert joinPath("usr/", "/lib", absOverrides = true) == "/lib" + doAssert joinPath("usr///", "//lib") == "usr/lib" ## `//` gets compressed + doAssert joinPath("//", "lib") == "/lib" ## ditto + when defined(Windows): + doAssert joinPath(r"C:\foo", r"D:\bar") == r"C:\foo\bar" + + if absOverrides and tail.isAbsolute: + return tail 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)`, 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. + var tail2 = tail + if tail.isAbsolute: + when defined(posix): + tail2 = strip(tail, leading = true, trailing = false, {DirSep}) + elif doslikeFileSystem: + # TODO: factor this logic with isAbsolute; is `\bar` allowed? + # TODO: how to handle "C:\foo" / "D:\bar" ? + doAssert tail.len>=2 and tail[1] == ':' + tail2 = tail[2..^1] + result = normalizePathEnd(head, trailingSep = true) & tail2 + +proc joinPath*(parts: varargs[string], absOverrides: bool): string {.noSideEffect, + rtl, extern: "nos$1varargs".} = + if parts.len == 0: + return "" result = parts[0] for i in 1..high(parts): - result = joinPath(result, parts[i]) + result = joinPath(result, parts[i], absOverrides) + +proc joinPath*(parts: varargs[string]): string {.noSideEffect, + rtl, extern: "nos$1varargs2".} = + ## The same as `joinPath(head, tail, absOverrides)`, but works with any number + ## of directory parts. + runnableExamples: + doAssert joinPath() == "" + doAssert joinPath("foo") == "foo" + when defined(posix): + doAssert joinPath("foo", "bar") == "foo/bar" + doAssert joinPath("foo//", "bar/") == "foo/bar/" + doAssert joinPath("foo//", "bar/", absOverrides = true) == "foo/bar/" + doAssert joinPath("foo", "/bar", "/baz", "tail", absOverrides = true) == "/baz/tail" + doAssert joinPath("foo", "/bar", "/baz", "tail", absOverrides = false) == "foo/bar/baz/tail" + joinPath(parts, absOverridesDefault) proc `/` * (head, tail: string): string {.noSideEffect.} = - ## The same as ``joinPath(head, tail)`` - ## - ## Here are some examples for Unix: - ## - ## .. code-block:: nim - ## assert "usr" / "" == "usr/" - ## assert "" / "lib" == "lib" - ## assert "" / "/lib" == "/lib" - ## assert "usr/" / "/lib" == "usr/lib" + ## The same as ``joinPath(head, tail)``. + runnableExamples: + when defined(posix): + doAssert "usr" / "" == "usr/" + doAssert "" / "lib" == "lib" + doAssert "" / "/lib" == "/lib" + doAssert "usr/" / "/lib" == "usr/lib" return joinPath(head, tail) proc splitPath*(path: string): tuple[head, tail: string] {. @@ -415,56 +487,6 @@ proc cmpPaths*(pathA, pathB: string): int {. else: result = cmpIgnoreCase(pathA, pathB) -proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} = - ## Checks whether a given `path` is absolute. - ## - ## On Windows, network paths are considered absolute too. - runnableExamples: - doAssert(not "".isAbsolute) - doAssert(not ".".isAbsolute) - when defined(posix): - doAssert "/".isAbsolute - doAssert(not "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): - result = path[0] == '/' - - -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`. - if path.len == 0: return - var i = path.len - while i >= 1 and path[i-1] in {DirSep, AltSep}: dec(i) - 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 = - result = path - result.normalizePathEnd(trailingSep) - proc unixToNativePath*(path: string, drive=""): string {. noSideEffect, rtl, extern: "nos$1".} = ## Converts an UNIX-like path to a native one. diff --git a/tests/stdlib/tospaths.nim b/tests/stdlib/tospaths.nim index 0ac7729d92420..f75728e5e133a 100644 --- a/tests/stdlib/tospaths.nim +++ b/tests/stdlib/tospaths.nim @@ -6,32 +6,33 @@ discard """ import os -doAssert unixToNativePath("") == "" -doAssert unixToNativePath(".") == $CurDir -doAssert unixToNativePath("..") == $ParDir -doAssert isAbsolute(unixToNativePath("/")) -doAssert isAbsolute(unixToNativePath("/", "a")) -doAssert isAbsolute(unixToNativePath("/a")) -doAssert isAbsolute(unixToNativePath("/a", "a")) -doAssert isAbsolute(unixToNativePath("/a/b")) -doAssert isAbsolute(unixToNativePath("/a/b", "a")) -doAssert unixToNativePath("a/b") == joinPath("a", "b") +block unixToNativePath: + doAssert unixToNativePath("") == "" + doAssert unixToNativePath(".") == $CurDir + doAssert unixToNativePath("..") == $ParDir + doAssert isAbsolute(unixToNativePath("/")) + doAssert isAbsolute(unixToNativePath("/", "a")) + doAssert isAbsolute(unixToNativePath("/a")) + doAssert isAbsolute(unixToNativePath("/a", "a")) + doAssert isAbsolute(unixToNativePath("/a/b")) + doAssert isAbsolute(unixToNativePath("/a/b", "a")) + doAssert unixToNativePath("a/b") == joinPath("a", "b") -when defined(macos): + when defined(macos): doAssert unixToNativePath("./") == ":" doAssert unixToNativePath("./abc") == ":abc" doAssert unixToNativePath("../abc") == "::abc" doAssert unixToNativePath("../../abc") == ":::abc" doAssert unixToNativePath("/abc", "a") == "abc" doAssert unixToNativePath("/abc/def", "a") == "abc:def" -elif doslikeFileSystem: + elif doslikeFileSystem: doAssert unixToNativePath("./") == ".\\" doAssert unixToNativePath("./abc") == ".\\abc" doAssert unixToNativePath("../abc") == "..\\abc" doAssert unixToNativePath("../../abc") == "..\\..\\abc" doAssert unixToNativePath("/abc", "a") == "a:\\abc" doAssert unixToNativePath("/abc/def", "a") == "a:\\abc\\def" -else: + else: #Tests for unix doAssert unixToNativePath("./") == "./" doAssert unixToNativePath("./abc") == "./abc" @@ -39,3 +40,20 @@ else: doAssert unixToNativePath("../../abc") == "../../abc" doAssert unixToNativePath("/abc", "a") == "/abc" doAssert unixToNativePath("/abc/def", "a") == "/abc/def" + +block normalizePathEnd: + doAssert "".normalizePathEnd == "" + doAssert "".normalizePathEnd(trailingSep = true) == "" + when defined(posix): + doAssert "/".normalizePathEnd == "/" + doAssert "foo.bar".normalizePathEnd == "foo.bar" + doAssert "foo.bar".normalizePathEnd(trailingSep = true) == "foo.bar/" + when defined(Windows): + doAssert r"C:\\".normalizePathEnd == r"C:\" + doAssert r"C:\".normalizePathEnd(trailingSep = true) == r"C:\" + doAssert r"C:\foo\\bar\".normalizePathEnd == r"C:\foo\\bar" + +block joinPath: + when defined(posix): + doAssert joinPath("", "/lib") == "/lib" +