summary refs log tree commit diff stats
path: root/lib/pure/os.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/os.nim')
-rw-r--r--[-rwxr-xr-x]lib/pure/os.nim2023
1 files changed, 917 insertions, 1106 deletions
diff --git a/lib/pure/os.nim b/lib/pure/os.nim
index 129774f6e..78ebb1c88 100755..100644
--- a/lib/pure/os.nim
+++ b/lib/pure/os.nim
@@ -1,611 +1,925 @@
 #
 #
-#            Nimrod's Runtime Library
-#        (c) Copyright 2010 Andreas Rumpf
+#            Nim's Runtime Library
+#        (c) Copyright 2015 Andreas Rumpf
 #
 #    See the file "copying.txt", included in this
 #    distribution, for details about the copyright.
 #
 
 ## This module contains basic operating system facilities like
-## retrieving environment variables, reading command line arguments,
-## working with directories, running shell commands, etc.
-{.deadCodeElim: on.}
+## retrieving environment variables, working with directories,
+## running shell commands, etc.
 
-{.push debugger: off.}
+## .. importdoc:: symlinks.nim, appdirs.nim, dirs.nim, ospaths2.nim
 
-import
-  strutils, times
+runnableExamples:
+  let myFile = "/path/to/my/file.nim"
+  assert splitPath(myFile) == (head: "/path/to/my", tail: "file.nim")
+  when defined(posix):
+    assert parentDir(myFile) == "/path/to/my"
+  assert splitFile(myFile) == (dir: "/path/to/my", name: "file", ext: ".nim")
+  assert myFile.changeFileExt("c") == "/path/to/my/file.c"
+
+## **See also:**
+## * `paths <paths.html>`_ and `files <files.html>`_ modules for high-level file manipulation
+## * `osproc module <osproc.html>`_ for process communication beyond
+##   `execShellCmd proc`_
+## * `uri module <uri.html>`_
+## * `distros module <distros.html>`_
+## * `dynlib module <dynlib.html>`_
+## * `streams module <streams.html>`_
+import std/private/ospaths2
+export ospaths2
+
+import std/private/osfiles
+export osfiles
+
+import std/private/osdirs
+export osdirs
+
+import std/private/ossymlinks
+export ossymlinks
+
+import std/private/osappdirs
+export osappdirs
+
+import std/private/oscommon
+
+include system/inclrtl
+import std/private/since
+
+import std/cmdline
+export cmdline
 
-when defined(windows):
-  import winlean
-elif defined(posix): 
-  import posix
+import std/[strutils, pathnorm]
+
+when defined(nimPreviewSlimSystem):
+  import std/[syncio, assertions, widestrs]
+
+const weirdTarget = defined(nimscript) or defined(js)
+
+since (1, 1):
+  const
+    invalidFilenameChars* = {'/', '\\', ':', '*', '?', '"', '<', '>', '|', '^', '\0'} ## \
+    ## Characters that may produce invalid filenames across Linux, Windows and Mac.
+    ## You can check if your filename contains any of these chars and strip them for safety.
+    ## Mac bans ``':'``, Linux bans ``'/'``, Windows bans all others.
+    invalidFilenames* = [
+      "CON", "PRN", "AUX", "NUL",
+      "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
+      "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"] ## \
+    ## Filenames that may be invalid across Linux, Windows, Mac, etc.
+    ## You can check if your filename match these and rename it for safety
+    ## (Currently all invalid filenames are from Windows only).
+
+when weirdTarget:
+  discard
+elif defined(windows):
+  import std/[winlean, times]
+elif defined(posix):
+  import std/[posix, times]
+
+  proc toTime(ts: Timespec): times.Time {.inline.} =
+    result = initTime(ts.tv_sec.int64, ts.tv_nsec.int)
 else:
   {.error: "OS module not ported to your operating system!".}
 
-include "system/ansi_c"
+when weirdTarget:
+  {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".}
+else:
+  {.pragma: noWeirdTarget.}
 
-const
-  doslike = defined(windows) or defined(OS2) or defined(DOS)
-    # DOS-like filesystem
+when defined(nimscript):
+  # for procs already defined in scriptconfig.nim
+  template noNimJs(body): untyped = discard
+elif defined(js):
+  {.pragma: noNimJs, error: "this proc is not available on the js target".}
+else:
+  {.pragma: noNimJs.}
 
-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.
-
-elif defined(macos):
-  const
-    curdir* = ':'
-    pardir* = "::"
-    dirsep* = ':'
-    altsep* = dirsep
-    pathsep* = ','
-    FileSystemCaseSensitive* = false
-    ExeExt* = ""
-    ScriptExt* = ""
-
-  #  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 doslike:
-  const
-    curdir* = '.'
-    pardir* = ".."
-    dirsep* = '\\' # seperator within paths
-    altsep* = '/'
-    pathSep* = ';' # seperator between paths
-    FileSystemCaseSensitive* = false
-    ExeExt* = "exe"
-    ScriptExt* = "bat"
-elif defined(PalmOS) or defined(MorphOS):
-  const
-    dirsep* = '/'
-    altsep* = dirsep
-    PathSep* = ';'
-    pardir* = ".."
-    FileSystemCaseSensitive* = false
-    ExeExt* = ""
-    ScriptExt* = ""
-elif defined(RISCOS):
-  const
-    dirsep* = '.'
-    altsep* = '.'
-    pardir* = ".." # is this correct?
-    pathSep* = ','
-    FileSystemCaseSensitive* = true
-    ExeExt* = ""
-    ScriptExt* = ""
-else: # UNIX-like operating system
-  const
-    curdir* = '.'
-    pardir* = ".."
-    dirsep* = '/'
-    altsep* = dirsep
-    pathSep* = ':'
-    FileSystemCaseSensitive* = true
-    ExeExt* = ""
-    ScriptExt* = ""
 
-const
-  ExtSep* = '.'
-    ## The character which separates the base filename from the extension;
-    ## for example, the '.' in ``os.nim``.
-
-# procs dealing with command line arguments:
-proc paramCount*(): int
-  ## Returns the number of command line arguments given to the
-  ## application.
-
-proc paramStr*(i: int): string
-  ## Returns the `i`-th command line arguments given to the
-  ## application.
+import std/oserrors
+export oserrors
+import std/envvars
+export envvars
+
+import std/private/osseps
+export osseps
+
+
+
+proc expandTilde*(path: string): string {.
+  tags: [ReadEnvEffect, ReadIOEffect].} =
+  ## Expands ``~`` or a path starting with ``~/`` to a full path, replacing
+  ## ``~`` with `getHomeDir()`_ (otherwise returns ``path`` unmodified).
   ##
-  ## `i` should be in the range `1..paramCount()`, else
-  ## the `EOutOfIndex` exception is raised.
-
-proc OSError*(msg: string = "") {.noinline.} =
-  ## raises an EOS 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.
-  if len(msg) == 0:
-    when defined(Windows):
-      var err = GetLastError()
-      if err != 0'i32:
-        # sigh, why is this is so difficult?
-        var msgbuf: cstring
-        if FormatMessageA(0x00000100 or 0x00001000 or 0x00000200,
-                          nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
-          var m = $msgbuf
-          if msgbuf != nil:
-            LocalFree(msgbuf)
-          raise newException(EOS, m)
-    if errno != 0'i32:
-      raise newException(EOS, $os.strerror(errno))
-    else:
-      raise newException(EOS, "unknown OS error")
+  ## Windows: this is still supported despite the Windows platform not having this
+  ## convention; also, both ``~/`` and ``~\`` are handled.
+  ##
+  ## See also:
+  ## * `getHomeDir proc`_
+  ## * `getConfigDir proc`_
+  ## * `getTempDir proc`_
+  ## * `getCurrentDir proc`_
+  ## * `setCurrentDir proc`_
+  runnableExamples:
+    assert expandTilde("~" / "appname.cfg") == getHomeDir() / "appname.cfg"
+    assert expandTilde("~/foo/bar") == getHomeDir() / "foo/bar"
+    assert expandTilde("/foo/bar") == "/foo/bar"
+
+  if len(path) == 0 or path[0] != '~':
+    result = path
+  elif len(path) == 1:
+    result = getHomeDir()
+  elif (path[1] in {DirSep, AltSep}):
+    result = getHomeDir() / path.substr(2)
   else:
-    raise newException(EOS, msg)
+    # TODO: handle `~bob` and `~bob/` which means home of bob
+    result = path
 
-proc UnixToNativePath*(path: string): string {.noSideEffect.} =
-  ## Converts an UNIX-like path to a native one.
+proc quoteShellWindows*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} =
+  ## Quote `s`, so it can be safely passed to Windows API.
   ##
-  ## On an UNIX system this does nothing. Else it converts
-  ## '/', '.', '..' to the appropriate things.
-  when defined(unix):
-    result = path
+  ## Based on Python's `subprocess.list2cmdline`.
+  ## See `this link <http://msdn.microsoft.com/en-us/library/17w5ykft.aspx>`_
+  ## for more details.
+  let needQuote = {' ', '\t'} in s or s.len == 0
+  result = ""
+  var backslashBuff = ""
+  if needQuote:
+    result.add("\"")
+
+  for c in s:
+    if c == '\\':
+      backslashBuff.add(c)
+    elif c == '\"':
+      for i in 0..<backslashBuff.len*2:
+        result.add('\\')
+      backslashBuff.setLen(0)
+      result.add("\\\"")
+    else:
+      if backslashBuff.len != 0:
+        result.add(backslashBuff)
+        backslashBuff.setLen(0)
+      result.add(c)
+
+  if backslashBuff.len > 0:
+    result.add(backslashBuff)
+  if needQuote:
+    result.add(backslashBuff)
+    result.add("\"")
+
+
+proc quoteShellPosix*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} =
+  ## Quote ``s``, so it can be safely passed to POSIX shell.
+  const safeUnixChars = {'%', '+', '-', '.', '/', '_', ':', '=', '@',
+                         '0'..'9', 'A'..'Z', 'a'..'z'}
+  if s.len == 0:
+    result = "''"
+  elif s.allCharsInSet(safeUnixChars):
+    result = s
   else:
-    var start: int
-    if path[0] == '/':
-      # an absolute path
-      when doslike:
-        result = r"C:\"
-      elif defined(macos):
-        result = "" # must not start with ':'
-      else:
-        result = $dirSep
-      start = 1
-    elif path[0] == '.' and path[1] == '/':
-      # current directory
-      result = $curdir
-      start = 2
+    result = "'" & s.replace("'", "'\"'\"'") & "'"
+
+when defined(windows) or defined(posix) or defined(nintendoswitch):
+  proc quoteShell*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} =
+    ## Quote ``s``, so it can be safely passed to shell.
+    ##
+    ## When on Windows, it calls `quoteShellWindows proc`_.
+    ## Otherwise, calls `quoteShellPosix proc`_.
+    when defined(windows):
+      result = quoteShellWindows(s)
     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
-        else:
-          add result, pardir & dirSep
-        inc(i, 3)
-      elif path[i] == '/':
-        add result, dirSep
-        inc(i)
-      else:
-        add result, path[i]
-        inc(i)
+      result = quoteShellPosix(s)
 
-proc existsFile*(filename: string): bool =
-  ## Returns true if the file exists, false otherwise.
-  when defined(windows):
-    var a = GetFileAttributesA(filename)
-    if a != -1'i32:
-      result = (a and FILE_ATTRIBUTE_DIRECTORY) == 0'i32
-  else:
-    var res: TStat
-    return stat(filename, res) >= 0'i32 and S_ISREG(res.st_mode)
+  proc quoteShellCommand*(args: openArray[string]): string =
+    ## Concatenates and quotes shell arguments `args`.
+    runnableExamples:
+      when defined(posix):
+        assert quoteShellCommand(["aaa", "", "c d"]) == "aaa '' 'c d'"
+      when defined(windows):
+        assert quoteShellCommand(["aaa", "", "c d"]) == "aaa \"\" \"c d\""
 
-proc existsDir*(dir: string): bool =
-  ## Returns true iff the directory `dir` exists. If `dir` is a file, false
-  ## is returned.
-  when defined(windows):
-    var a = GetFileAttributesA(dir)
-    if a != -1'i32:
-      result = (a and FILE_ATTRIBUTE_DIRECTORY) != 0'i32
-  else:
-    var res: TStat
-    return stat(dir, res) >= 0'i32 and S_ISDIR(res.st_mode)
+    # can't use `map` pending https://github.com/nim-lang/Nim/issues/8303
+    result = ""
+    for i in 0..<args.len:
+      if i > 0: result.add " "
+      result.add quoteShell(args[i])
+
+when not weirdTarget:
+  proc c_system(cmd: cstring): cint {.
+    importc: "system", header: "<stdlib.h>".}
 
-proc getLastModificationTime*(file: string): TTime =
+  when not defined(windows):
+    proc c_free(p: pointer) {.
+      importc: "free", header: "<stdlib.h>".}
+
+
+const
+  ExeExts* = ## Platform specific file extension for executables.
+    ## On Windows ``["exe", "cmd", "bat"]``, on Posix ``[""]``.
+    when defined(windows): ["exe", "cmd", "bat"] else: [""]
+
+proc findExe*(exe: string, followSymlinks: bool = true;
+              extensions: openArray[string]=ExeExts): string {.
+  tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect], noNimJs.} =
+  ## 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`_ 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
+  ## by setting `followSymlinks = false`.
+
+  if exe.len == 0: return
+  template checkCurrentDir() =
+    for ext in extensions:
+      result = addFileExt(exe, ext)
+      if fileExists(result): return
+  when defined(posix):
+    if '/' in exe: checkCurrentDir()
+  else:
+    checkCurrentDir()
+  let path = getEnv("PATH")
+  for candidate in split(path, PathSep):
+    if candidate.len == 0: continue
+    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 fileExists(x):
+        when not (defined(windows) or defined(nintendoswitch)):
+          while followSymlinks: # doubles as if here
+            if x.symlinkExists:
+              var r = newString(maxSymlinkLen)
+              var len = readlink(x.cstring, r.cstring, maxSymlinkLen)
+              if len < 0:
+                raiseOSError(osLastError(), exe)
+              if len > maxSymlinkLen:
+                r = newString(len+1)
+                len = readlink(x.cstring, r.cstring, len)
+              setLen(r, len)
+              if isAbsolute(r):
+                x = r
+              else:
+                x = parentDir(x) / r
+            else:
+              break
+        return x
+  result = ""
+
+when weirdTarget:
+  const times = "fake const"
+  template Time(x: untyped): untyped = string
+
+proc getLastModificationTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} =
   ## Returns the `file`'s last modification time.
+  ##
+  ## See also:
+  ## * `getLastAccessTime proc`_
+  ## * `getCreationTime proc`_
+  ## * `fileNewer proc`_
   when defined(posix):
-    var res: TStat
-    if stat(file, res) < 0'i32: OSError()
-    return res.st_mtime
+    var res: Stat
+    if stat(file, res) < 0'i32: raiseOSError(osLastError(), file)
+    result = res.st_mtim.toTime
   else:
-    var f: TWIN32_Find_Data
-    var h = findfirstFileA(file, f)
-    if h == -1'i32: OSError()
-    result = winTimeToUnixTime(rdFileTime(f.ftLastWriteTime))
-    findclose(h)
+    var f: WIN32_FIND_DATA
+    var h = findFirstFile(file, f)
+    if h == -1'i32: raiseOSError(osLastError(), file)
+    result = fromWinTime(rdFileTime(f.ftLastWriteTime))
+    findClose(h)
 
-proc getLastAccessTime*(file: string): TTime =
+proc getLastAccessTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} =
   ## Returns the `file`'s last read or write access time.
+  ##
+  ## See also:
+  ## * `getLastModificationTime proc`_
+  ## * `getCreationTime proc`_
+  ## * `fileNewer proc`_
   when defined(posix):
-    var res: TStat
-    if stat(file, res) < 0'i32: OSError()
-    return res.st_atime
+    var res: Stat
+    if stat(file, res) < 0'i32: raiseOSError(osLastError(), file)
+    result = res.st_atim.toTime
   else:
-    var f: TWIN32_Find_Data
-    var h = findfirstFileA(file, f)
-    if h == -1'i32: OSError()
-    result = winTimeToUnixTime(rdFileTime(f.ftLastAccessTime))
-    findclose(h)
+    var f: WIN32_FIND_DATA
+    var h = findFirstFile(file, f)
+    if h == -1'i32: raiseOSError(osLastError(), file)
+    result = fromWinTime(rdFileTime(f.ftLastAccessTime))
+    findClose(h)
 
-proc getCreationTime*(file: string): TTime = 
+proc getCreationTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} =
   ## Returns the `file`'s creation time.
+  ##
+  ## **Note:** Under POSIX OS's, the returned time may actually be the time at
+  ## which the file's attribute's were last modified. See
+  ## `here <https://github.com/nim-lang/Nim/issues/1058>`_ for details.
+  ##
+  ## See also:
+  ## * `getLastModificationTime proc`_
+  ## * `getLastAccessTime proc`_
+  ## * `fileNewer proc`_
   when defined(posix):
-    var res: TStat
-    if stat(file, res) < 0'i32: OSError()
-    return res.st_ctime
+    var res: Stat
+    if stat(file, res) < 0'i32: raiseOSError(osLastError(), file)
+    result = res.st_ctim.toTime
   else:
-    var f: TWIN32_Find_Data
-    var h = findfirstFileA(file, f)
-    if h == -1'i32: OSError()
-    result = winTimeToUnixTime(rdFileTime(f.ftCreationTime))
-    findclose(h)
+    var f: WIN32_FIND_DATA
+    var h = findFirstFile(file, f)
+    if h == -1'i32: raiseOSError(osLastError(), file)
+    result = fromWinTime(rdFileTime(f.ftCreationTime))
+    findClose(h)
 
-proc fileNewer*(a, b: string): bool =
+proc fileNewer*(a, b: string): bool {.rtl, extern: "nos$1", noWeirdTarget.} =
   ## Returns true if the file `a` is newer than file `b`, i.e. if `a`'s
   ## modification time is later than `b`'s.
-  result = getLastModificationTime(a) - getLastModificationTime(b) > 0
+  ##
+  ## See also:
+  ## * `getLastModificationTime proc`_
+  ## * `getLastAccessTime proc`_
+  ## * `getCreationTime proc`_
+  when defined(posix):
+    # If we don't have access to nanosecond resolution, use '>='
+    when not StatHasNanoseconds:
+      result = getLastModificationTime(a) >= getLastModificationTime(b)
+    else:
+      result = getLastModificationTime(a) > getLastModificationTime(b)
+  else:
+    result = getLastModificationTime(a) > getLastModificationTime(b)
 
-proc getCurrentDir*(): string =
-  ## Returns the current working directory.
-  const bufsize = 512 # should be enough
-  result = newString(bufsize)
+
+proc isAdmin*: bool {.noWeirdTarget.} =
+  ## Returns whether the caller's process is a member of the Administrators local
+  ## group (on Windows) or a root (on POSIX), via `geteuid() == 0`.
   when defined(windows):
-    var L = GetCurrentDirectoryA(bufsize, result)
-    if L == 0'i32: OSError()
-    setLen(result, L)
+    # Rewrite of the example from Microsoft Docs:
+    # https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership#examples
+    # and corresponding PostgreSQL function:
+    # https://doxygen.postgresql.org/win32security_8c.html#ae6b61e106fa5d6c5d077a9d14ee80569
+    var ntAuthority = SID_IDENTIFIER_AUTHORITY(value: SECURITY_NT_AUTHORITY)
+    var administratorsGroup: PSID
+    if not isSuccess(allocateAndInitializeSid(addr ntAuthority,
+                                              BYTE(2),
+                                              SECURITY_BUILTIN_DOMAIN_RID,
+                                              DOMAIN_ALIAS_RID_ADMINS,
+                                              0, 0, 0, 0, 0, 0,
+                                              addr administratorsGroup)):
+      raiseOSError(osLastError(), "could not get SID for Administrators group")
+
+    try:
+      var b: WINBOOL
+      if not isSuccess(checkTokenMembership(0, administratorsGroup, addr b)):
+        raiseOSError(osLastError(), "could not check access token membership")
+
+      result = isSuccess(b)
+    finally:
+      if freeSid(administratorsGroup) != nil:
+        raiseOSError(osLastError(), "failed to free SID for Administrators group")
+
   else:
-    if getcwd(result, bufsize) != nil:
-      setlen(result, c_strlen(result))
-    else:
-      OSError()
+    result = geteuid() == 0
+
 
-proc setCurrentDir*(newDir: string) {.inline.} =
-  ## Sets the current working directory; `EOS` is raised if
-  ## `newDir` cannot been set.
-  when defined(Windows):
-    if SetCurrentDirectoryA(newDir) == 0'i32: OSError()
+proc exitStatusLikeShell*(status: cint): cint =
+  ## Converts exit code from `c_system` into a shell exit code.
+  when defined(posix) and not weirdTarget:
+    if WIFSIGNALED(status):
+      # like the shell!
+      128 + WTERMSIG(status)
+    else:
+      WEXITSTATUS(status)
   else:
-    if chdir(newDir) != 0'i32: OSError()
+    status
 
-proc JoinPath*(head, tail: string): string {.noSideEffect.} =
-  ## Joins two directory names to one.
-  ##
-  ## For example on Unix:
+proc execShellCmd*(command: string): int {.rtl, extern: "nos$1",
+  tags: [ExecIOEffect], noWeirdTarget.} =
+  ## Executes a `shell command`:idx:.
   ##
-  ## .. code-block:: nimrod
-  ##   JoinPath("usr", "lib")
+  ## Command has the form 'program args' where args are the command
+  ## line arguments given to program. The proc returns the error code
+  ## of the shell when it has finished (zero if there is no error).
+  ## The proc does not return until the process has finished.
   ##
-  ## results in:
+  ## To execute a program without having a shell involved, use `osproc.execProcess proc
+  ## <osproc.html#execProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_.
   ##
-  ## .. code-block:: nimrod
-  ##   "usr/lib"
+  ## **Examples:**
+  ##   ```Nim
+  ##   discard execShellCmd("ls -la")
+  ##   ```
+  result = exitStatusLikeShell(c_system(command))
+
+proc expandFilename*(filename: string): string {.rtl, extern: "nos$1",
+  tags: [ReadDirEffect], noWeirdTarget.} =
+  ## Returns the full (`absolute`:idx:) path of an existing file `filename`.
   ##
-  ## If head is the empty string, tail is returned.
-  ## If tail is the empty string, head is returned.
-  if len(head) == 0:
-    result = tail
-  elif head[len(head)-1] in {DirSep, AltSep}:
-    if tail[0] in {DirSep, AltSep}:
-      result = head & copy(tail, 1)
-    else:
-      result = head & tail
+  ## Raises `OSError` in case of an error. Follows symlinks.
+  when defined(windows):
+    var bufsize = MAX_PATH.int32
+    var unused: WideCString = nil
+    var res = newWideCString(bufsize)
+    while true:
+      var L = getFullPathNameW(newWideCString(filename), bufsize, res, unused)
+      if L == 0'i32:
+        raiseOSError(osLastError(), filename)
+      elif L > bufsize:
+        res = newWideCString(L)
+        bufsize = L
+      else:
+        result = res$L
+        break
+    # getFullPathName doesn't do case corrections, so we have to use this convoluted
+    # way of retrieving the true filename
+    for x in walkFiles(result):
+      result = x
+    if not fileExists(result) and not dirExists(result):
+      # consider using: `raiseOSError(osLastError(), result)`
+      raise newException(OSError, "file '" & result & "' does not exist")
   else:
-    if tail[0] in {DirSep, AltSep}:
-      result = head & tail
+    # according to Posix we don't need to allocate space for result pathname.
+    # But we need to free return value with free(3).
+    var r = realpath(filename, nil)
+    if r.isNil:
+      raiseOSError(osLastError(), filename)
     else:
-      result = head & DirSep & tail
-
-proc JoinPath*(parts: openarray[string]): string {.noSideEffect.} =
-  ## The same as `JoinPath(head, tail)`, but works with any number
-  ## of directory parts.
-  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)``
-  return joinPath(head, tail)
-
-proc SplitPath*(path: string, head, tail: var string) {.noSideEffect,
-                                                        deprecated.} =
-  ## **Deprecated since version 0.8.2**: use the version that returns a tuple
-  ## instead
-  var
-    sepPos = -1
-  for i in countdown(len(path)-1, 0):
-    if path[i] in {dirsep, altsep}:
-      sepPos = i
-      break
-  if sepPos >= 0:
-    head = copy(path, 0, sepPos-1)
-    tail = copy(path, sepPos+1)
-  else:
-    head = ""
-    tail = path # make a string copy here
+      result = $r
+      c_free(cast[pointer](r))
 
-proc SplitPath*(path: string): tuple[head, tail: string] {.noSideEffect.} =
-  ## Splits a directory into (head, tail), so that
-  ## ``JoinPath(head, tail) == path``.
+proc getCurrentCompilerExe*(): string {.compileTime.} = discard
+  ## Returns the path of the currently running Nim compiler or nimble executable.
   ##
-  ## Examples: 
+  ## Can be used to retrieve the currently executing
+  ## Nim compiler from a Nim or nimscript program, or the nimble binary
+  ## inside a nimble program (likewise with other binaries built from
+  ## compiler API).
+
+proc createHardlink*(src, dest: string) {.noWeirdTarget.} =
+  ## Create a hard link at `dest` which points to the item specified
+  ## by `src`.
   ##
-  ## .. code-block:: nimrod
-  ##   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 = copy(path, 0, sepPos-1)
-    result.tail = copy(path, sepPos+1)
+  ## .. warning:: Some OS's restrict the creation of hard links to
+  ##   root users (administrators).
+  ##
+  ## See also:
+  ## * `createSymlink proc`_
+  when defined(windows):
+    var wSrc = newWideCString(src)
+    var wDst = newWideCString(dest)
+    if createHardLinkW(wDst, wSrc, nil) == 0:
+      raiseOSError(osLastError(), $(src, dest))
   else:
-    result.head = ""
-    result.tail = path
+    if link(src, dest) != 0:
+      raiseOSError(osLastError(), $(src, dest))
+
+proc inclFilePermissions*(filename: string,
+                          permissions: set[FilePermission]) {.
+  rtl, extern: "nos$1", tags: [ReadDirEffect, WriteDirEffect], noWeirdTarget.} =
+  ## A convenience proc for:
+  ##   ```nim
+  ##   setFilePermissions(filename, getFilePermissions(filename)+permissions)
+  ##   ```
+  setFilePermissions(filename, getFilePermissions(filename)+permissions)
 
-proc parentDir*(path: string): string {.noSideEffect.} =
-  ## Returns the parent directory of `path`.
-  ##
-  ## This is often the same as the ``head`` result of ``splitPath``.
-  ## If there is no parent, ``path`` is returned.
-  ## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
-  ## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
-  var
-    sepPos = -1
-    q = 1
-  if path[len(path)-1] in {dirsep, altsep}:
-    q = 2
-  for i in countdown(len(path)-q, 0):
-    if path[i] in {dirsep, altsep}:
-      sepPos = i
-      break
-  if sepPos >= 0:
-    result = copy(path, 0, sepPos-1)
+proc exclFilePermissions*(filename: string,
+                          permissions: set[FilePermission]) {.
+  rtl, extern: "nos$1", tags: [ReadDirEffect, WriteDirEffect], noWeirdTarget.} =
+  ## A convenience proc for:
+  ##   ```nim
+  ##   setFilePermissions(filename, getFilePermissions(filename)-permissions)
+  ##   ```
+  setFilePermissions(filename, getFilePermissions(filename)-permissions)
+
+when not weirdTarget and (defined(freebsd) or defined(dragonfly) or defined(netbsd)):
+  proc sysctl(name: ptr cint, namelen: cuint, oldp: pointer, oldplen: ptr csize_t,
+              newp: pointer, newplen: csize_t): cint
+       {.importc: "sysctl",header: """#include <sys/types.h>
+                                      #include <sys/sysctl.h>""".}
+  const
+    CTL_KERN = 1
+    KERN_PROC = 14
+    MAX_PATH = 1024
+
+  when defined(freebsd):
+    const KERN_PROC_PATHNAME = 12
+  elif defined(netbsd):
+    const KERN_PROC_ARGS = 48
+    const KERN_PROC_PATHNAME = 5
   else:
-    result = path
+    const KERN_PROC_PATHNAME = 9
 
-proc `/../` * (head, tail: string): string {.noSideEffect.} =
-  ## The same as ``parentDir(head) / tail``
-  return parentDir(head) / tail
+  proc getApplFreebsd(): string =
+    var pathLength = csize_t(0)
 
-proc normExt(ext: string): string =
-  if ext == "" or ext[0] == extSep: result = ext # no copy needed here
-  else: result = extSep & ext
+    when defined(netbsd):
+      var req = [CTL_KERN.cint, KERN_PROC_ARGS.cint, -1.cint, KERN_PROC_PATHNAME.cint]
+    else:
+      var req = [CTL_KERN.cint, KERN_PROC.cint, KERN_PROC_PATHNAME.cint, -1.cint]
 
-proc searchExtPos(s: string): int =
-  # BUGFIX: do not search until 0! .DS_Store is no file extension!
-  result = -1
-  for i in countdown(len(s)-1, 1):
-    if s[i] == extsep:
-      result = i
-      break
-    elif s[i] in {dirsep, altsep}:
-      break # do not skip over path
+    # first call to get the required length
+    var res = sysctl(addr req[0], 4, nil, addr pathLength, nil, 0)
+
+    if res < 0:
+      return ""
+
+    result.setLen(pathLength)
+    res = sysctl(addr req[0], 4, addr result[0], addr pathLength, nil, 0)
+
+    if res < 0:
+      return ""
+
+    let realLen = len(cstring(result))
+    setLen(result, realLen)
+
+when not weirdTarget and (defined(linux) or defined(solaris) or defined(bsd) or defined(aix)):
+  proc getApplAux(procPath: string): string =
+    result = newString(maxSymlinkLen)
+    var len = readlink(procPath, result.cstring, maxSymlinkLen)
+    if len > maxSymlinkLen:
+      result = newString(len+1)
+      len = readlink(procPath, result.cstring, len)
+    setLen(result, len)
+
+when not weirdTarget and defined(openbsd):
+  proc getApplOpenBsd(): string =
+    # similar to getApplHeuristic, but checks current working directory
+    when declared(paramStr):
+      result = ""
+
+      # POSIX guaranties that this contains the executable
+      # as it has been executed by the calling process
+      let exePath = paramStr(0)
+
+      if len(exePath) == 0:
+        return ""
 
-proc splitFile*(path: string): tuple[dir, name, ext: string] {.noSideEffect.} =
-  ## Splits a filename into (dir, filename, extension).
-  ## `dir` does not end in `DirSep`.
-  ## `extension` includes the leading dot.
+      if exePath[0] == DirSep:
+        # path is absolute
+        result = exePath
+      else:
+        # not an absolute path, check if it's relative to the current working directory
+        for i in 1..<len(exePath):
+          if exePath[i] == DirSep:
+            result = joinPath(getCurrentDir(), exePath)
+            break
+
+      if len(result) > 0:
+        return expandFilename(result)
+
+      # search in path
+      for p in split(getEnv("PATH"), {PathSep}):
+        var x = joinPath(p, exePath)
+        if fileExists(x):
+          return expandFilename(x)
+    else:
+      result = ""
+
+when not (defined(windows) or defined(macosx) or weirdTarget):
+  proc getApplHeuristic(): string =
+    when declared(paramStr):
+      result = paramStr(0)
+      # POSIX guaranties that this contains the executable
+      # as it has been executed by the calling process
+      if len(result) > 0 and result[0] != DirSep: # not an absolute path?
+        # iterate over any path in the $PATH environment variable
+        for p in split(getEnv("PATH"), {PathSep}):
+          var x = joinPath(p, result)
+          if fileExists(x): return x
+    else:
+      result = ""
+
+when defined(macosx):
+  type
+    cuint32* {.importc: "unsigned int", nodecl.} = int
+    ## This is the same as the type ``uint32_t`` in *C*.
+
+  # a really hacky solution: since we like to include 2 headers we have to
+  # define two procs which in reality are the same
+  proc getExecPath1(c: cstring, size: var cuint32) {.
+    importc: "_NSGetExecutablePath", header: "<sys/param.h>".}
+  proc getExecPath2(c: cstring, size: var cuint32): bool {.
+    importc: "_NSGetExecutablePath", header: "<mach-o/dyld.h>".}
+
+when defined(haiku):
+  const
+    PATH_MAX = 1024
+    B_FIND_PATH_IMAGE_PATH = 1000
+
+  proc find_path(codePointer: pointer, baseDirectory: cint, subPath: cstring,
+                 pathBuffer: cstring, bufferSize: csize_t): int32
+                {.importc, header: "<FindDirectory.h>".}
+
+  proc getApplHaiku(): string =
+    result = newString(PATH_MAX)
+
+    if find_path(nil, B_FIND_PATH_IMAGE_PATH, nil, result, PATH_MAX) == 0:
+      let realLen = len(cstring(result))
+      setLen(result, realLen)
+    else:
+      result = ""
+
+proc getAppFilename*(): string {.rtl, extern: "nos$1", tags: [ReadIOEffect], noWeirdTarget, raises: [].} =
+  ## Returns the filename of the application's executable.
+  ## This proc will resolve symlinks.
   ##
-  ## Example:
+  ## Returns empty string when name is unavailable
   ##
-  ## .. code-block:: nimrod
-  ##   var (dir, name, ext) = splitFile("usr/local/nimrodc.html")
-  ##   assert dir == "usr/local"
-  ##   assert name == "nimrodc"
-  ##   assert ext == ".html"
+  ## See also:
+  ## * `getAppDir proc`_
+  ## * `getCurrentCompilerExe proc`_
+
+  # Linux: /proc/<pid>/exe
+  # Solaris:
+  # /proc/<pid>/object/a.out (filename only)
+  # /proc/<pid>/path/a.out (complete pathname)
+  when defined(windows):
+    var bufsize = int32(MAX_PATH)
+    var buf = newWideCString(bufsize)
+    while true:
+      var L = getModuleFileNameW(0, buf, bufsize)
+      if L == 0'i32:
+        result = "" # error!
+        break
+      elif L > bufsize:
+        buf = newWideCString(L)
+        bufsize = L
+      else:
+        result = buf$L
+        break
+  elif defined(macosx):
+    var size = cuint32(0)
+    getExecPath1(nil, size)
+    result = newString(int(size))
+    if getExecPath2(result.cstring, size):
+      result = "" # error!
+    if result.len > 0:
+      try:
+        result = result.expandFilename
+      except OSError:
+        result = ""
+  else:
+    when defined(linux) or defined(aix):
+      result = getApplAux("/proc/self/exe")
+    elif defined(solaris):
+      result = getApplAux("/proc/" & $getpid() & "/path/a.out")
+    elif defined(genode):
+      result = "" # Not supported
+    elif defined(freebsd) or defined(dragonfly) or defined(netbsd):
+      result = getApplFreebsd()
+    elif defined(haiku):
+      result = getApplHaiku()
+    elif defined(openbsd):
+      result = try: getApplOpenBsd() except OSError: ""
+    elif defined(nintendoswitch):
+      result = ""
+
+    # little heuristic that may work on other POSIX-like systems:
+    if result.len == 0:
+      result = try: getApplHeuristic() except OSError: ""
+
+proc getAppDir*(): string {.rtl, extern: "nos$1", tags: [ReadIOEffect], noWeirdTarget.} =
+  ## Returns the directory of the application's executable.
   ##
-  ## 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, "", "")
+  ## See also:
+  ## * `getAppFilename proc`_
+  result = splitFile(getAppFilename()).dir
+
+proc sleep*(milsecs: int) {.rtl, extern: "nos$1", tags: [TimeEffect], noWeirdTarget.} =
+  ## Sleeps `milsecs` milliseconds.
+  ## A negative `milsecs` causes sleep to return immediately.
+  when defined(windows):
+    if milsecs < 0:
+      return  # fixes #23732
+    winlean.sleep(int32(milsecs))
   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: dotPos = i
-      elif path[i] in {dirsep, altsep}:
-        sepPos = i
-        break
-    result.dir = copy(path, 0, sepPos-1)
-    result.name = copy(path, sepPos+1, dotPos-1)
-    result.ext = copy(path, dotPos)
-
-proc extractDir*(path: string): string {.noSideEffect, deprecated.} =
-  ## Extracts the directory of a given path. This is almost the
-  ## same as the `head` result of `splitPath`, except that
-  ## ``extractDir("/usr/lib/") == "/usr/lib/"``.
-  ## **Deprecated since version 0.8.2**: Use ``splitFile(path).dir`` instead.
-  result = splitFile(path).dir
-
-proc extractFilename*(path: string): string {.noSideEffect.} =
-  ## 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 = ""
+    var a, b: Timespec
+    a.tv_sec = posix.Time(milsecs div 1000)
+    a.tv_nsec = (milsecs mod 1000) * 1000 * 1000
+    discard posix.nanosleep(a, b)
+
+proc getFileSize*(file: string): BiggestInt {.rtl, extern: "nos$1",
+  tags: [ReadIOEffect], noWeirdTarget.} =
+  ## Returns the file size of `file` (in bytes). ``OSError`` is
+  ## raised in case of an error.
+  when defined(windows):
+    var a: WIN32_FIND_DATA
+    var resA = findFirstFile(file, a)
+    if resA == -1: raiseOSError(osLastError(), file)
+    result = rdFileSize(a)
+    findClose(resA)
   else:
-    result = splitPath(path).tail
+    var rawInfo: Stat
+    if stat(file, rawInfo) < 0'i32:
+      raiseOSError(osLastError(), file)
+    rawInfo.st_size
+
+when defined(windows) or weirdTarget:
+  type
+    DeviceId* = int32
+    FileId* = int64
+else:
+  type
+    DeviceId* = Dev
+    FileId* = Ino
 
-proc expandFilename*(filename: string): string =
-  ## Returns the full path of `filename`, raises EOS in case of an error.
+type
+  FileInfo* = object
+    ## Contains information associated with a file object.
+    ##
+    ## See also:
+    ## * `getFileInfo(handle) proc`_
+    ## * `getFileInfo(file) proc`_
+    ## * `getFileInfo(path, followSymlink) proc`_
+    id*: tuple[device: DeviceId, file: FileId] ## Device and file id.
+    kind*: PathComponent              ## Kind of file object - directory, symlink, etc.
+    size*: BiggestInt                 ## Size of file.
+    permissions*: set[FilePermission] ## File permissions
+    linkCount*: BiggestInt            ## Number of hard links the file object has.
+    lastAccessTime*: times.Time       ## Time file was last accessed.
+    lastWriteTime*: times.Time        ## Time file was last modified/written to.
+    creationTime*: times.Time         ## Time file was created. Not supported on all systems!
+    blockSize*: int                   ## Preferred I/O block size for this object.
+                                      ## In some filesystems, this may vary from file to file.
+    isSpecial*: bool                  ## Is file special? (on Unix some "files"
+                                      ## can be special=non-regular like FIFOs,
+                                      ## devices); for directories `isSpecial`
+                                      ## is always `false`, for symlinks it is
+                                      ## the same as for the link's target.
+
+template rawToFormalFileInfo(rawInfo, path, formalInfo): untyped =
+  ## Transforms the native file info structure into the one nim uses.
+  ## 'rawInfo' is either a 'BY_HANDLE_FILE_INFORMATION' structure on Windows,
+  ## or a 'Stat' structure on posix
   when defined(windows):
-    var unused: cstring
-    result = newString(3072)
-    var L = GetFullPathNameA(filename, 3072'i32, result, unused)
-    if L <= 0'i32 or L >= 3072'i32: OSError()
-    setLen(result, L)
+    template merge[T](a, b): untyped =
+       cast[T](
+        (uint64(cast[uint32](a))) or
+        (uint64(cast[uint32](b)) shl 32)
+       )
+    formalInfo.id.device = rawInfo.dwVolumeSerialNumber
+    formalInfo.id.file = merge[FileId](rawInfo.nFileIndexLow, rawInfo.nFileIndexHigh)
+    formalInfo.size = merge[BiggestInt](rawInfo.nFileSizeLow, rawInfo.nFileSizeHigh)
+    formalInfo.linkCount = rawInfo.nNumberOfLinks
+    formalInfo.lastAccessTime = fromWinTime(rdFileTime(rawInfo.ftLastAccessTime))
+    formalInfo.lastWriteTime = fromWinTime(rdFileTime(rawInfo.ftLastWriteTime))
+    formalInfo.creationTime = fromWinTime(rdFileTime(rawInfo.ftCreationTime))
+    formalInfo.blockSize = 8192 # xxx use Windows API instead of hardcoding
+
+    # Retrieve basic permissions
+    if (rawInfo.dwFileAttributes and FILE_ATTRIBUTE_READONLY) != 0'i32:
+      formalInfo.permissions = {fpUserExec, fpUserRead, fpGroupExec,
+                                fpGroupRead, fpOthersExec, fpOthersRead}
+    else:
+      formalInfo.permissions = {fpUserExec..fpOthersRead}
+
+    # Retrieve basic file kind
+    if (rawInfo.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0'i32:
+      formalInfo.kind = pcDir
+    else:
+      formalInfo.kind = pcFile
+    if (rawInfo.dwFileAttributes and FILE_ATTRIBUTE_REPARSE_POINT) != 0'i32:
+      formalInfo.kind = succ(formalInfo.kind)
+
   else:
-    var res = realpath(filename, nil)
-    if res == nil: OSError()
-    result = $res
-    c_free(res)
-
-proc SplitFilename*(filename: string, name, extension: var string) {.
-  noSideEffect, deprecated.} = 
-  ## Splits a filename into (name, extension), so that
-  ## ``name & extension == filename``.
+    template checkAndIncludeMode(rawMode, formalMode: untyped) =
+      if (rawInfo.st_mode and rawMode.Mode) != 0.Mode:
+        formalInfo.permissions.incl(formalMode)
+    formalInfo.id = (rawInfo.st_dev, rawInfo.st_ino)
+    formalInfo.size = rawInfo.st_size
+    formalInfo.linkCount = rawInfo.st_nlink.BiggestInt
+    formalInfo.lastAccessTime = rawInfo.st_atim.toTime
+    formalInfo.lastWriteTime = rawInfo.st_mtim.toTime
+    formalInfo.creationTime = rawInfo.st_ctim.toTime
+    formalInfo.blockSize = rawInfo.st_blksize
+
+    formalInfo.permissions = {}
+    checkAndIncludeMode(S_IRUSR, fpUserRead)
+    checkAndIncludeMode(S_IWUSR, fpUserWrite)
+    checkAndIncludeMode(S_IXUSR, fpUserExec)
+
+    checkAndIncludeMode(S_IRGRP, fpGroupRead)
+    checkAndIncludeMode(S_IWGRP, fpGroupWrite)
+    checkAndIncludeMode(S_IXGRP, fpGroupExec)
+
+    checkAndIncludeMode(S_IROTH, fpOthersRead)
+    checkAndIncludeMode(S_IWOTH, fpOthersWrite)
+    checkAndIncludeMode(S_IXOTH, fpOthersExec)
+
+    (formalInfo.kind, formalInfo.isSpecial) =
+      if S_ISDIR(rawInfo.st_mode):
+        (pcDir, false)
+      elif S_ISLNK(rawInfo.st_mode):
+        assert(path != "") # symlinks can't occur for file handles
+        getSymlinkFileKind(path)
+      else:
+        (pcFile, not S_ISREG(rawInfo.st_mode))
+
+when defined(js):
+  when not declared(FileHandle):
+    type FileHandle = distinct int32
+  when not declared(File):
+    type File = object
+
+proc getFileInfo*(handle: FileHandle): FileInfo {.noWeirdTarget.} =
+  ## Retrieves file information for the file object represented by the given
+  ## handle.
   ##
-  ## Example: After ``SplitFilename("usr/local/nimrodc.html", name, ext)``,
-  ## `name` is "usr/local/nimrodc" and `ext` is ".html".
-  ## If the file has no extension, extension is the empty string.
-  ## **Deprecated since version 0.8.2**: Use ``splitFile(filename)`` instead.
-  var extPos = searchExtPos(filename)
-  if extPos >= 0:
-    name = copy(filename, 0, extPos-1)
-    extension = copy(filename, extPos)
+  ## If the information cannot be retrieved, such as when the file handle
+  ## is invalid, `OSError` is raised.
+  ##
+  ## See also:
+  ## * `getFileInfo(file) proc`_
+  ## * `getFileInfo(path, followSymlink) proc`_
+
+  # Done: ID, Kind, Size, Permissions, Link Count
+  when defined(windows):
+    var rawInfo: BY_HANDLE_FILE_INFORMATION
+    # We have to use the super special '_get_osfhandle' call (wrapped above)
+    # To transform the C file descriptor to a native file handle.
+    var realHandle = get_osfhandle(handle)
+    if getFileInformationByHandle(realHandle, addr rawInfo) == 0:
+      raiseOSError(osLastError(), $handle)
+    rawToFormalFileInfo(rawInfo, "", result)
   else:
-    name = filename # make a string copy here
-    extension = ""
-
-proc extractFileExt*(filename: string): string {.noSideEffect, deprecated.} =
-  ## Extracts the file extension of a given `filename`. This is the
-  ## same as the `extension` result of `splitFilename`.
-  ## **Deprecated since version 0.8.2**: Use ``splitFile(filename).ext``
-  ## instead.
-  result = splitFile(filename).ext
-
-proc extractFileTrunk*(filename: string): string {.noSideEffect, deprecated.} =
-  ## Extracts the file name of a given `filename`. This removes any
-  ## directory information and the file extension.
-  ## **Deprecated since version 0.8.2**: Use ``splitFile(path).name`` instead.
-  result = splitFile(filename).name
-  
-proc ChangeFileExt*(filename, ext: string): string {.noSideEffect.} =
-  ## Changes the file extension to `ext`.
+    var rawInfo: Stat
+    if fstat(handle, rawInfo) < 0'i32:
+      raiseOSError(osLastError(), $handle)
+    rawToFormalFileInfo(rawInfo, "", result)
+
+proc getFileInfo*(file: File): FileInfo {.noWeirdTarget.} =
+  ## Retrieves file information for the file object.
   ##
-  ## 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 = copy(filename, 0, extPos-1) & normExt(ext)
-
-proc addFileExt*(filename, ext: string): string {.noSideEffect.} =
-  ## Adds the file extension `ext` to `filename`, unless
-  ## `filename` already has an extension.
+  ## See also:
+  ## * `getFileInfo(handle) proc`_
+  ## * `getFileInfo(path, followSymlink) proc`_
+  if file.isNil:
+    raise newException(IOError, "File is nil")
+  result = getFileInfo(file.getFileHandle())
+
+proc getFileInfo*(path: string, followSymlink = true): FileInfo {.noWeirdTarget.} =
+  ## Retrieves file information for the file object pointed to by `path`.
   ##
-  ## `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 AppendFileExt*(filename, ext: string): string {.
-  noSideEffect, deprecated.} =
-  ## **Deprecated since version 0.8.2**: Use `addFileExt` instead.
-  result = addFileExt(filename, ext)
-
-proc cmpPaths*(pathA, pathB: string): int {.noSideEffect.} =
-  ## Compares two paths.
+  ## Due to intrinsic differences between operating systems, the information
+  ## contained by the returned `FileInfo object`_ will be slightly
+  ## different across platforms, and in some cases, incomplete or inaccurate.
   ##
-  ## On a case-sensitive filesystem this is done
-  ## case-sensitively otherwise case-insensitively. Returns:
+  ## When `followSymlink` is true (default), symlinks are followed and the
+  ## information retrieved is information related to the symlink's target.
+  ## Otherwise, information on the symlink itself is retrieved (however,
+  ## field `isSpecial` is still determined from the target on Unix).
   ##
-  ## | 0 iff pathA == pathB
-  ## | < 0 iff pathA < pathB
-  ## | > 0 iff pathA > pathB
-  if FileSystemCaseSensitive:
-    result = cmp(pathA, pathB)
-  else:
-    result = cmpIgnoreCase(pathA, pathB)
-
-proc sameFile*(path1, path2: string): bool =
-  ## Returns True if both pathname arguments refer to the same file or
-  ## directory (as indicated by device number and i-node number).
-  ## Raises an exception if an stat() call on either pathname fails.
-  when defined(Windows):
+  ## If the information cannot be retrieved, such as when the path doesn't
+  ## exist, or when permission restrictions prevent the program from retrieving
+  ## file information, `OSError` is raised.
+  ##
+  ## See also:
+  ## * `getFileInfo(handle) proc`_
+  ## * `getFileInfo(file) proc`_
+  when defined(windows):
     var
-      a, b: TWin32FindData
-    var resA = findfirstFileA(path1, a)
-    var resB = findfirstFileA(path2, b)
-    if resA != -1 and resB != -1:
-      result = $a.cFileName == $b.cFileName
-    else:
-      # work around some ``findfirstFileA`` bugs
-      result = cmpPaths(path1, path2) == 0
-    if resA != -1: findclose(resA)
-    if resB != -1: findclose(resB)
+      handle = openHandle(path, followSymlink)
+      rawInfo: BY_HANDLE_FILE_INFORMATION
+    if handle == INVALID_HANDLE_VALUE:
+      raiseOSError(osLastError(), path)
+    if getFileInformationByHandle(handle, addr rawInfo) == 0:
+      raiseOSError(osLastError(), path)
+    rawToFormalFileInfo(rawInfo, path, result)
+    discard closeHandle(handle)
   else:
-    var
-      a, b: TStat
-    if stat(path1, a) < 0'i32 or stat(path2, b) < 0'i32:
-      result = cmpPaths(path1, path2) == 0 # be consistent with Windows
+    var rawInfo: Stat
+    if followSymlink:
+      if stat(path, rawInfo) < 0'i32:
+        raiseOSError(osLastError(), path)
     else:
-      result = a.st_dev == b.st_dev and a.st_ino == b.st_ino
+      if lstat(path, rawInfo) < 0'i32:
+        raiseOSError(osLastError(), path)
+    rawToFormalFileInfo(rawInfo, path, result)
 
-proc sameFileContent*(path1, path2: string): bool =
-  ## Returns True if both pathname arguments refer to files with identical
+proc sameFileContent*(path1, path2: string): bool {.rtl, extern: "nos$1",
+  tags: [ReadIOEffect], noWeirdTarget.} =
+  ## Returns true if both pathname arguments refer to files with identical
   ## binary content.
-  const
-    bufSize = 8192 # 8K buffer
+  ##
+  ## See also:
+  ## * `sameFile proc`_
   var
-    a, b: TFile
+    a, b: File
   if not open(a, path1): return false
   if not open(b, path2):
     close(a)
     return false
-  var bufA = alloc(bufsize)
-  var bufB = alloc(bufsize)
-  while True:
-    var readA = readBuffer(a, bufA, bufsize)
-    var readB = readBuffer(b, bufB, bufsize)
+  let bufSize = getFileInfo(a).blockSize
+  var bufA = alloc(bufSize)
+  var bufB = alloc(bufSize)
+  while true:
+    var readA = readBuffer(a, bufA, bufSize)
+    var readB = readBuffer(b, bufB, bufSize)
     if readA != readB:
       result = false
       break
@@ -620,602 +934,99 @@ proc sameFileContent*(path1, path2: string): bool =
   close(a)
   close(b)
 
-proc copyFile*(dest, source: string) {.deprecated.} =
-  ## Copies a file from `source` to `dest`. If this fails,
-  ## `EOS` is raised.
-  ## **Deprecated since version 0.8.8**: Use this proc with named arguments
-  ## only, because the order will change!
-  when defined(Windows):
-    if CopyFileA(source, dest, 0'i32) == 0'i32: OSError()
-  else:
-    # generic version of copyFile which works for any platform:
-    const bufSize = 8000 # better for memory manager
-    var d, s: TFile
-    if not open(s, source): OSError()
-    if not open(d, dest, fmWrite):
-      close(s)
-      OSError()
-    var buf = alloc(bufsize)
-    while True:
-      var bytesread = readBuffer(s, buf, bufsize)
-      if bytesread > 0:
-        var byteswritten = writeBuffer(d, buf, bytesread)
-        if bytesread != bytesWritten:
-          dealloc(buf)
-          close(s)
-          close(d)
-          OSError()
-      if bytesread != bufSize: break
-    dealloc(buf)
-    close(s)
-    close(d)
-
-proc moveFile*(dest, source: string) {.deprecated.} =
-  ## Moves a file from `source` to `dest`. If this fails, `EOS` is raised.
-  ## **Deprecated since version 0.8.8**: Use this proc with named arguments
-  ## only, because the order will change!
-  if crename(source, dest) != 0'i32: OSError()
-
-proc removeFile*(file: string) =
-  ## Removes the `file`. If this fails, `EOS` is raised.
-  if cremove(file) != 0'i32: OSError()
-
-proc executeShellCommand*(command: string): int {.deprecated.} =
-  ## **Deprecated since version 0.8.2**: Use `execShellCmd` instead.
-  result = csystem(command)
-
-proc execShellCmd*(command: string): int =
-  ## Executes a shell command.
+proc isHidden*(path: string): bool {.noWeirdTarget.} =
+  ## Determines whether ``path`` is hidden or not, using `this
+  ## reference <https://en.wikipedia.org/wiki/Hidden_file_and_hidden_directory>`_.
   ##
-  ## Command has the form 'program args' where args are the command
-  ## line arguments given to program. The proc returns the error code
-  ## of the shell when it has finished. The proc does not return until
-  ## the process has finished. To execute a program without having a
-  ## shell involved, use the `execProcess` proc of the `osproc`
-  ## module.
-  result = csystem(command)
-
-var
-  envComputed: bool = false
-  environment: seq[string] = @[]
-
-when defined(windows):
-  # because we support Windows GUI applications, things get really
-  # messy here...
-  proc strEnd(cstr: CString, c = 0'i32): CString {.
-    importc: "strchr", header: "<string.h>".}
-
-  proc getEnvVarsC() =
-    if not envComputed:
-      var
-        env = getEnvironmentStringsA()
-        e = env
-      if e == nil: return # an error occured
-      while True:
-        var eend = strEnd(e)
-        add(environment, $e)
-        e = cast[CString](cast[TAddress](eend)+1)
-        if eend[1] == '\0': break
-      envComputed = true
-      discard FreeEnvironmentStringsA(env)
-
-else:
-  var
-    gEnv {.importc: "gEnv".}: ptr array [0..10_000, CString]
-
-  proc getEnvVarsC() =
-    # retrieves the variables of char** env of C's main proc
-    if not envComputed:
-      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): string =
-  ## Returns the value of the environment variable named `key`.
+  ## On Windows: returns true if it exists and its "hidden" attribute is set.
   ##
-  ## 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 copy(environment[i], find(environment[i], '=')+1)
-  else:
-    var env = cgetenv(key)
-    if env == nil: return ""
-    result = $env
-
-proc existsEnv*(key: string): bool =
-  ## Checks whether the environment variable named `key` exists.
-  ## Returns true if it exists, false otherwise.
-  if cgetenv(key) != nil: return true
-  else: return findEnvVar(key) >= 0
-
-proc putEnv*(key, val: string) =
-  ## Sets the value of the environment variable named `key` to `val`.
-  ## If an error occurs, `EInvalidEnvVar` is raised.
-
-  # Note: by storing the string in the environment sequence,
-  # we gurantee 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(unix):
-    if cputenv(environment[indx]) != 0'i32:
-      OSError()
-  else:
-    if SetEnvironmentVariableA(key, val) == 0'i32:
-      OSError()
-
-iterator iterOverEnvironment*(): tuple[key, value: string] {.deprecated.} =
-  ## Iterate over all environments variables. In the first component of the
-  ## tuple is the name of the current variable stored, in the second its value.
-  ## **Deprecated since version 0.8.2**: Use `envPairs` instead.
-  getEnvVarsC()
-  for i in 0..high(environment):
-    var p = find(environment[i], '=')
-    yield (copy(environment[i], 0, p-1), copy(environment[i], p+1))
-
-iterator envPairs*(): tuple[key, value: string] =
-  ## Iterate over all environments variables. 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 (copy(environment[i], 0, p-1), copy(environment[i], p+1))
-
-iterator walkFiles*(pattern: string): string =
-  ## Iterate over all the files that match the `pattern`.
+  ## On posix: returns true if ``lastPathPart(path)`` starts with ``.`` and is
+  ## not ``.`` or ``..``.
   ##
-  ## `pattern` is OS dependant, but at least the "\*.ext"
-  ## notation is supported.
-  when defined(windows):
-    var
-      f: TWin32FindData
-      res: int
-    res = findfirstFileA(pattern, f)
-    if res != -1:
-      while true:
-        if f.cFileName[0] != '.':
-          yield splitFile(pattern).dir / extractFilename($f.cFileName)
-        if findnextFileA(res, f) == 0'i32: break
-      findclose(res)
-  else: # here we use glob
-    var
-      f: TGlob
-      res: int
-    f.gl_offs = 0
-    f.gl_pathc = 0
-    f.gl_pathv = nil
-    res = glob(pattern, 0, nil, addr(f))
-    if res == 0:
-      for i in 0.. f.gl_pathc - 1:
-        assert(f.gl_pathv[i] != nil)
-        yield $f.gl_pathv[i]
-    globfree(addr(f))
-
-type
-  TPathComponent* = enum  ## Enumeration specifying a path component.
-    pcFile,               ## path refers to a file
-    pcLinkToFile,         ## path refers to a symbolic link to a file
-    pcDir,                ## path refers to a directory
-    pcLinkToDir           ## path refers to a symbolic link to a directory
+  ## **Note**: paths are not normalized to determine `isHidden`.
+  runnableExamples:
+    when defined(posix):
+      assert ".foo".isHidden
+      assert not ".foo/bar".isHidden
+      assert not ".".isHidden
+      assert not "..".isHidden
+      assert not "".isHidden
+      assert ".foo/".isHidden
 
-const
-  pcDirectory* {.deprecated.} = pcDir ## deprecated alias 
-  pcLinkToDirectory* {.deprecated.} = pcLinkToDir ## deprecated alias
-
-iterator walkDir*(dir: string): tuple[kind: TPathComponent, path: string] =
-  ## walks over the directory `dir` and yields for each directory or file in
-  ## `dir`. The component type and full path for each item is returned.
-  ## Walking is not recursive.
-  ## Example: This directory structure::
-  ##   dirA / dirB / fileB1.txt
-  ##        / dirC
-  ##        / fileA1.txt
-  ##        / fileA2.txt
-  ##
-  ## and this code:
-  ##
-  ## .. code-block:: Nimrod
-  ##     for kind, path in walkDir("dirA"):
-  ##       echo(path)
-  ##
-  ## produces this output (but not necessarily in this order!)::
-  ##   dirA/dirB
-  ##   dirA/dirC
-  ##   dirA/fileA1.txt
-  ##   dirA/fileA2.txt
   when defined(windows):
-    var f: TWIN32_Find_Data
-    var h = findfirstFileA(dir / "*", f)
-    if h != -1:
-      while true:
-        var k = pcFile
-        if f.cFilename[0] != '.':
-          if (f.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0'i32:
-            k = pcDir
-          yield (k, dir / extractFilename($f.cFilename))
-        if findnextFileA(h, f) == 0'i32: break
-      findclose(h)
+    wrapUnary(attributes, getFileAttributesW, path)
+    if attributes != -1'i32:
+      result = (attributes and FILE_ATTRIBUTE_HIDDEN) != 0'i32
   else:
-    var d = openDir(dir)
-    if d != nil:
-      while true:
-        var x = readDir(d)
-        if x == nil: break
-        var y = $x.d_name
-        if y != "." and y != "..":
-          var s: TStat
-          y = dir / y
-          if stat(y, s) < 0'i32: break
-          var k = pcFile
-          if S_ISDIR(s.st_mode): k = pcDir
-          if S_ISLNK(s.st_mode): k = succ(k)
-          yield (k, y)
-      discard closeDir(d)
-
-iterator walkDirRec*(dir: string, filter={pcFile, pcDir}): string =
-  ## walks over the directory `dir` and yields for each file in `dir`. The 
-  ## full path for each file is returned.
-  ## Walking is recursive. `filter` controls the behaviour of the iterator:
+    let fileName = lastPathPart(path)
+    result = len(fileName) >= 2 and fileName[0] == '.' and fileName != ".."
+
+proc getCurrentProcessId*(): int {.noWeirdTarget.} =
+  ## Return current process ID.
   ##
-  ## ---------------------   ---------------------------------------------
-  ## filter                  meaning
-  ## ---------------------   ---------------------------------------------
-  ## ``pcFile``              yield real files
-  ## ``pcLinkToFile``        yield symbolic links to files
-  ## ``pcDir``               follow real directories
-  ## ``pcLinkToDir``         follow symbolic links to directories
-  ## ---------------------   ---------------------------------------------
-  ## 
-  var stack = @[dir]
-  while stack.len > 0:
-    for k,p in walkDir(stack.pop()):
-      if k in filter:
-        case k
-        of pcFile, pcLinkToFile: yield p
-        of pcDir, pcLinkToDir: stack.add(p)
-
-proc rawRemoveDir(dir: string) = 
+  ## See also:
+  ## * `osproc.processID(p: Process) <osproc.html#processID,Process>`_
   when defined(windows):
-    if RemoveDirectoryA(dir) == 0'i32: OSError()
+    proc GetCurrentProcessId(): DWORD {.stdcall, dynlib: "kernel32",
+                                        importc: "GetCurrentProcessId".}
+    result = GetCurrentProcessId().int
   else:
-    if rmdir(dir) != 0'i32: OSError()
-
-proc removeDir*(dir: string) =
-  ## Removes the directory `dir` including all subdirectories and files
-  ## in `dir` (recursively). If this fails, `EOS` is raised.
-  for kind, path in walkDir(dir): 
-    case kind
-    of pcFile, pcLinkToFile, pcLinkToDir: removeFile(path)
-    of pcDir: removeDir(path)
-  rawRemoveDir(dir)
-
-proc rawCreateDir(dir: string) =
-  when defined(unix):
-    if mkdir(dir, 0o711) != 0'i32 and errno != EEXIST:
-      OSError()
+    result = getpid()
+
+proc setLastModificationTime*(file: string, t: times.Time) {.noWeirdTarget.} =
+  ## Sets the `file`'s last modification time. `OSError` is raised in case of
+  ## an error.
+  when defined(posix):
+    let unixt = posix.Time(t.toUnix)
+    let micro = convert(Nanoseconds, Microseconds, t.nanosecond)
+    var timevals = [Timeval(tv_sec: unixt, tv_usec: micro),
+      Timeval(tv_sec: unixt, tv_usec: micro)] # [last access, last modification]
+    if utimes(file, timevals.addr) != 0: raiseOSError(osLastError(), file)
   else:
-    if CreateDirectoryA(dir, nil) == 0'i32 and GetLastError() != 183'i32:
-      OSError()
+    let h = openHandle(path = file, writeAccess = true)
+    if h == INVALID_HANDLE_VALUE: raiseOSError(osLastError(), file)
+    var ft = t.toWinTime.toFILETIME
+    let res = setFileTime(h, nil, nil, ft.addr)
+    discard h.closeHandle
+    if res == 0'i32: raiseOSError(osLastError(), file)
 
-proc createDir*(dir: string) =
-  ## Creates the directory `dir`.
-  ##
-  ## The directory may contain several subdirectories that do not exist yet.
-  ## The full path is created. If this fails, `EOS` is raised. It does **not**
-  ## fail if the path already exists because for most usages this does not 
-  ## indicate an error.
-  for i in 1.. dir.len-1:
-    if dir[i] in {dirsep, altsep}: rawCreateDir(copy(dir, 0, i-1))
-  rawCreateDir(dir)
-
-proc parseCmdLine*(c: string): seq[string] =
-  ## Splits a command line into several components;  
-  ## This proc is only occassionally useful, better use the `parseopt` module.
-  ##
-  ## On Windows, it uses the following parsing rules
-  ## (see http://msdn.microsoft.com/en-us/library/17w5ykft.aspx):
+
+func isValidFilename*(filename: string, maxLen = 259.Positive): bool {.since: (1, 1).} =
+  ## Returns `true` if `filename` is valid for crossplatform use.
   ##
-  ## * Arguments are delimited by white space, which is either a space or a tab.
-  ## * The caret character (^) is not recognized as an escape character or
-  ##   delimiter. The character is handled completely by the command-line parser
-  ##   in the operating system before being passed to the argv array in the
-  ##   program.
-  ## * A string surrounded by double quotation marks ("string") is interpreted
-  ##   as a single argument, regardless of white space contained within. A
-  ##   quoted string can be embedded in an argument.
-  ## * A double quotation mark preceded by a backslash (\") is interpreted as a
-  ##   literal double quotation mark character (").
-  ## * Backslashes are interpreted literally, unless they immediately precede
-  ##   a double quotation mark.
-  ## * If an even number of backslashes is followed by a double quotation mark,
-  ##   one backslash is placed in the argv array for every pair of backslashes,
-  ##   and the double quotation mark is interpreted as a string delimiter.
-  ## * If an odd number of backslashes is followed by a double quotation mark,
-  ##   one backslash is placed in the argv array for every pair of backslashes,
-  ##   and the double quotation mark is "escaped" by the remaining backslash,
-  ##   causing a literal double quotation mark (") to be placed in argv.
+  ## This is useful if you want to copy or save files across Windows, Linux, Mac, etc.
+  ## It uses `invalidFilenameChars`, `invalidFilenames` and `maxLen` to verify the specified `filename`.
   ##
-  ## On Posix systems, it uses the following parsing rules:
-  ## components are separated by
-  ## whitespace unless the whitespace occurs within ``"`` or ``'`` quotes.
-  result = @[]
-  var i = 0
-  var a = ""
-  while true:
-    setLen(a, 0)
-    while c[i] == ' ' or c[i] == '\t': inc(i)
-    when defined(windows):
-      # parse a single argument according to the above rules:
-      var inQuote = false
-      while true:
-        case c[i]        
-        of '\0': break
-        of '\\':
-          var j = i
-          while c[j] == '\\': inc(j)
-          if c[j] == '"': 
-            for k in 0..(j-i) div 2: a.add('\\')
-            if (j-i) mod 2 == 0: 
-              i = j
-            else: 
-              a.add('"')
-              i = j+1
-          else: 
-            a.add(c[i])
-            inc(i)
-        of '"':
-          inc(i)
-          if not inQuote: inQuote = true
-          elif c[i] == '"': 
-            a.add(c[i])
-            inc(i)
-          else:
-            inQuote = false
-            break
-        of ' ', '\t': 
-          if not inQuote: break
-          a.add(c[i])
-          inc(i)
-        else:
-          a.add(c[i])
-          inc(i)
-    else:
-      case c[i]
-      of '\'', '\"':
-        var delim = c[i]
-        inc(i) # skip ' or "
-        while c[i] != '\0' and c[i] != delim:
-          add a, c[i]
-          inc(i)
-        if c[i] != '\0': inc(i)
-      of '\0': break
-      else:
-        while c[i] > ' ':
-          add(a, c[i])
-          inc(i)
-    add(result, a)
-
-type
-  TFilePermission* = enum  ## file access permission; modelled after UNIX
-    fpUserExec,            ## execute access for the file owner
-    fpUserWrite,           ## write access for the file owner
-    fpUserRead,            ## read access for the file owner
-    fpGroupExec,           ## execute access for the group
-    fpGroupWrite,          ## write access for the group
-    fpGroupRead,           ## read access for the group
-    fpOthersExec,          ## execute access for others
-    fpOthersWrite,         ## write access for others
-    fpOthersRead           ## read access for others
-
-proc getFilePermissions*(filename: string): set[TFilePermission] =
-  ## retrieves file permissions for `filename`. `OSError` is raised in case of
-  ## an error. On Windows, only the ``readonly`` flag is checked, every other
-  ## permission is available in any case.
-  when defined(posix):
-    var a: TStat
-    if stat(filename, a) < 0'i32: OSError()
-    result = {}
-    if (a.st_mode and S_IRUSR) != 0'i32: result.incl(fpUserRead)
-    if (a.st_mode and S_IWUSR) != 0'i32: result.incl(fpUserWrite)
-    if (a.st_mode and S_IXUSR) != 0'i32: result.incl(fpUserExec)
-
-    if (a.st_mode and S_IRGRP) != 0'i32: result.incl(fpGroupRead)
-    if (a.st_mode and S_IWGRP) != 0'i32: result.incl(fpGroupWrite)
-    if (a.st_mode and S_IXGRP) != 0'i32: result.incl(fpGroupExec)
-
-    if (a.st_mode and S_IROTH) != 0'i32: result.incl(fpOthersRead)
-    if (a.st_mode and S_IWOTH) != 0'i32: result.incl(fpOthersWrite)
-    if (a.st_mode and S_IXOTH) != 0'i32: result.incl(fpOthersExec)
-  else:
-    var res = GetFileAttributesA(filename)
-    if res == -1'i32: OSError()
-    if (res and FILE_ATTRIBUTE_READONLY) != 0'i32:
-      result = {fpUserExec, fpUserRead, fpGroupExec, fpGroupRead, 
-                fpOthersExec, fpOthersRead}
-    else:
-      result = {fpUserExec..fpOthersRead}
-  
-proc setFilePermissions*(filename: string, permissions: set[TFilePermission]) =
-  ## sets the file permissions for `filename`. `OSError` is raised in case of
-  ## an error. On Windows, only the ``readonly`` flag is changed, depending on
-  ## ``fpUserWrite``.
-  when defined(posix):
-    var p = 0'i32
-    if fpUserRead in permissions: p = p or S_IRUSR
-    if fpUserWrite in permissions: p = p or S_IWUSR
-    if fpUserExec in permissions: p = p or S_IXUSR
-    
-    if fpGroupRead in permissions: p = p or S_IRGRP
-    if fpGroupWrite in permissions: p = p or S_IWGRP
-    if fpGroupExec in permissions: p = p or S_IXGRP
-    
-    if fpOthersRead in permissions: p = p or S_IROTH
-    if fpOthersWrite in permissions: p = p or S_IWOTH
-    if fpOthersExec in permissions: p = p or S_IXOTH
-    
-    if chmod(filename, p) != 0: OSError()
-  else:
-    var res = GetFileAttributesA(filename)
-    if res == -1'i32: OSError()
-    if fpUserWrite in permissions: 
-      res = res and not FILE_ATTRIBUTE_READONLY
-    else:
-      res = res or FILE_ATTRIBUTE_READONLY
-    if SetFileAttributesA(filename, res) == - 1'i32: 
-      OSError()
-  
-proc inclFilePermissions*(filename: string, 
-                          permissions: set[TFilePermission]) =
-  ## a convenience procedure for: 
+  ## See also:
   ##
-  ## .. code-block:: nimrod
-  ##   setFilePermissions(filename, getFilePermissions(filename)+permissions)
-  setFilePermissions(filename, getFilePermissions(filename)+permissions)
-
-proc exclFilePermissions*(filename: string, 
-                          permissions: set[TFilePermission]) =
-  ## a convenience procedure for: 
+  ## * https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception
+  ## * https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
+  ## * https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
   ##
-  ## .. code-block:: nimrod
-  ##   setFilePermissions(filename, getFilePermissions(filename)-permissions)
-  setFilePermissions(filename, getFilePermissions(filename)-permissions)
-
-proc getHomeDir*(): string =
-  ## Returns the home directory of the current user.
-  when defined(windows): return getEnv("USERPROFILE") & "\\"
-  else: return getEnv("HOME") & "/"
-
-proc getConfigDir*(): string =
-  ## Returns the config directory of the current user for applications.
-  when defined(windows): return getEnv("APPDATA") & "\\"
-  else: return getEnv("HOME") & "/.config/"
-
-when defined(windows):
-  # Since we support GUI applications with Nimrod, we sometimes generate
-  # a WinMain entry proc. But a WinMain proc has no access to the parsed
-  # command line arguments. The way to get them differs. Thus we parse them
-  # ourselves. This has the additional benefit that the program's behaviour
-  # is always the same -- independent of the used C compiler.
-  var
-    ownArgv: seq[string]
-
-  proc paramStr(i: int): string =
-    if isNil(ownArgv): ownArgv = parseCmdLine($getCommandLineA())
-    return ownArgv[i]
-
-  proc paramCount(): int =
-    if isNil(ownArgv): ownArgv = parseCmdLine($getCommandLineA())
-    result = ownArgv.len-1
-
-else:
-  var
-    cmdCount {.importc: "cmdCount".}: cint
-    cmdLine {.importc: "cmdLine".}: cstringArray
-
-  proc paramStr(i: int): string =
-    if i < cmdCount and i >= 0: return $cmdLine[i]
-    raise newException(EInvalidIndex, "invalid index")
-
-  proc paramCount(): int = return cmdCount-1
-
-when defined(linux) or defined(solaris) or defined(bsd) or defined(aix):
-  proc getApplAux(procPath: string): string =
-    result = newString(256)
-    var len = readlink(procPath, result, 256)
-    if len > 256:
-      result = newString(len+1)
-      len = readlink(procPath, result, len)
-    setlen(result, len)
-
-when defined(macosx):
-  # a really hacky solution: since we like to include 2 headers we have to
-  # define two procs which in reality are the same
-  proc getExecPath1(c: cstring, size: var int32) {.
-    importc: "_NSGetExecutablePath", header: "<sys/param.h>".}
-  proc getExecPath2(c: cstring, size: var int32): bool {.
-    importc: "_NSGetExecutablePath", header: "<mach-o/dyld.h>".}
-
-proc getApplicationFilename*(): string =
-  ## Returns the filename of the application's executable.
-
-  # Linux: /proc/<pid>/exe
-  # Solaris:
-  # /proc/<pid>/object/a.out (filename only)
-  # /proc/<pid>/path/a.out (complete pathname)
-  # *BSD (and maybe Darwin too):
-  # /proc/<pid>/file
-  when defined(windows):
-    result = newString(256)
-    var len = getModuleFileNameA(0, result, 256)
-    setlen(result, int(len))
-  elif defined(linux) or defined(aix):
-    result = getApplAux("/proc/self/exe")
-  elif defined(solaris):
-    result = getApplAux("/proc/" & $getpid() & "/path/a.out")
-  elif defined(bsd):
-    result = getApplAux("/proc/" & $getpid() & "/file")
-  elif defined(macosx):
-    var size: int32
-    getExecPath1(nil, size)
-    result = newString(int(size))
-    if getExecPath2(result, size):
-      result = "" # error!
-  else:
-    # little heuristic that may work on other POSIX-like systems:
-    result = getEnv("_")
-    if len(result) == 0:
-      result = ParamStr(0) # POSIX guaranties that this contains the executable
-                           # as it has been executed by the calling process
-      if len(result) > 0 and result[0] != DirSep: # not an absolute path?
-        # iterate over any path in the $PATH environment variable
-        for p in split(getEnv("PATH"), {PathSep}):
-          var x = joinPath(p, result)
-          if ExistsFile(x): return x
-
-proc getApplicationDir*(): string =
-  ## Returns the directory of the application's executable.
-  result = splitFile(getApplicationFilename()).dir
-
-proc sleep*(milsecs: int) =
-  ## sleeps `milsecs` milliseconds.
-  when defined(windows):
-    winlean.sleep(int32(milsecs))
-  else:
-    var a, b: Ttimespec
-    a.tv_sec = TTime(milsecs div 1000)
-    a.tv_nsec = (milsecs mod 1000) * 1000
-    discard posix.nanosleep(a, b)
-
-proc getFileSize*(file: string): biggestInt =
-  ## returns the file size of `file`. Can raise ``EOS``. 
-  when defined(windows):
-    var a: TWin32FindData
-    var resA = findfirstFileA(file, a)
-    if resA == -1: OSError()
-    result = rdFileSize(a)
-    findclose(resA)
-  else:
-    var f: TFile
-    if open(f, file): 
-      result = getFileSize(f)
-      close(f)
-    else: OSError()
-
-{.pop.}
+  ## .. warning:: This only checks filenames, not whole paths
+  ##    (because basically you can mount anything as a path on Linux).
+  runnableExamples:
+    assert not isValidFilename(" foo")     # Leading white space
+    assert not isValidFilename("foo ")     # Trailing white space
+    assert not isValidFilename("foo.")     # Ends with dot
+    assert not isValidFilename("con.txt")  # "CON" is invalid (Windows)
+    assert not isValidFilename("OwO:UwU")  # ":" is invalid (Mac)
+    assert not isValidFilename("aux.bat")  # "AUX" is invalid (Windows)
+    assert not isValidFilename("")         # Empty string
+    assert not isValidFilename("foo/")     # Filename is empty
+
+  result = true
+  let f = filename.splitFile()
+  if unlikely(f.name.len + f.ext.len > maxLen or f.name.len == 0 or
+    f.name[0] == ' ' or f.name[^1] == ' ' or f.name[^1] == '.' or
+    find(f.name, invalidFilenameChars) != -1): return false
+  for invalid in invalidFilenames:
+    if cmpIgnoreCase(f.name, invalid) == 0: return false
+
+
+# deprecated declarations
+when not weirdTarget:
+  template existsFile*(args: varargs[untyped]): untyped {.deprecated: "use fileExists".} =
+    fileExists(args)
+  template existsDir*(args: varargs[untyped]): untyped {.deprecated: "use dirExists".} =
+    dirExists(args)