Skip to content

Commit

Permalink
fix issue #8268 (joinPaths); add lots of tests
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheecour committed Aug 26, 2018
1 parent f66cd90 commit 760d909
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 95 deletions.
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
- `threadpool`'s `await` and derivatives have been renamed to `blockUntil`
to avoid confusions with `await` from the `async` macro.

- `joinPaths("foo", "/bar")` now returns "/bar" to match the behavior of most
systems, see https://github.com/nim-lang/Nim/issues/8268.

#### Breaking changes in the compiler

Expand Down
186 changes: 104 additions & 82 deletions lib/pure/ospaths.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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] {.
Expand Down Expand Up @@ -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.
Expand Down
44 changes: 31 additions & 13 deletions tests/stdlib/tospaths.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,54 @@ 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"
doAssert unixToNativePath("../abc") == "../abc"
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"

0 comments on commit 760d909

Please sign in to comment.