summary refs log tree commit diff stats
path: root/lib/pure
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2017-09-01 10:35:50 +0200
committerAndreas Rumpf <rumpf_a@web.de>2017-09-01 10:35:50 +0200
commit50666a1f8bc5961ada8e9ecdcd9c3bc9bc8561d0 (patch)
treec7ff1ac85a5d29ba3056f3d178c9bb19c76687f1 /lib/pure
parent8ce42738648e70e562e7f1bbe925257cddf0b392 (diff)
downloadNim-50666a1f8bc5961ada8e9ecdcd9c3bc9bc8561d0.tar.gz
refactor os.nim and ospaths.nim
Diffstat (limited to 'lib/pure')
-rw-r--r--lib/pure/includes/osenv.nim159
-rw-r--r--lib/pure/includes/oserr.nim132
-rw-r--r--lib/pure/os.nim320
-rw-r--r--lib/pure/ospaths.nim1138
4 files changed, 877 insertions, 872 deletions
diff --git a/lib/pure/includes/osenv.nim b/lib/pure/includes/osenv.nim
new file mode 100644
index 000000000..63500c5fa
--- /dev/null
+++ b/lib/pure/includes/osenv.nim
@@ -0,0 +1,159 @@
+## Include file that implements 'getEnv' and friends. Do not import it!
+
+when not declared(ospaths):
+  {.error: "This is an include file for ospaths.nim!".}
+
+proc c_getenv(env: cstring): cstring {.
+  importc: "getenv", header: "<stdlib.h>".}
+proc c_putenv(env: cstring): cint {.
+  importc: "putenv", header: "<stdlib.h>".}
+
+# Environment handling cannot be put into RTL, because the ``envPairs``
+# iterator depends on ``environment``.
+
+var
+  envComputed {.threadvar.}: bool
+  environment {.threadvar.}: seq[string]
+
+when defined(windows):
+  # because we support Windows GUI applications, things get really
+  # messy here...
+  when useWinUnicode:
+    when defined(cpp):
+      proc strEnd(cstr: WideCString, c = 0'i32): WideCString {.
+        importcpp: "(NI16*)wcschr((const wchar_t *)#, #)", header: "<string.h>".}
+    else:
+      proc strEnd(cstr: WideCString, c = 0'i32): WideCString {.
+        importc: "wcschr", header: "<string.h>".}
+  else:
+    proc strEnd(cstr: cstring, c = 0'i32): cstring {.
+      importc: "strchr", header: "<string.h>".}
+
+  proc getEnvVarsC() =
+    if not envComputed:
+      environment = @[]
+      when useWinUnicode:
+        var
+          env = getEnvironmentStringsW()
+          e = env
+        if e == nil: return # an error occurred
+        while true:
+          var eend = strEnd(e)
+          add(environment, $e)
+          e = cast[WideCString](cast[ByteAddress](eend)+2)
+          if eend[1].int == 0: break
+        discard freeEnvironmentStringsW(env)
+      else:
+        var
+          env = getEnvironmentStringsA()
+          e = env
+        if e == nil: return # an error occurred
+        while true:
+          var eend = strEnd(e)
+          add(environment, $e)
+          e = cast[cstring](cast[ByteAddress](eend)+1)
+          if eend[1] == '\0': break
+        discard freeEnvironmentStringsA(env)
+      envComputed = true
+
+else:
+  const
+    useNSGetEnviron = defined(macosx) and not defined(ios)
+
+  when useNSGetEnviron:
+    # From the manual:
+    # Shared libraries and bundles don't have direct access to environ,
+    # which is only available to the loader ld(1) when a complete program
+    # is being linked.
+    # The environment routines can still be used, but if direct access to
+    # environ is needed, the _NSGetEnviron() routine, defined in
+    # <crt_externs.h>, can be used to retrieve the address of environ
+    # at runtime.
+    proc NSGetEnviron(): ptr cstringArray {.
+      importc: "_NSGetEnviron", header: "<crt_externs.h>".}
+  else:
+    var gEnv {.importc: "environ".}: cstringArray
+
+  proc getEnvVarsC() =
+    # retrieves the variables of char** env of C's main proc
+    if not envComputed:
+      environment = @[]
+      when useNSGetEnviron:
+        var gEnv = NSGetEnviron()[]
+      var i = 0
+      while true:
+        if gEnv[i] == nil: break
+        add environment, $gEnv[i]
+        inc(i)
+      envComputed = true
+
+proc findEnvVar(key: string): int =
+  getEnvVarsC()
+  var temp = key & '='
+  for i in 0..high(environment):
+    if startsWith(environment[i], temp): return i
+  return -1
+
+proc getEnv*(key: string): TaintedString {.tags: [ReadEnvEffect].} =
+  ## Returns the value of the `environment variable`:idx: named `key`.
+  ##
+  ## If the variable does not exist, "" is returned. To distinguish
+  ## whether a variable exists or it's value is just "", call
+  ## `existsEnv(key)`.
+  when nimvm:
+    discard "built into the compiler"
+  else:
+    var i = findEnvVar(key)
+    if i >= 0:
+      return TaintedString(substr(environment[i], find(environment[i], '=')+1))
+    else:
+      var env = c_getenv(key)
+      if env == nil: return TaintedString("")
+      result = TaintedString($env)
+
+proc existsEnv*(key: string): bool {.tags: [ReadEnvEffect].} =
+  ## Checks whether the environment variable named `key` exists.
+  ## Returns true if it exists, false otherwise.
+  when nimvm:
+    discard "built into the compiler"
+  else:
+    if c_getenv(key) != nil: return true
+    else: return findEnvVar(key) >= 0
+
+proc putEnv*(key, val: string) {.tags: [WriteEnvEffect].} =
+  ## Sets the value of the `environment variable`:idx: named `key` to `val`.
+  ## If an error occurs, `EInvalidEnvVar` is raised.
+
+  # Note: by storing the string in the environment sequence,
+  # we guarantee that we don't free the memory before the program
+  # ends (this is needed for POSIX compliance). It is also needed so that
+  # the process itself may access its modified environment variables!
+  when nimvm:
+    discard "built into the compiler"
+  else:
+    var indx = findEnvVar(key)
+    if indx >= 0:
+      environment[indx] = key & '=' & val
+    else:
+      add environment, (key & '=' & val)
+      indx = high(environment)
+    when defined(windows):
+      when useWinUnicode:
+        var k = newWideCString(key)
+        var v = newWideCString(val)
+        if setEnvironmentVariableW(k, v) == 0'i32: raiseOSError(osLastError())
+      else:
+        if setEnvironmentVariableA(key, val) == 0'i32: raiseOSError(osLastError())
+    else:
+      if c_putenv(environment[indx]) != 0'i32:
+        raiseOSError(osLastError())
+
+iterator envPairs*(): tuple[key, value: TaintedString] {.tags: [ReadEnvEffect].} =
+  ## Iterate over all `environments variables`:idx:. In the first component
+  ## of the tuple is the name of the current variable stored, in the second
+  ## its value.
+  getEnvVarsC()
+  for i in 0..high(environment):
+    var p = find(environment[i], '=')
+    yield (TaintedString(substr(environment[i], 0, p-1)),
+           TaintedString(substr(environment[i], p+1)))
diff --git a/lib/pure/includes/oserr.nim b/lib/pure/includes/oserr.nim
new file mode 100644
index 000000000..ef3f82628
--- /dev/null
+++ b/lib/pure/includes/oserr.nim
@@ -0,0 +1,132 @@
+## Include file that implements 'osErrorMsg' and friends. Do not import it!
+
+when not declared(ospaths):
+  {.error: "This is an include file for ospaths.nim!".}
+
+when not defined(nimscript):
+  var errno {.importc, header: "<errno.h>".}: cint
+
+  proc c_strerror(errnum: cint): cstring {.
+    importc: "strerror", header: "<string.h>".}
+
+proc osErrorMsg*(): string {.rtl, extern: "nos$1", deprecated.} =
+  ## Retrieves the operating system's error flag, ``errno``.
+  ## On Windows ``GetLastError`` is checked before ``errno``.
+  ## Returns "" if no error occurred.
+  ##
+  ## **Deprecated since version 0.9.4**: use the other ``osErrorMsg`` proc.
+
+  result = ""
+  when defined(Windows) and not defined(nimscript):
+    var err = getLastError()
+    if err != 0'i32:
+      when useWinUnicode:
+        var msgbuf: WideCString
+        if formatMessageW(0x00000100 or 0x00001000 or 0x00000200 or 0x000000FF,
+                          nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
+          result = $msgbuf
+          if msgbuf != nil: localFree(cast[pointer](msgbuf))
+      else:
+        var msgbuf: cstring
+        if formatMessageA(0x00000100 or 0x00001000 or 0x00000200 or 0x000000FF,
+                          nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
+          result = $msgbuf
+          if msgbuf != nil: localFree(msgbuf)
+  when not defined(nimscript):
+    if errno != 0'i32:
+      result = $c_strerror(errno)
+
+{.push warning[deprecated]: off.}
+proc raiseOSError*(msg: string = "") {.noinline, rtl, extern: "nos$1",
+                                       deprecated.} =
+  ## raises an OSError exception with the given message ``msg``.
+  ## If ``msg == ""``, the operating system's error flag
+  ## (``errno``) is converted to a readable error message. On Windows
+  ## ``GetLastError`` is checked before ``errno``.
+  ## If no error flag is set, the message ``unknown OS error`` is used.
+  ##
+  ## **Deprecated since version 0.9.4**: use the other ``raiseOSError`` proc.
+  if len(msg) == 0:
+    var m = osErrorMsg()
+    raise newException(OSError, if m.len > 0: m else: "unknown OS error")
+  else:
+    raise newException(OSError, msg)
+{.pop.}
+
+when not defined(nimfix):
+  {.deprecated: [osError: raiseOSError].}
+
+proc `==`*(err1, err2: OSErrorCode): bool {.borrow.}
+proc `$`*(err: OSErrorCode): string {.borrow.}
+
+proc osErrorMsg*(errorCode: OSErrorCode): string =
+  ## Converts an OS error code into a human readable string.
+  ##
+  ## The error code can be retrieved using the ``osLastError`` proc.
+  ##
+  ## If conversion fails, or ``errorCode`` is ``0`` then ``""`` will be
+  ## returned.
+  ##
+  ## On Windows, the ``-d:useWinAnsi`` compilation flag can be used to
+  ## make this procedure use the non-unicode Win API calls to retrieve the
+  ## message.
+  result = ""
+  when defined(nimscript):
+    discard
+  elif defined(Windows):
+    if errorCode != OSErrorCode(0'i32):
+      when useWinUnicode:
+        var msgbuf: WideCString
+        if formatMessageW(0x00000100 or 0x00001000 or 0x00000200,
+                        nil, errorCode.int32, 0, addr(msgbuf), 0, nil) != 0'i32:
+          result = $msgbuf
+          if msgbuf != nil: localFree(cast[pointer](msgbuf))
+      else:
+        var msgbuf: cstring
+        if formatMessageA(0x00000100 or 0x00001000 or 0x00000200,
+                        nil, errorCode.int32, 0, addr(msgbuf), 0, nil) != 0'i32:
+          result = $msgbuf
+          if msgbuf != nil: localFree(msgbuf)
+  else:
+    if errorCode != OSErrorCode(0'i32):
+      result = $c_strerror(errorCode.int32)
+
+proc raiseOSError*(errorCode: OSErrorCode; additionalInfo = "") {.noinline.} =
+  ## Raises an ``OSError`` exception. The ``errorCode`` will determine the
+  ## message, ``osErrorMsg`` will be used to get this message.
+  ##
+  ## The error code can be retrieved using the ``osLastError`` proc.
+  ##
+  ## If the error code is ``0`` or an error message could not be retrieved,
+  ## the message ``unknown OS error`` will be used.
+  var e: ref OSError; new(e)
+  e.errorCode = errorCode.int32
+  if additionalInfo.len == 0:
+    e.msg = osErrorMsg(errorCode)
+  else:
+    e.msg = osErrorMsg(errorCode) & "\nAdditional info: " & additionalInfo
+  if e.msg == "":
+    e.msg = "unknown OS error"
+  raise e
+
+{.push stackTrace:off.}
+proc osLastError*(): OSErrorCode =
+  ## Retrieves the last operating system error code.
+  ##
+  ## This procedure is useful in the event when an OS call fails. In that case
+  ## this procedure will return the error code describing the reason why the
+  ## OS call failed. The ``OSErrorMsg`` procedure can then be used to convert
+  ## this code into a string.
+  ##
+  ## **Warning**:
+  ## The behaviour of this procedure varies between Windows and POSIX systems.
+  ## On Windows some OS calls can reset the error code to ``0`` causing this
+  ## procedure to return ``0``. It is therefore advised to call this procedure
+  ## immediately after an OS call fails. On POSIX systems this is not a problem.
+  when defined(nimscript):
+    discard
+  elif defined(windows):
+    result = OSErrorCode(getLastError())
+  else:
+    result = OSErrorCode(errno)
+{.pop.}
diff --git a/lib/pure/os.nim b/lib/pure/os.nim
index fff71b818..b85181edf 100644
--- a/lib/pure/os.nim
+++ b/lib/pure/os.nim
@@ -35,136 +35,11 @@ proc c_rename(oldname, newname: cstring): cint {.
   importc: "rename", header: "<stdio.h>".}
 proc c_system(cmd: cstring): cint {.
   importc: "system", header: "<stdlib.h>".}
-proc c_strerror(errnum: cint): cstring {.
-  importc: "strerror", header: "<string.h>".}
 proc c_strlen(a: cstring): cint {.
   importc: "strlen", header: "<string.h>", noSideEffect.}
-proc c_getenv(env: cstring): cstring {.
-  importc: "getenv", header: "<stdlib.h>".}
-proc c_putenv(env: cstring): cint {.
-  importc: "putenv", header: "<stdlib.h>".}
 proc c_free(p: pointer) {.
   importc: "free", header: "<stdlib.h>".}
 
-var errno {.importc, header: "<errno.h>".}: cint
-
-proc osErrorMsg*(): string {.rtl, extern: "nos$1", deprecated.} =
-  ## Retrieves the operating system's error flag, ``errno``.
-  ## On Windows ``GetLastError`` is checked before ``errno``.
-  ## Returns "" if no error occurred.
-  ##
-  ## **Deprecated since version 0.9.4**: use the other ``osErrorMsg`` proc.
-
-  result = ""
-  when defined(Windows):
-    var err = getLastError()
-    if err != 0'i32:
-      when useWinUnicode:
-        var msgbuf: WideCString
-        if formatMessageW(0x00000100 or 0x00001000 or 0x00000200 or 0x000000FF,
-                          nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
-          result = $msgbuf
-          if msgbuf != nil: localFree(cast[pointer](msgbuf))
-      else:
-        var msgbuf: cstring
-        if formatMessageA(0x00000100 or 0x00001000 or 0x00000200 or 0x000000FF,
-                          nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
-          result = $msgbuf
-          if msgbuf != nil: localFree(msgbuf)
-  if errno != 0'i32:
-    result = $os.c_strerror(errno)
-
-{.push warning[deprecated]: off.}
-proc raiseOSError*(msg: string = "") {.noinline, rtl, extern: "nos$1",
-                                       deprecated.} =
-  ## raises an OSError exception with the given message ``msg``.
-  ## If ``msg == ""``, the operating system's error flag
-  ## (``errno``) is converted to a readable error message. On Windows
-  ## ``GetLastError`` is checked before ``errno``.
-  ## If no error flag is set, the message ``unknown OS error`` is used.
-  ##
-  ## **Deprecated since version 0.9.4**: use the other ``raiseOSError`` proc.
-  if len(msg) == 0:
-    var m = osErrorMsg()
-    raise newException(OSError, if m.len > 0: m else: "unknown OS error")
-  else:
-    raise newException(OSError, msg)
-{.pop.}
-
-when not defined(nimfix):
-  {.deprecated: [osError: raiseOSError].}
-
-proc `==`*(err1, err2: OSErrorCode): bool {.borrow.}
-proc `$`*(err: OSErrorCode): string {.borrow.}
-
-proc osErrorMsg*(errorCode: OSErrorCode): string =
-  ## Converts an OS error code into a human readable string.
-  ##
-  ## The error code can be retrieved using the ``osLastError`` proc.
-  ##
-  ## If conversion fails, or ``errorCode`` is ``0`` then ``""`` will be
-  ## returned.
-  ##
-  ## On Windows, the ``-d:useWinAnsi`` compilation flag can be used to
-  ## make this procedure use the non-unicode Win API calls to retrieve the
-  ## message.
-  result = ""
-  when defined(Windows):
-    if errorCode != OSErrorCode(0'i32):
-      when useWinUnicode:
-        var msgbuf: WideCString
-        if formatMessageW(0x00000100 or 0x00001000 or 0x00000200,
-                        nil, errorCode.int32, 0, addr(msgbuf), 0, nil) != 0'i32:
-          result = $msgbuf
-          if msgbuf != nil: localFree(cast[pointer](msgbuf))
-      else:
-        var msgbuf: cstring
-        if formatMessageA(0x00000100 or 0x00001000 or 0x00000200,
-                        nil, errorCode.int32, 0, addr(msgbuf), 0, nil) != 0'i32:
-          result = $msgbuf
-          if msgbuf != nil: localFree(msgbuf)
-  else:
-    if errorCode != OSErrorCode(0'i32):
-      result = $os.c_strerror(errorCode.int32)
-
-proc raiseOSError*(errorCode: OSErrorCode; additionalInfo = "") {.noinline.} =
-  ## Raises an ``OSError`` exception. The ``errorCode`` will determine the
-  ## message, ``osErrorMsg`` will be used to get this message.
-  ##
-  ## The error code can be retrieved using the ``osLastError`` proc.
-  ##
-  ## If the error code is ``0`` or an error message could not be retrieved,
-  ## the message ``unknown OS error`` will be used.
-  var e: ref OSError; new(e)
-  e.errorCode = errorCode.int32
-  if additionalInfo.len == 0:
-    e.msg = osErrorMsg(errorCode)
-  else:
-    e.msg = osErrorMsg(errorCode) & "\nAdditional info: " & additionalInfo
-  if e.msg == "":
-    e.msg = "unknown OS error"
-  raise e
-
-{.push stackTrace:off.}
-proc osLastError*(): OSErrorCode =
-  ## Retrieves the last operating system error code.
-  ##
-  ## This procedure is useful in the event when an OS call fails. In that case
-  ## this procedure will return the error code describing the reason why the
-  ## OS call failed. The ``OSErrorMsg`` procedure can then be used to convert
-  ## this code into a string.
-  ##
-  ## **Warning**:
-  ## The behaviour of this procedure varies between Windows and POSIX systems.
-  ## On Windows some OS calls can reset the error code to ``0`` causing this
-  ## procedure to return ``0``. It is therefore advised to call this procedure
-  ## immediately after an OS call fails. On POSIX systems this is not a problem.
-
-  when defined(windows):
-    result = OSErrorCode(getLastError())
-  else:
-    result = OSErrorCode(errno)
-{.pop.}
 
 when defined(windows):
   when useWinUnicode:
@@ -244,6 +119,60 @@ proc dirExists*(dir: string): bool {.inline.} =
   ## Synonym for existsDir
   existsDir(dir)
 
+when not defined(windows):
+  proc checkSymlink(path: string): bool =
+    var rawInfo: Stat
+    if lstat(path, rawInfo) < 0'i32: result = false
+    else: result = S_ISLNK(rawInfo.st_mode)
+
+const
+  ExeExts* = when defined(windows): ["exe", "cmd", "bat"] else: [""] ## \
+    ## platform specific file extension for executables. On Windows
+    ## ``["exe", "cmd", "bat"]``, on Posix ``[""]``.
+
+proc findExe*(exe: string, followSymlinks: bool = true;
+              extensions: openarray[string]=ExeExts): string {.
+  tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect].} =
+  ## Searches for `exe` in the current working directory and then
+  ## in directories listed in the ``PATH`` environment variable.
+  ## Returns "" if the `exe` cannot be found. `exe`
+  ## is added the `ExeExts <#ExeExts>`_ file extensions if it has none.
+  ## If the system supports symlinks it also resolves them until it
+  ## meets the actual file. This behavior can be disabled if desired.
+  for ext in extensions:
+    result = addFileExt(exe, ext)
+    if existsFile(result): return
+  var path = string(getEnv("PATH"))
+  for candidate in split(path, PathSep):
+    when defined(windows):
+      var x = (if candidate[0] == '"' and candidate[^1] == '"':
+                substr(candidate, 1, candidate.len-2) else: candidate) /
+              exe
+    else:
+      var x = expandTilde(candidate) / exe
+    for ext in extensions:
+      var x = addFileExt(x, ext)
+      if existsFile(x):
+        when not defined(windows):
+          while followSymlinks: # doubles as if here
+            if x.checkSymlink:
+              var r = newString(256)
+              var len = readlink(x, r, 256)
+              if len < 0:
+                raiseOSError(osLastError())
+              if len > 256:
+                r = newString(len+1)
+                len = readlink(x, r, len)
+              setLen(r, len)
+              if isAbsolute(r):
+                x = r
+              else:
+                x = parentDir(x) / r
+            else:
+              break
+        return x
+  result = ""
+
 proc getLastModificationTime*(file: string): Time {.rtl, extern: "nos$1".} =
   ## Returns the `file`'s last modification time.
   when defined(posix):
@@ -706,147 +635,6 @@ proc execShellCmd*(command: string): int {.rtl, extern: "nos$1",
   else:
     result = c_system(command)
 
-# Environment handling cannot be put into RTL, because the ``envPairs``
-# iterator depends on ``environment``.
-
-var
-  envComputed {.threadvar.}: bool
-  environment {.threadvar.}: seq[string]
-
-when defined(windows):
-  # because we support Windows GUI applications, things get really
-  # messy here...
-  when useWinUnicode:
-    when defined(cpp):
-      proc strEnd(cstr: WideCString, c = 0'i32): WideCString {.
-        importcpp: "(NI16*)wcschr((const wchar_t *)#, #)", header: "<string.h>".}
-    else:
-      proc strEnd(cstr: WideCString, c = 0'i32): WideCString {.
-        importc: "wcschr", header: "<string.h>".}
-  else:
-    proc strEnd(cstr: cstring, c = 0'i32): cstring {.
-      importc: "strchr", header: "<string.h>".}
-
-  proc getEnvVarsC() =
-    if not envComputed:
-      environment = @[]
-      when useWinUnicode:
-        var
-          env = getEnvironmentStringsW()
-          e = env
-        if e == nil: return # an error occurred
-        while true:
-          var eend = strEnd(e)
-          add(environment, $e)
-          e = cast[WideCString](cast[ByteAddress](eend)+2)
-          if eend[1].int == 0: break
-        discard freeEnvironmentStringsW(env)
-      else:
-        var
-          env = getEnvironmentStringsA()
-          e = env
-        if e == nil: return # an error occurred
-        while true:
-          var eend = strEnd(e)
-          add(environment, $e)
-          e = cast[cstring](cast[ByteAddress](eend)+1)
-          if eend[1] == '\0': break
-        discard freeEnvironmentStringsA(env)
-      envComputed = true
-
-else:
-  const
-    useNSGetEnviron = defined(macosx) and not defined(ios)
-
-  when useNSGetEnviron:
-    # From the manual:
-    # Shared libraries and bundles don't have direct access to environ,
-    # which is only available to the loader ld(1) when a complete program
-    # is being linked.
-    # The environment routines can still be used, but if direct access to
-    # environ is needed, the _NSGetEnviron() routine, defined in
-    # <crt_externs.h>, can be used to retrieve the address of environ
-    # at runtime.
-    proc NSGetEnviron(): ptr cstringArray {.
-      importc: "_NSGetEnviron", header: "<crt_externs.h>".}
-  else:
-    var gEnv {.importc: "environ".}: cstringArray
-
-  proc getEnvVarsC() =
-    # retrieves the variables of char** env of C's main proc
-    if not envComputed:
-      environment = @[]
-      when useNSGetEnviron:
-        var gEnv = NSGetEnviron()[]
-      var i = 0
-      while true:
-        if gEnv[i] == nil: break
-        add environment, $gEnv[i]
-        inc(i)
-      envComputed = true
-
-proc findEnvVar(key: string): int =
-  getEnvVarsC()
-  var temp = key & '='
-  for i in 0..high(environment):
-    if startsWith(environment[i], temp): return i
-  return -1
-
-proc getEnv*(key: string): TaintedString {.tags: [ReadEnvEffect].} =
-  ## Returns the value of the `environment variable`:idx: named `key`.
-  ##
-  ## If the variable does not exist, "" is returned. To distinguish
-  ## whether a variable exists or it's value is just "", call
-  ## `existsEnv(key)`.
-  var i = findEnvVar(key)
-  if i >= 0:
-    return TaintedString(substr(environment[i], find(environment[i], '=')+1))
-  else:
-    var env = c_getenv(key)
-    if env == nil: return TaintedString("")
-    result = TaintedString($env)
-
-proc existsEnv*(key: string): bool {.tags: [ReadEnvEffect].} =
-  ## Checks whether the environment variable named `key` exists.
-  ## Returns true if it exists, false otherwise.
-  if c_getenv(key) != nil: return true
-  else: return findEnvVar(key) >= 0
-
-proc putEnv*(key, val: string) {.tags: [WriteEnvEffect].} =
-  ## Sets the value of the `environment variable`:idx: named `key` to `val`.
-  ## If an error occurs, `EInvalidEnvVar` is raised.
-
-  # Note: by storing the string in the environment sequence,
-  # we guarantee that we don't free the memory before the program
-  # ends (this is needed for POSIX compliance). It is also needed so that
-  # the process itself may access its modified environment variables!
-  var indx = findEnvVar(key)
-  if indx >= 0:
-    environment[indx] = key & '=' & val
-  else:
-    add environment, (key & '=' & val)
-    indx = high(environment)
-  when defined(windows):
-    when useWinUnicode:
-      var k = newWideCString(key)
-      var v = newWideCString(val)
-      if setEnvironmentVariableW(k, v) == 0'i32: raiseOSError(osLastError())
-    else:
-      if setEnvironmentVariableA(key, val) == 0'i32: raiseOSError(osLastError())
-  else:
-    if c_putenv(environment[indx]) != 0'i32:
-      raiseOSError(osLastError())
-
-iterator envPairs*(): tuple[key, value: TaintedString] {.tags: [ReadEnvEffect].} =
-  ## Iterate over all `environments variables`:idx:. In the first component
-  ## of the tuple is the name of the current variable stored, in the second
-  ## its value.
-  getEnvVarsC()
-  for i in 0..high(environment):
-    var p = find(environment[i], '=')
-    yield (TaintedString(substr(environment[i], 0, p-1)),
-           TaintedString(substr(environment[i], p+1)))
-
 # Templates for filtering directories and files
 when defined(windows):
   template isDir(f: WIN32_FIND_DATA): bool =
diff --git a/lib/pure/ospaths.nim b/lib/pure/ospaths.nim
index 4e76e4e18..ea6a56a06 100644
--- a/lib/pure/ospaths.nim
+++ b/lib/pure/ospaths.nim
@@ -7,627 +7,553 @@
 #    distribution, for details about the copyright.
 #
 
-# Included by the ``os`` module but a module in its own right for NimScript
+# Forwarded by the ``os`` module but a module in its own right for NimScript
 # support.
 
-when not declared(os):
-  {.pragma: rtl.}
-  import strutils
-
-when defined(nimscript) or (defined(nimdoc) and not declared(os)):
-  {.pragma: rtl.}
-  {.push hint[ConvFromXtoItselfNotNeeded]:off.}
-
-when not declared(getEnv) or defined(nimscript):
-  type
-    ReadEnvEffect* = object of ReadIOEffect   ## effect that denotes a read
-                                              ## from an environment variable
-    WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
-                                              ## to an environment variable
-
-    ReadDirEffect* = object of ReadIOEffect   ## effect that denotes a read
-                                              ## operation from the directory
-                                              ## structure
-    WriteDirEffect* = object of WriteIOEffect ## effect that denotes a write
-                                              ## operation to
-                                              ## the directory structure
-
-    OSErrorCode* = distinct int32 ## Specifies an OS Error Code.
-
-  {.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
-      FReadDir: ReadDirEffect,
-      FWriteDir: WriteDirEffect,
-      TOSErrorCode: OSErrorCode
-  ].}
-  const
-    doslikeFileSystem* = defined(windows) or defined(OS2) or defined(DOS)
-
-  when defined(Nimdoc): # only for proper documentation:
-    const
-      CurDir* = '.'
-        ## The constant string used by the operating system to refer to the
-        ## current directory.
-        ##
-        ## For example: '.' for POSIX or ':' for the classic Macintosh.
-
-      ParDir* = ".."
-        ## The constant string used by the operating system to refer to the
-        ## parent directory.
-        ##
-        ## For example: ".." for POSIX or "::" for the classic Macintosh.
-
-      DirSep* = '/'
-        ## The character used by the operating system to separate pathname
-        ## components, for example, '/' for POSIX or ':' for the classic
-        ## Macintosh.
-
-      AltSep* = '/'
-        ## An alternative character used by the operating system to separate
-        ## pathname components, or the same as `DirSep` if only one separator
-        ## character exists. This is set to '/' on Windows systems
-        ## where `DirSep` is a backslash.
-
-      PathSep* = ':'
-        ## The character conventionally used by the operating system to separate
-        ## search patch components (as in PATH), such as ':' for POSIX
-        ## or ';' for Windows.
-
-      FileSystemCaseSensitive* = true
-        ## true if the file system is case sensitive, false otherwise. Used by
-        ## `cmpPaths` to compare filenames properly.
-
-      ExeExt* = ""
-        ## The file extension of native executables. For example:
-        ## "" for POSIX, "exe" on Windows.
-
-      ScriptExt* = ""
-        ## The file extension of a script file. For example: "" for POSIX,
-        ## "bat" on Windows.
-
-      DynlibFormat* = "lib$1.so"
-        ## The format string to turn a filename into a `DLL`:idx: file (also
-        ## called `shared object`:idx: on some operating systems).
+include "system/inclrtl"
 
-  elif defined(macos):
-    const
-      CurDir* = ':'
-      ParDir* = "::"
-      DirSep* = ':'
-      AltSep* = Dirsep
-      PathSep* = ','
-      FileSystemCaseSensitive* = false
-      ExeExt* = ""
-      ScriptExt* = ""
-      DynlibFormat* = "$1.dylib"
-
-    #  MacOS paths
-    #  ===========
-    #  MacOS directory separator is a colon ":" which is the only character not
-    #  allowed in filenames.
-    #
-    #  A path containing no colon or which begins with a colon is a partial
-    #  path.
-    #  E.g. ":kalle:petter" ":kalle" "kalle"
-    #
-    #  All other paths are full (absolute) paths. E.g. "HD:kalle:" "HD:"
-    #  When generating paths, one is safe if one ensures that all partial paths
-    #  begin with a colon, and all full paths end with a colon.
-    #  In full paths the first name (e g HD above) is the name of a mounted
-    #  volume.
-    #  These names are not unique, because, for instance, two diskettes with the
-    #  same names could be inserted. This means that paths on MacOS are not
-    #  waterproof. In case of equal names the first volume found will do.
-    #  Two colons "::" are the relative path to the parent. Three is to the
-    #  grandparent etc.
-  elif doslikeFileSystem:
-    const
-      CurDir* = '.'
-      ParDir* = ".."
-      DirSep* = '\\' # seperator within paths
-      AltSep* = '/'
-      PathSep* = ';' # seperator between paths
-      FileSystemCaseSensitive* = false
-      ExeExt* = "exe"
-      ScriptExt* = "bat"
-      DynlibFormat* = "$1.dll"
-  elif defined(PalmOS) or defined(MorphOS):
-    const
-      DirSep* = '/'
-      AltSep* = Dirsep
-      PathSep* = ';'
-      ParDir* = ".."
-      FileSystemCaseSensitive* = false
-      ExeExt* = ""
-      ScriptExt* = ""
-      DynlibFormat* = "$1.prc"
-  elif defined(RISCOS):
-    const
-      DirSep* = '.'
-      AltSep* = '.'
-      ParDir* = ".." # is this correct?
-      PathSep* = ','
-      FileSystemCaseSensitive* = true
-      ExeExt* = ""
-      ScriptExt* = ""
-      DynlibFormat* = "lib$1.so"
-  else: # UNIX-like operating system
-    const
-      CurDir* = '.'
-      ParDir* = ".."
-      DirSep* = '/'
-      AltSep* = DirSep
-      PathSep* = ':'
-      FileSystemCaseSensitive* = true
-      ExeExt* = ""
-      ScriptExt* = ""
-      DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"
+import strutils
+
+type
+  ReadEnvEffect* = object of ReadIOEffect   ## effect that denotes a read
+                                            ## from an environment variable
+  WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
+                                            ## to an environment variable
+
+  ReadDirEffect* = object of ReadIOEffect   ## effect that denotes a read
+                                            ## operation from the directory
+                                            ## structure
+  WriteDirEffect* = object of WriteIOEffect ## effect that denotes a write
+                                            ## operation to
+                                            ## the directory structure
+
+  OSErrorCode* = distinct int32 ## Specifies an OS Error Code.
 
+{.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
+    FReadDir: ReadDirEffect,
+    FWriteDir: WriteDirEffect,
+    TOSErrorCode: OSErrorCode
+].}
+const
+  doslikeFileSystem* = defined(windows) or defined(OS2) or defined(DOS)
+
+when defined(Nimdoc): # only for proper documentation:
   const
-    ExtSep* = '.'
-      ## The character which separates the base filename from the extension;
-      ## for example, the '.' in ``os.nim``.
-
-
-  proc joinPath*(head, tail: string): string {.
-    noSideEffect, rtl, extern: "nos$1".} =
-    ## Joins two directory names to one.
-    ##
-    ## For example on Unix:
-    ##
-    ## .. code-block:: nim
-    ##   joinPath("usr", "lib")
-    ##
-    ## results in:
-    ##
-    ## .. code-block:: nim
-    ##   "usr/lib"
-    ##
-    ## 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. More
-    ## examples on Unix:
-    ##
-    ## .. code-block:: nim
-    ##   assert joinPath("usr", "") == "usr/"
-    ##   assert joinPath("", "lib") == "lib"
-    ##   assert joinPath("", "/lib") == "/lib"
-    ##   assert joinPath("usr/", "/lib") == "usr/lib"
-    if len(head) == 0:
-      result = tail
-    elif head[len(head)-1] in {DirSep, AltSep}:
-      if tail[0] in {DirSep, AltSep}:
-        result = head & substr(tail, 1)
-      else:
-        result = head & tail
-    else:
-      if 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.
-    result = parts[0]
-    for i in 1..high(parts):
-      result = joinPath(result, parts[i])
-
-  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"
-    return joinPath(head, tail)
-
-  proc splitPath*(path: string): tuple[head, tail: string] {.
-    noSideEffect, rtl, extern: "nos$1".} =
-    ## Splits a directory into (head, tail), so that
-    ## ``head / tail == path`` (except for edge cases like "/usr").
-    ##
-    ## Examples:
-    ##
-    ## .. code-block:: nim
-    ##   splitPath("usr/local/bin") -> ("usr/local", "bin")
-    ##   splitPath("usr/local/bin/") -> ("usr/local/bin", "")
-    ##   splitPath("bin") -> ("", "bin")
-    ##   splitPath("/bin") -> ("", "bin")
-    ##   splitPath("") -> ("", "")
-    var sepPos = -1
-    for i in countdown(len(path)-1, 0):
-      if path[i] in {DirSep, AltSep}:
-        sepPos = i
-        break
-    if sepPos >= 0:
-      result.head = substr(path, 0, sepPos-1)
-      result.tail = substr(path, sepPos+1)
+    CurDir* = '.'
+      ## The constant string used by the operating system to refer to the
+      ## current directory.
+      ##
+      ## For example: '.' for POSIX or ':' for the classic Macintosh.
+
+    ParDir* = ".."
+      ## The constant string used by the operating system to refer to the
+      ## parent directory.
+      ##
+      ## For example: ".." for POSIX or "::" for the classic Macintosh.
+
+    DirSep* = '/'
+      ## The character used by the operating system to separate pathname
+      ## components, for example, '/' for POSIX or ':' for the classic
+      ## Macintosh.
+
+    AltSep* = '/'
+      ## An alternative character used by the operating system to separate
+      ## pathname components, or the same as `DirSep` if only one separator
+      ## character exists. This is set to '/' on Windows systems
+      ## where `DirSep` is a backslash.
+
+    PathSep* = ':'
+      ## The character conventionally used by the operating system to separate
+      ## search patch components (as in PATH), such as ':' for POSIX
+      ## or ';' for Windows.
+
+    FileSystemCaseSensitive* = true
+      ## true if the file system is case sensitive, false otherwise. Used by
+      ## `cmpPaths` to compare filenames properly.
+
+    ExeExt* = ""
+      ## The file extension of native executables. For example:
+      ## "" for POSIX, "exe" on Windows.
+
+    ScriptExt* = ""
+      ## The file extension of a script file. For example: "" for POSIX,
+      ## "bat" on Windows.
+
+    DynlibFormat* = "lib$1.so"
+      ## The format string to turn a filename into a `DLL`:idx: file (also
+      ## called `shared object`:idx: on some operating systems).
+
+elif defined(macos):
+  const
+    CurDir* = ':'
+    ParDir* = "::"
+    DirSep* = ':'
+    AltSep* = Dirsep
+    PathSep* = ','
+    FileSystemCaseSensitive* = false
+    ExeExt* = ""
+    ScriptExt* = ""
+    DynlibFormat* = "$1.dylib"
+
+  #  MacOS paths
+  #  ===========
+  #  MacOS directory separator is a colon ":" which is the only character not
+  #  allowed in filenames.
+  #
+  #  A path containing no colon or which begins with a colon is a partial
+  #  path.
+  #  E.g. ":kalle:petter" ":kalle" "kalle"
+  #
+  #  All other paths are full (absolute) paths. E.g. "HD:kalle:" "HD:"
+  #  When generating paths, one is safe if one ensures that all partial paths
+  #  begin with a colon, and all full paths end with a colon.
+  #  In full paths the first name (e g HD above) is the name of a mounted
+  #  volume.
+  #  These names are not unique, because, for instance, two diskettes with the
+  #  same names could be inserted. This means that paths on MacOS are not
+  #  waterproof. In case of equal names the first volume found will do.
+  #  Two colons "::" are the relative path to the parent. Three is to the
+  #  grandparent etc.
+elif doslikeFileSystem:
+  const
+    CurDir* = '.'
+    ParDir* = ".."
+    DirSep* = '\\' # seperator within paths
+    AltSep* = '/'
+    PathSep* = ';' # seperator between paths
+    FileSystemCaseSensitive* = false
+    ExeExt* = "exe"
+    ScriptExt* = "bat"
+    DynlibFormat* = "$1.dll"
+elif defined(PalmOS) or defined(MorphOS):
+  const
+    DirSep* = '/'
+    AltSep* = Dirsep
+    PathSep* = ';'
+    ParDir* = ".."
+    FileSystemCaseSensitive* = false
+    ExeExt* = ""
+    ScriptExt* = ""
+    DynlibFormat* = "$1.prc"
+elif defined(RISCOS):
+  const
+    DirSep* = '.'
+    AltSep* = '.'
+    ParDir* = ".." # is this correct?
+    PathSep* = ','
+    FileSystemCaseSensitive* = true
+    ExeExt* = ""
+    ScriptExt* = ""
+    DynlibFormat* = "lib$1.so"
+else: # UNIX-like operating system
+  const
+    CurDir* = '.'
+    ParDir* = ".."
+    DirSep* = '/'
+    AltSep* = DirSep
+    PathSep* = ':'
+    FileSystemCaseSensitive* = true
+    ExeExt* = ""
+    ScriptExt* = ""
+    DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"
+
+const
+  ExtSep* = '.'
+    ## The character which separates the base filename from the extension;
+    ## for example, the '.' in ``os.nim``.
+
+
+proc joinPath*(head, tail: string): string {.
+  noSideEffect, rtl, extern: "nos$1".} =
+  ## Joins two directory names to one.
+  ##
+  ## For example on Unix:
+  ##
+  ## .. code-block:: nim
+  ##   joinPath("usr", "lib")
+  ##
+  ## results in:
+  ##
+  ## .. code-block:: nim
+  ##   "usr/lib"
+  ##
+  ## 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. More
+  ## examples on Unix:
+  ##
+  ## .. code-block:: nim
+  ##   assert joinPath("usr", "") == "usr/"
+  ##   assert joinPath("", "lib") == "lib"
+  ##   assert joinPath("", "/lib") == "/lib"
+  ##   assert joinPath("usr/", "/lib") == "usr/lib"
+  if len(head) == 0:
+    result = tail
+  elif head[len(head)-1] in {DirSep, AltSep}:
+    if tail[0] in {DirSep, AltSep}:
+      result = head & substr(tail, 1)
     else:
-      result.head = ""
-      result.tail = path
-
-  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 often the same as the ``head`` result of ``splitPath``.
-    ## If there is no parent, "" is returned.
-    ## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
-    ## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
-    let sepPos = parentDirPos(path)
-    if sepPos >= 0:
-      result = substr(path, 0, sepPos-1)
+      result = head & tail
+  else:
+    if tail[0] in {DirSep, AltSep}:
+      result = head & tail
     else:
-      result = ""
-
-  proc tailDir*(path: string): string {.
-    noSideEffect, rtl, extern: "nos$1".} =
-    ## Returns the tail part of `path`..
-    ##
-    ## | Example: ``tailDir("/usr/local/bin") == "local/bin"``.
-    ## | Example: ``tailDir("usr/local/bin/") == "local/bin"``.
-    ## | Example: ``tailDir("bin") == ""``.
-    var q = 1
-    if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
-    for i in 0..len(path)-q:
-      if path[i] in {DirSep, AltSep}:
-        return substr(path, i+1)
+      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.
+  result = parts[0]
+  for i in 1..high(parts):
+    result = joinPath(result, parts[i])
+
+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"
+  return joinPath(head, tail)
+
+proc splitPath*(path: string): tuple[head, tail: string] {.
+  noSideEffect, rtl, extern: "nos$1".} =
+  ## Splits a directory into (head, tail), so that
+  ## ``head / tail == path`` (except for edge cases like "/usr").
+  ##
+  ## Examples:
+  ##
+  ## .. code-block:: nim
+  ##   splitPath("usr/local/bin") -> ("usr/local", "bin")
+  ##   splitPath("usr/local/bin/") -> ("usr/local/bin", "")
+  ##   splitPath("bin") -> ("", "bin")
+  ##   splitPath("/bin") -> ("", "bin")
+  ##   splitPath("") -> ("", "")
+  var sepPos = -1
+  for i in countdown(len(path)-1, 0):
+    if path[i] in {DirSep, AltSep}:
+      sepPos = i
+      break
+  if sepPos >= 0:
+    result.head = substr(path, 0, sepPos-1)
+    result.tail = substr(path, sepPos+1)
+  else:
+    result.head = ""
+    result.tail = path
+
+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 often the same as the ``head`` result of ``splitPath``.
+  ## If there is no parent, "" is returned.
+  ## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
+  ## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
+  let sepPos = parentDirPos(path)
+  if sepPos >= 0:
+    result = substr(path, 0, sepPos-1)
+  else:
     result = ""
 
-  proc isRootDir*(path: string): bool {.
-    noSideEffect, rtl, extern: "nos$1".} =
-    ## Checks whether a given `path` is a root directory
-    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 set, the traversal will start from the file system root
-    ## diretory. If `inclusive` is set, the original argument will be included
-    ## in the traversal.
-    ##
-    ## Relative paths won't be expanded by this proc. Instead, it will traverse
-    ## only the directories appearing in the relative path.
-    if not fromRoot:
-      var current = path
-      if inclusive: yield path
-      while true:
-        if current.isRootDir: break
-        current = current.parentDir
-        yield current
-    else:
-      for i in countup(0, 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.
-    let sepPos = parentDirPos(head)
-    if sepPos >= 0:
-      result = substr(head, 0, sepPos-1) / tail
-    else:
-      result = head / tail
-
-  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.
-    # BUGFIX: do not search until 0! .DS_Store is no file extension!
-    result = -1
-    for i in countdown(len(path)-1, 1):
+proc tailDir*(path: string): string {.
+  noSideEffect, rtl, extern: "nos$1".} =
+  ## Returns the tail part of `path`..
+  ##
+  ## | Example: ``tailDir("/usr/local/bin") == "local/bin"``.
+  ## | Example: ``tailDir("usr/local/bin/") == "local/bin"``.
+  ## | Example: ``tailDir("bin") == ""``.
+  var q = 1
+  if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
+  for i in 0..len(path)-q:
+    if path[i] in {DirSep, AltSep}:
+      return substr(path, i+1)
+  result = ""
+
+proc isRootDir*(path: string): bool {.
+  noSideEffect, rtl, extern: "nos$1".} =
+  ## Checks whether a given `path` is a root directory
+  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 set, the traversal will start from the file system root
+  ## diretory. If `inclusive` is set, the original argument will be included
+  ## in the traversal.
+  ##
+  ## Relative paths won't be expanded by this proc. Instead, it will traverse
+  ## only the directories appearing in the relative path.
+  if not fromRoot:
+    var current = path
+    if inclusive: yield path
+    while true:
+      if current.isRootDir: break
+      current = current.parentDir
+      yield current
+  else:
+    for i in countup(0, 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.
+  let sepPos = parentDirPos(head)
+  if sepPos >= 0:
+    result = substr(head, 0, sepPos-1) / tail
+  else:
+    result = head / tail
+
+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.
+  # 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, filename, extension).
+  ## `dir` does not end in `DirSep`.
+  ## `extension` includes the leading dot.
+  ##
+  ## Example:
+  ##
+  ## .. code-block:: nim
+  ##   var (dir, name, ext) = splitFile("usr/local/nimc.html")
+  ##   assert dir == "usr/local"
+  ##   assert name == "nimc"
+  ##   assert ext == ".html"
+  ##
+  ## 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.
+  if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
+    result = (path, "", "")
+  else:
+    var sepPos = -1
+    var dotPos = path.len
+    for i in countdown(len(path)-1, 0):
       if path[i] == ExtSep:
-        result = i
-        break
+        if dotPos == path.len and i > 0 and
+            path[i-1] notin {DirSep, AltSep}: dotPos = i
       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, filename, extension).
-    ## `dir` does not end in `DirSep`.
-    ## `extension` includes the leading dot.
-    ##
-    ## Example:
-    ##
-    ## .. code-block:: nim
-    ##   var (dir, name, ext) = splitFile("usr/local/nimc.html")
-    ##   assert dir == "usr/local"
-    ##   assert name == "nimc"
-    ##   assert ext == ".html"
-    ##
-    ## 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.
-    if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
-      result = (path, "", "")
-    else:
-      var sepPos = -1
-      var dotPos = path.len
-      for i in countdown(len(path)-1, 0):
-        if path[i] == ExtSep:
-          if dotPos == path.len and i > 0 and
-              path[i-1] notin {DirSep, AltSep}: dotPos = i
-        elif path[i] in {DirSep, AltSep}:
-          sepPos = i
-          break
-      result.dir = substr(path, 0, sepPos-1)
-      result.name = substr(path, sepPos+1, dotPos-1)
-      result.ext = substr(path, dotPos)
-
-  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)``.
-    if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
-      result = ""
-    else:
-      result = splitPath(path).tail
-
-
-  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.)
-    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.)
-    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 iff pathA == pathB
-    ## | < 0 iff pathA < pathB
-    ## | > 0 iff pathA > pathB
-    if FileSystemCaseSensitive:
-      result = cmp(pathA, pathB)
-    else:
-      when defined(nimscript):
-        result = cmpic(pathA, pathB)
-      elif defined(nimdoc): discard
-      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.
-    when doslikeFileSystem:
-      var len = len(path)
-      result = (len > 0 and path[0] in {'/', '\\'}) or
-               (len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
-    elif defined(macos):
-      result = path.len > 0 and path[0] != ':'
-    elif defined(RISCOS):
-      result = path[0] == '$'
-    elif defined(posix):
-      result = path[0] == '/'
-
-  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
+        sepPos = i
+        break
+    result.dir = substr(path, 0, sepPos-1)
+    result.name = substr(path, sepPos+1, dotPos-1)
+    result.ext = substr(path, dotPos)
+
+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)``.
+  if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
+    result = ""
+  else:
+    result = splitPath(path).tail
+
+
+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.)
+  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.)
+  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 iff pathA == pathB
+  ## | < 0 iff pathA < pathB
+  ## | > 0 iff pathA > pathB
+  if FileSystemCaseSensitive:
+    result = cmp(pathA, pathB)
+  else:
+    when defined(nimscript):
+      result = cmpic(pathA, pathB)
+    elif defined(nimdoc): discard
     else:
-      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 ':'
+      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.
+  when doslikeFileSystem:
+    var len = len(path)
+    result = (len > 0 and path[0] in {'/', '\\'}) or
+              (len > 1 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
+  elif defined(macos):
+    result = path.len > 0 and path[0] != ':'
+  elif defined(RISCOS):
+    result = path[0] == '$'
+  elif defined(posix):
+    result = path[0] == '/'
+
+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:
+    var start: int
+    if path[0] == '/':
+      # an absolute path
+      when doslikeFileSystem:
+        if drive != "":
+          result = drive & ":" & DirSep
         else:
           result = $DirSep
-        start = 1
-      elif path[0] == '.' and path[1] == '/':
-        # current directory
-        result = $CurDir
-        start = 2
+      elif defined(macos):
+        result = "" # must not start with ':'
       else:
-        result = ""
-        start = 0
-
-      var i = start
-      while i < len(path): # ../../../ --> ::::
-        if 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
+        result = $DirSep
+      start = 1
+    elif path[0] == '.' and path[1] == '/':
+      # current directory
+      result = $CurDir
+      start = 2
+    else:
+      result = ""
+      start = 0
+
+    var i = start
+    while i < len(path): # ../../../ --> ::::
+      if 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 & DirSep
-          inc(i, 3)
-        elif path[i] == '/':
-          add result, DirSep
-          inc(i)
+            add result, ParDir
         else:
-          add result, path[i]
-          inc(i)
-
-when defined(nimdoc) and not declared(os):
-  proc getEnv(x: string): string = discard
-  proc existsFile(x: string): bool = discard
-
-when declared(getEnv) or defined(nimscript):
-  proc getHomeDir*(): string {.rtl, extern: "nos$1",
-    tags: [ReadEnvEffect, ReadIOEffect].} =
-    ## Returns the home directory of the current user.
-    ##
-    ## This proc is wrapped by the expandTilde proc for the convenience of
-    ## processing paths coming from user configuration files.
-    when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
-    else: return string(getEnv("HOME")) & "/"
-
-  proc getConfigDir*(): string {.rtl, extern: "nos$1",
-    tags: [ReadEnvEffect, ReadIOEffect].} =
-    ## Returns the config directory of the current user for applications.
-    ##
-    ## On non-Windows OSs, this proc conforms to the XDG Base Directory
-    ## spec. Thus, this proc returns the value of the XDG_CONFIG_DIR environment
-    ## variable if it is set, and returns the default configuration directory,
-    ## "~/.config/", otherwise.
-    ##
-    ## An OS-dependent trailing slash is always present at the end of the
-    ## returned string; `\\` on Windows and `/` on all other OSs.
-    when defined(windows): return string(getEnv("APPDATA")) & "\\"
-    elif getEnv("XDG_CONFIG_DIR"): return string(getEnv("XDG_CONFIG_DIR")) & "/"
-    else: return string(getEnv("HOME")) & "/.config/"
-
-  proc getTempDir*(): string {.rtl, extern: "nos$1",
-    tags: [ReadEnvEffect, ReadIOEffect].} =
-    ## Returns the temporary directory of the current user for applications to
-    ## save temporary files in.
-    ##
-    ## **Please do not use this**: On Android, it currently
-    ## returns ``getHomeDir()``, and on other Unix based systems it can cause
-    ## security problems too. That said, you can override this implementation
-    ## by adding ``-d:tempDir=mytempname`` to your compiler invokation.
-    when defined(tempDir):
-      const tempDir {.strdefine.}: string = nil
-      return tempDir
-    elif defined(windows): return string(getEnv("TEMP")) & "\\"
-    elif defined(android): return getHomeDir()
-    else: return "/tmp/"
-
-  proc expandTilde*(path: string): string {.
-    tags: [ReadEnvEffect, ReadIOEffect].} =
-    ## Expands a path starting with ``~/`` to a full path.
-    ##
-    ## If `path` starts with the tilde character and is followed by `/` or `\\`
-    ## this proc will return the reminder of the path appended to the result of
-    ## the getHomeDir() proc, otherwise the input path will be returned without
-    ## modification.
-    ##
-    ## The behaviour of this proc is the same on the Windows platform despite
-    ## not having this convention. Example:
-    ##
-    ## .. code-block:: nim
-    ##   let configFile = expandTilde("~" / "appname.cfg")
-    ##   echo configFile
-    ##   # --> C:\Users\amber\appname.cfg
-    if len(path) > 1 and path[0] == '~' and (path[1] == '/' or path[1] == '\\'):
-      result = getHomeDir() / path.substr(2)
-    else:
-      result = path
-
-  when not declared(split):
-    iterator split(s: string, sep: char): string =
-      var last = 0
-      if len(s) > 0:
-        while last <= len(s):
-          var first = last
-          while last < len(s) and s[last] != sep: inc(last)
-          yield substr(s, first, last-1)
-          inc(last)
-
-  when not defined(windows) and declared(os):
-    proc checkSymlink(path: string): bool =
-      var rawInfo: Stat
-      if lstat(path, rawInfo) < 0'i32: result = false
-      else: result = S_ISLNK(rawInfo.st_mode)
-
-  const
-    ExeExts* = when defined(windows): ["exe", "cmd", "bat"] else: [""] ## \
-      ## platform specific file extension for executables. On Windows
-      ## ``["exe", "cmd", "bat"]``, on Posix ``[""]``.
-
-  proc findExe*(exe: string, followSymlinks: bool = true;
-                extensions: openarray[string]=ExeExts): string {.
-    tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect].} =
-    ## Searches for `exe` in the current working directory and then
-    ## in directories listed in the ``PATH`` environment variable.
-    ## Returns "" if the `exe` cannot be found. `exe`
-    ## is added the `ExeExts <#ExeExts>`_ file extensions if it has none.
-    ## If the system supports symlinks it also resolves them until it
-    ## meets the actual file. This behavior can be disabled if desired.
-    for ext in extensions:
-      result = addFileExt(exe, ext)
-      if existsFile(result): return
-    var path = string(getEnv("PATH"))
-    for candidate in split(path, PathSep):
-      when defined(windows):
-        var x = (if candidate[0] == '"' and candidate[^1] == '"':
-                  substr(candidate, 1, candidate.len-2) else: candidate) /
-               exe
+          add result, ParDir & DirSep
+        inc(i, 3)
+      elif path[i] == '/':
+        add result, DirSep
+        inc(i)
       else:
-        var x = expandTilde(candidate) / exe
-      for ext in extensions:
-        var x = addFileExt(x, ext)
-        if existsFile(x):
-          when not defined(windows) and declared(os):
-            while followSymlinks: # doubles as if here
-              if x.checkSymlink:
-                var r = newString(256)
-                var len = readlink(x, r, 256)
-                if len < 0:
-                  raiseOSError(osLastError())
-                if len > 256:
-                  r = newString(len+1)
-                  len = readlink(x, r, len)
-                setLen(r, len)
-                if isAbsolute(r):
-                  x = r
-                else:
-                  x = parentDir(x) / r
-              else:
-                break
-          return x
-    result = ""
-
-when defined(nimscript) or (defined(nimdoc) and not declared(os)):
-  {.pop.} # hint[ConvFromXtoItselfNotNeeded]:off
+        add result, path[i]
+        inc(i)
+
+include "includes/oserr"
+include "includes/osenv"
+
+proc getHomeDir*(): string {.rtl, extern: "nos$1",
+  tags: [ReadEnvEffect, ReadIOEffect].} =
+  ## Returns the home directory of the current user.
+  ##
+  ## This proc is wrapped by the expandTilde proc for the convenience of
+  ## processing paths coming from user configuration files.
+  when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
+  else: return string(getEnv("HOME")) & "/"
+
+proc getConfigDir*(): string {.rtl, extern: "nos$1",
+  tags: [ReadEnvEffect, ReadIOEffect].} =
+  ## Returns the config directory of the current user for applications.
+  ##
+  ## On non-Windows OSs, this proc conforms to the XDG Base Directory
+  ## spec. Thus, this proc returns the value of the XDG_CONFIG_DIR environment
+  ## variable if it is set, and returns the default configuration directory,
+  ## "~/.config/", otherwise.
+  ##
+  ## An OS-dependent trailing slash is always present at the end of the
+  ## returned string; `\\` on Windows and `/` on all other OSs.
+  when defined(windows): return string(getEnv("APPDATA")) & "\\"
+  elif getEnv("XDG_CONFIG_DIR"): return string(getEnv("XDG_CONFIG_DIR")) & "/"
+  else: return string(getEnv("HOME")) & "/.config/"
+
+proc getTempDir*(): string {.rtl, extern: "nos$1",
+  tags: [ReadEnvEffect, ReadIOEffect].} =
+  ## Returns the temporary directory of the current user for applications to
+  ## save temporary files in.
+  ##
+  ## **Please do not use this**: On Android, it currently
+  ## returns ``getHomeDir()``, and on other Unix based systems it can cause
+  ## security problems too. That said, you can override this implementation
+  ## by adding ``-d:tempDir=mytempname`` to your compiler invokation.
+  when defined(tempDir):
+    const tempDir {.strdefine.}: string = nil
+    return tempDir
+  elif defined(windows): return string(getEnv("TEMP")) & "\\"
+  elif defined(android): return getHomeDir()
+  else: return "/tmp/"
+
+proc expandTilde*(path: string): string {.
+  tags: [ReadEnvEffect, ReadIOEffect].} =
+  ## Expands a path starting with ``~/`` to a full path.
+  ##
+  ## If `path` starts with the tilde character and is followed by `/` or `\\`
+  ## this proc will return the reminder of the path appended to the result of
+  ## the getHomeDir() proc, otherwise the input path will be returned without
+  ## modification.
+  ##
+  ## The behaviour of this proc is the same on the Windows platform despite
+  ## not having this convention. Example:
+  ##
+  ## .. code-block:: nim
+  ##   let configFile = expandTilde("~" / "appname.cfg")
+  ##   echo configFile
+  ##   # --> C:\Users\amber\appname.cfg
+  if len(path) > 1 and path[0] == '~' and (path[1] == '/' or path[1] == '\\'):
+    result = getHomeDir() / path.substr(2)
+  else:
+    result = path