diff options
Diffstat (limited to 'lib/pure/os.nim')
-rw-r--r--[-rwxr-xr-x] | lib/pure/os.nim | 2274 |
1 files changed, 907 insertions, 1367 deletions
diff --git a/lib/pure/os.nim b/lib/pure/os.nim index 408fbd9d4..78ebb1c88 100755..100644 --- a/lib/pure/os.nim +++ b/lib/pure/os.nim @@ -1,1386 +1,604 @@ # # -# Nimrod's Runtime Library -# (c) Copyright 2012 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 -include "system/inclrtl" +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 + +import std/[strutils, pathnorm] -import - strutils, times +when defined(nimPreviewSlimSystem): + import std/[syncio, assertions, widestrs] -when defined(windows): - import winlean -elif defined(posix): - import posix +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.} -type - FReadEnv* = object of FReadIO ## effect that denotes a read - ## from an environment variable - FWriteEnv* = object of FWriteIO ## effect that denotes a write - ## to an environment variable - - FReadDir* = object of FReadIO ## effect that denotes a write operation to - ## the directory structure - FWriteDir* = object of FWriteIO ## effect that denotes a write operation to - ## the directory structure +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.} -const - doslike = defined(windows) or defined(OS2) or defined(DOS) - # DOS-like filesystem -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). - -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 doslike: - 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" - -when defined(macosx) or defined(bsd): - var - pathMax {.importc: "PATH_MAX", header: "<stdlib.h>".}: cint +import std/oserrors +export oserrors +import std/envvars +export envvars + +import std/private/osseps +export osseps -const - ExtSep* = '.' - ## The character which separates the base filename from the extension; - ## for example, the '.' in ``os.nim``. - -proc OSErrorMsg*(): string {.rtl, extern: "nos$1".} = - ## Retrieves the operating system's error flag, ``errno``. - ## On Windows ``GetLastError`` is checked before ``errno``. - ## Returns "" if no error occured. - result = "" - when defined(Windows): - var err = GetLastError() - if err != 0'i32: - when useWinUnicode: - var msgbuf: widecstring - if FormatMessageW(0x00000100 or 0x00001000 or 0x00000200, - nil, err, 0, addr(msgbuf), 0, nil) != 0'i32: - result = $msgbuf - if msgbuf != nil: LocalFree(msgbuf) - else: - var msgbuf: cstring - if FormatMessageA(0x00000100 or 0x00001000 or 0x00000200, - nil, err, 0, addr(msgbuf), 0, nil) != 0'i32: - result = $msgbuf - if msgbuf != nil: LocalFree(msgbuf) - if errno != 0'i32: - result = $os.strerror(errno) - -proc OSError*(msg: string = "") {.noinline, rtl, extern: "nos$1".} = - ## 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: - var m = OSErrorMsg() - raise newException(EOS, if m.len > 0: m else: "unknown OS error") - else: - raise newException(EOS, msg) -proc UnixToNativePath*(path: string): string {. - noSideEffect, rtl, extern: "nos$1".} = - ## Converts an UNIX-like path to a native one. + +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). + ## + ## Windows: this is still supported despite the Windows platform not having this + ## convention; also, both ``~/`` and ``~\`` are handled. ## - ## On an UNIX system this does nothing. Else it converts - ## '/', '.', '..' to the appropriate things. - when defined(unix): + ## 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: - 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 - 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) - -when defined(windows): - template wrapUnary(varname, winApiProc, arg: expr) {.immediate.} = - var tmp = allocWideCString(arg) - var varname = winApiProc(tmp) - dealloc tmp - - template wrapBinary(varname, winApiProc, arg, arg2: expr) {.immediate.} = - var tmp2 = allocWideCString(arg) - var varname = winApiProc(tmp2, arg2) - dealloc tmp2 - - when useWinUnicode: - proc FindFirstFile(a: string, b: var TWIN32_FIND_DATA): THandle = - var aa = allocWideCString(a) - result = FindFirstFileW(aa, b) - dealloc aa - template FindNextFile(a, b: expr): expr = FindNextFileW(a, b) - template getCommandLine(): expr = getCommandLineW() - - proc skipFindData(f: TWIN32_FIND_DATA): bool {.inline.} = - result = f.cFilename[0].int == ord('.') - - template getFilename(f: expr): expr = - $cast[WideCString](addr(f.cFilename[0])) - else: - template FindFirstFile(a, b: expr): expr = FindFirstFileA(a, b) - template FindNextFile(a, b: expr): expr = FindNextFileA(a, b) - template getCommandLine(): expr = getCommandLineA() - - proc skipFindData(f: TWIN32_FIND_DATA): bool {.inline.} = - result = f.cFilename[0] == '.' - - template getFilename(f: expr): expr = $f.cFilename - -proc existsFile*(filename: string): bool {.rtl, extern: "nos$1", - tags: [FReadDir].} = - ## Returns true if the file exists, false otherwise. - when defined(windows): - when useWinUnicode: - wrapUnary(a, GetFileAttributesW, filename) + # TODO: handle `~bob` and `~bob/` which means home of bob + result = path + +proc quoteShellWindows*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} = + ## Quote `s`, so it can be safely passed to Windows API. + ## + ## 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: - var a = GetFileAttributesA(filename) - if a != -1'i32: - result = (a and FILE_ATTRIBUTE_DIRECTORY) == 0'i32 + 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 res: TStat - return stat(filename, res) >= 0'i32 and S_ISREG(res.st_mode) + result = "'" & s.replace("'", "'\"'\"'") & "'" -proc existsDir*(dir: string): bool {.rtl, extern: "nos$1", tags: [FReadDir].} = - ## Returns true iff the directory `dir` exists. If `dir` is a file, false - ## is returned. - when defined(windows): - when useWinUnicode: - wrapUnary(a, GetFileAttributesW, dir) +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: - 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) + result = quoteShellPosix(s) -proc getLastModificationTime*(file: string): TTime {.rtl, extern: "nos$1".} = - ## Returns the `file`'s last modification time. - when defined(posix): - var res: TStat - if stat(file, res) < 0'i32: OSError() - return res.st_mtime - else: - var f: TWIN32_Find_Data - var h = findfirstFile(file, f) - if h == -1'i32: OSError() - result = winTimeToUnixTime(rdFileTime(f.ftLastWriteTime)) - findclose(h) + 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 getLastAccessTime*(file: string): TTime {.rtl, extern: "nos$1".} = - ## Returns the `file`'s last read or write access time. - when defined(posix): - var res: TStat - if stat(file, res) < 0'i32: OSError() - return res.st_atime - else: - var f: TWIN32_Find_Data - var h = findfirstFile(file, f) - if h == -1'i32: OSError() - result = winTimeToUnixTime(rdFileTime(f.ftLastAccessTime)) - findclose(h) + # 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]) -proc getCreationTime*(file: string): TTime {.rtl, extern: "nos$1".} = - ## Returns the `file`'s creation time. - when defined(posix): - var res: TStat - if stat(file, res) < 0'i32: OSError() - return res.st_ctime - else: - var f: TWIN32_Find_Data - var h = findfirstFile(file, f) - if h == -1'i32: OSError() - result = winTimeToUnixTime(rdFileTime(f.ftCreationTime)) - findclose(h) +when not weirdTarget: + proc c_system(cmd: cstring): cint {. + importc: "system", header: "<stdlib.h>".} -proc fileNewer*(a, b: string): bool {.rtl, extern: "nos$1".} = - ## 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 + when not defined(windows): + proc c_free(p: pointer) {. + importc: "free", header: "<stdlib.h>".} -proc getCurrentDir*(): string {.rtl, extern: "nos$1", tags: [].} = - ## Returns the `current working directory`:idx:. - const bufsize = 512 # should be enough - when defined(windows): - when useWinUnicode: - var res = cast[wideCString](alloc0(bufsize+1)) - var L = GetCurrentDirectoryW(bufsize, res) - result = res$L - dealloc res - if L == 0'i32: OSError() - else: - result = newString(bufsize) - var L = GetCurrentDirectoryA(bufsize, result) - if L == 0'i32: OSError() - setLen(result, L) - else: - result = newString(bufsize) - if getcwd(result, bufsize) != nil: - setlen(result, c_strlen(result)) - else: - OSError() - -proc setCurrentDir*(newDir: string) {.inline, tags: [].} = - ## Sets the `current working directory`:idx:; `EOS` is raised if - ## `newDir` cannot been set. - when defined(Windows): - when useWinUnicode: - var x = allocWideCString(newDir) - let res = SetCurrentDirectoryW(x) - dealloc x - if res == 0'i32: OSError() - else: - if SetCurrentDirectoryA(newDir) == 0'i32: OSError() - else: - if chdir(newDir) != 0'i32: OSError() -proc JoinPath*(head, tail: string): string {. - noSideEffect, rtl, extern: "nos$1".} = - ## Joins two directory names to one. - ## - ## For example on Unix: - ## - ## .. code-block:: nimrod - ## JoinPath("usr", "lib") - ## - ## results in: +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. ## - ## .. code-block:: nimrod - ## "usr/lib" + ## Returns `""` if the `exe` cannot be found. `exe` + ## is added the `ExeExts`_ file extensions if it has none. ## - ## 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 & substr(tail, 1) - else: - result = head & tail + ## 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: - if tail[0] in {DirSep, AltSep}: - result = head & tail + 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: - 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. - 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): tuple[head, tail: string] {. - noSideEffect, rtl, extern: "nos$1".} = - ## Splits a directory into (head, tail), so that - ## ``JoinPath(head, tail) == path``. - ## - ## Examples: + 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. ## - ## .. 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 = substr(path, 0, sepPos-1) - result.tail = substr(path, sepPos+1) + ## See also: + ## * `getLastAccessTime proc`_ + ## * `getCreationTime proc`_ + ## * `fileNewer proc`_ + when defined(posix): + var res: Stat + if stat(file, res) < 0'i32: raiseOSError(osLastError(), file) + result = res.st_mtim.toTime else: - result.head = "" - result.tail = path + 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 parentDir*(path: string): string {. - noSideEffect, rtl, extern: "nos$1".} = - ## Returns the parent directory of `path`. +proc getLastAccessTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} = + ## Returns the `file`'s last read or write access time. ## - ## 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 = substr(path, 0, sepPos-1) + ## See also: + ## * `getLastModificationTime proc`_ + ## * `getCreationTime proc`_ + ## * `fileNewer proc`_ + when defined(posix): + var res: Stat + if stat(file, res) < 0'i32: raiseOSError(osLastError(), file) + result = res.st_atim.toTime else: - result = path + 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 isRootDir*(path: string): bool {. - noSideEffect, rtl, extern: "nos$1".} = - ## Checks whether a given `path` is a root directory - var p = parentDir(path) - result = p == path or p.len == 0 - -iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string = - ## Walks over all parent directories of a given `path` +proc getCreationTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} = + ## Returns the `file`'s creation time. ## - ## 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. + ## **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. ## - ## 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 + ## See also: + ## * `getLastModificationTime proc`_ + ## * `getLastAccessTime proc`_ + ## * `fileNewer proc`_ + when defined(posix): + var res: Stat + if stat(file, res) < 0'i32: raiseOSError(osLastError(), file) + result = res.st_ctim.toTime 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`` - return parentDir(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(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 - -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:: nimrod - ## var (dir, name, ext) = splitFile("usr/local/nimrodc.html") - ## assert dir == "usr/local" - ## assert name == "nimrodc" - ## assert ext == ".html" + 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 {.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. ## - ## 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 = "" + ## 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 = splitPath(path).tail + result = getLastModificationTime(a) > getLastModificationTime(b) -proc expandFilename*(filename: string): string {.rtl, extern: "nos$1", - tags: [FReadDir].} = - ## Returns the full path of `filename`, raises EOS in case of an error. + +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): - const bufsize = 3072'i32 - when useWinUnicode: - var unused: widecstring - var res = cast[widecstring](alloc(bufsize*2+2)) - var f = allocWideCString(filename) - var L = GetFullPathNameW(f, bufsize, res, unused) - dealloc f - if L <= 0'i32 or L >= bufsize: - dealloc res - OSError() - result = res$L - dealloc res - else: - var unused: cstring - result = newString(bufsize) - var L = GetFullPathNameA(filename, bufsize, result, unused) - if L <= 0'i32 or L >= bufsize: OSError() - setLen(result, L) - elif defined(macosx) or defined(bsd): - # On Mac OS X 10.5, realpath does not allocate the buffer on its own - var pathBuffer: cstring = newString(pathMax) - var resultBuffer = realpath(filename, pathBuffer) - if resultBuffer == nil: OSError() - result = $resultBuffer - else: - var res = realpath(filename, nil) - if res == nil: OSError() - result = $res - c_free(res) - -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: - result = cmpIgnoreCase(pathA, pathB) + # 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") -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 doslike: - var len = len(path) - result = (len > 1 and path[0] in {'/', '\\'}) or - (len > 2 and path[0] in Letters 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 sameFile*(path1, path2: string): bool {.rtl, extern: "nos$1", - tags: [FReadDir].} = - ## Returns True if both pathname arguments refer to the same physical - ## file or directory. Raises an exception if any of the files does not - ## exist or information about it can not be obtained. - ## - ## This proc will return true if given two alternative hard-linked or - ## sym-linked paths to the same file or directory. - when defined(Windows): - var success = true - - when useWinUnicode: - var p1 = allocWideCString(path1) - var p2 = allocWideCString(path2) - template OpenHandle(path: expr): expr = - CreateFileW(path, 0'i32, FILE_SHARE_DELETE or FILE_SHARE_READ or - FILE_SHARE_WRITE, nil, OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL, 0) - - var f1 = OpenHandle(p1) - var f2 = OpenHandle(p2) - - else: - template OpenHandle(path: expr): expr = - CreateFileA(path, 0'i32, FILE_SHARE_DELETE or FILE_SHARE_READ or - FILE_SHARE_WRITE, nil, OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL, 0) - - var f1 = OpenHandle(path1) - var f2 = OpenHandle(path2) - - if f1 != INVALID_HANDLE_VALUE and f2 != INVALID_HANDLE_VALUE: - var fi1, fi2: TBY_HANDLE_FILE_INFORMATION - - if GetFileInformationByHandle(f1, addr(fi1)) != 0 and - GetFileInformationByHandle(f2, addr(fi2)) != 0: - result = fi1.dwVolumeSerialNumber == fi2.dwVolumeSerialNumber and - fi1.nFileIndexHigh == fi2.nFileIndexHigh and - fi1.nFileIndexLow == fi2.nFileIndexLow - else: success = false - else: success = false - - discard CloseHandle(f1) - discard CloseHandle(f2) - when useWinUnicode: - dealloc p1 - dealloc p2 - - if not success: OSError() else: - var a, b: TStat - if stat(path1, a) < 0'i32 or stat(path2, b) < 0'i32: - OSError() - else: - result = a.st_dev == b.st_dev and a.st_ino == b.st_ino + result = geteuid() == 0 -proc sameFileContent*(path1, path2: string): bool {.rtl, extern: "nos$1", - tags: [FReadIO].} = - ## Returns True if both pathname arguments refer to files with identical - ## binary content. - const - bufSize = 8192 # 8K buffer - var - a, b: TFile - 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) - if readA != readB: - result = false - break - if readA == 0: - result = true - break - result = equalMem(bufA, bufB, readA) - if not result: break - if readA != bufSize: break # end of file - dealloc(bufA) - dealloc(bufB) - close(a) - close(b) -proc copyFile*(source, dest: string) {.rtl, extern: "nos$1", - tags: [FReadIO, FWriteIO].} = - ## Copies a file from `source` to `dest`. If this fails, - ## `EOS` is raised. - when defined(Windows): - when useWinUnicode: - var s = allocWideCString(source) - var d = allocWideCString(dest) - let res = CopyFileW(s, d, 0'i32) - dealloc s - dealloc d - if res == 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: - if CopyFileA(source, dest, 0'i32) == 0'i32: OSError() + WEXITSTATUS(status) 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*(source, dest: string) {.rtl, extern: "nos$1", - tags: [FReadIO, FWriteIO].} = - ## Moves a file from `source` to `dest`. If this fails, `EOS` is raised. - if crename(source, dest) != 0'i32: OSError() - -when not defined(ENOENT): - var ENOENT {.importc, header: "<errno.h>".}: cint - -proc removeFile*(file: string) {.rtl, extern: "nos$1", tags: [FWriteDir].} = - ## Removes the `file`. If this fails, `EOS` is raised. This does not fail - ## if the file never existed in the first place. - if cremove(file) != 0'i32 and errno != ENOENT: OSError() - -proc execShellCmd*(command: string): int {.rtl, extern: "nos$1", - tags: [FExecIO].} = + status + +proc execShellCmd*(command: string): int {.rtl, extern: "nos$1", + tags: [ExecIOEffect], noWeirdTarget.} = ## Executes a `shell command`:idx:. ## ## 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) - -# 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: - 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 occured - while True: - var eend = strEnd(e) - add(environment, $e) - e = cast[wideCString](cast[TAddress](eend)+2) - if eend[1].int == 0: break - discard FreeEnvironmentStringsW(env) - else: - 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 - discard FreeEnvironmentStringsA(env) - envComputed = true - -else: - const - useNSGetEnviron = defined(macosx) and - (defined(createNimRtl) or defined(useNimRtl)) - 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: [FReadEnv].} = - ## Returns the value of the `environment variable`:idx: named `key`. + ## of the shell when it has finished (zero if there is no error). + ## The proc does not return until the process has finished. ## - ## 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 = cgetenv(key) - if env == nil: return TaintedString("") - result = TaintedString($env) - -proc existsEnv*(key: string): bool {.tags: [FReadEnv].} = - ## 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) {.tags: [FWriteEnv].} = - ## 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 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: - when useWinUnicode: - var k = allocWideCString(key) - var v = allocWideCString(val) - let res = SetEnvironmentVariableW(k, v) - dealloc k - dealloc v - if res == 0'i32: OSError() - else: - if SetEnvironmentVariableA(key, val) == 0'i32: OSError() - -iterator envPairs*(): tuple[key, value: TaintedString] {.tags: [FReadEnv].} = - ## 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))) - -iterator walkFiles*(pattern: string): string {.tags: [FReadDir].} = - ## Iterate over all the files that match the `pattern`. On POSIX this uses - ## the `glob`:idx: call. + ## To execute a program without having a shell involved, use `osproc.execProcess proc + ## <osproc.html#execProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_. ## - ## `pattern` is OS dependent, but at least the "\*.ext" - ## notation is supported. - when defined(windows): - var - f: TWin32FindData - res: int - res = findfirstFile(pattern, f) - if res != -1: - while true: - if not skipFindData(f): - yield splitFile(pattern).dir / extractFilename(getFilename(f)) - if findnextFile(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)) + ## **Examples:** + ## ```Nim + ## discard execShellCmd("ls -la") + ## ``` + result = exitStatusLikeShell(c_system(command)) -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 - -iterator walkDir*(dir: string): tuple[kind: TPathComponent, path: string] {. - tags: [FReadDir].} = - ## 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 = findfirstFile(dir / "*", f) - if h != -1: - while true: - var k = pcFile - if not skipFindData(f): - if (f.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0'i32: - k = pcDir - yield (k, dir / extractFilename(getFilename(f))) - if findnextFile(h, f) == 0'i32: break - findclose(h) - 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 lstat(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 {. - tags: [FReadDir].} = - ## 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: +proc expandFilename*(filename: string): string {.rtl, extern: "nos$1", + tags: [ReadDirEffect], noWeirdTarget.} = + ## Returns the full (`absolute`:idx:) path of an existing file `filename`. ## - ## --------------------- --------------------------------------------- - ## 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) = + ## Raises `OSError` in case of an error. Follows symlinks. when defined(windows): - when useWinUnicode: - wrapUnary(res, RemoveDirectoryW, dir) - else: - var res = RemoveDirectoryA(dir) - if res == 0'i32 and GetLastError() != 3'i32 and - GetLastError() != 18'i32: OSError() - else: - if rmdir(dir) != 0'i32 and errno != ENOENT: OSError() - -proc removeDir*(dir: string) {.rtl, extern: "nos$1", tags: [ - FWriteDir, FReadDir].} = - ## Removes the directory `dir` including all subdirectories and files - ## in `dir` (recursively). If this fails, `EOS` is raised. This does not fail - ## if the directory never existed in the first place. - 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() + 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: - when useWinUnicode: - wrapUnary(res, CreateDirectoryW, dir) + # 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: - var res = CreateDirectoryA(dir) - if res == 0'i32 and GetLastError() != 183'i32: - OSError() + result = $r + c_free(cast[pointer](r)) -proc createDir*(dir: string) {.rtl, extern: "nos$1", tags: [FWriteDir].} = - ## Creates the `directory`:idx: `dir`. +proc getCurrentCompilerExe*(): string {.compileTime.} = discard + ## Returns the path of the currently running Nim compiler or nimble executable. ## - ## 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. - var omitNext = false - when defined(doslike): - omitNext = isAbsolute(dir) - for i in 1.. dir.len-1: - if dir[i] in {dirsep, altsep}: - if omitNext: - omitNext = false - else: - rawCreateDir(substr(dir, 0, i-1)) - rawCreateDir(dir) - -proc copyDir*(source, dest: string) {.rtl, extern: "nos$1", - tags: [FWriteIO, FReadIO].} = - ## Copies a directory from `source` to `dest`. If this fails, `EOS` is raised. - createDir(dest) - for kind, path in walkDir(source): - var noSource = path.substr(source.len()+1) - case kind - of pcFile: - copyFile(path, dest / noSource) - of pcDir: - copyDir(path, dest / noSource) - else: nil - -proc parseCmdLine*(c: string): seq[string] {. - noSideEffect, rtl, extern: "nos$1".} = - ## Splits a command line into several components; - ## This proc is only occassionally useful, better use the `parseopt` module. + ## 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`. ## - ## On Windows, it uses the following parsing rules - ## (see http://msdn.microsoft.com/en-us/library/17w5ykft.aspx ): + ## .. warning:: Some OS's restrict the creation of hard links to + ## root users (administrators). ## - ## * 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. - ## - ## 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: - if c[i] == '\0': break - 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 1..(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] {. - rtl, extern: "nos$1", tags: [FReadDir].} = - ## 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: - when useWinUnicode: - wrapUnary(res, GetFileAttributesW, filename) - 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]) {. - rtl, extern: "nos$1", tags: [FWriteDir].} = - ## 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() + ## 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: - when useWinUnicode: - wrapUnary(res, GetFileAttributesW, filename) - 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 - when useWinUnicode: - wrapBinary(res2, SetFileAttributesW, filename, res) - else: - var res2 = SetFileAttributesA(filename, res) - if res2 == - 1'i32: OSError() - -proc inclFilePermissions*(filename: string, - permissions: set[TFilePermission]) {. - rtl, extern: "nos$1", tags: [FReadDir, FWriteDir].} = - ## a convenience procedure for: - ## - ## .. code-block:: nimrod + 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 exclFilePermissions*(filename: string, - permissions: set[TFilePermission]) {. - rtl, extern: "nos$1", tags: [FReadDir, FWriteDir].} = - ## a convenience procedure for: - ## - ## .. code-block:: nimrod +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) -proc getHomeDir*(): string {.rtl, extern: "nos$1", tags: [FReadEnv].} = - ## Returns the home directory of the current user. - when defined(windows): return string(getEnv("USERPROFILE")) & "\\" - else: return string(getEnv("HOME")) & "/" - -proc getConfigDir*(): string {.rtl, extern: "nos$1", tags: [FReadEnv].} = - ## Returns the config directory of the current user for applications. - when defined(windows): return string(getEnv("APPDATA")) & "\\" - else: return string(getEnv("HOME")) & "/.config/" - -proc getTempDir*(): string {.rtl, extern: "nos$1", tags: [FReadEnv].} = - ## Returns the temporary directory of the current user for applications to - ## save temporary files in. - when defined(windows): return string(getEnv("TEMP")) & "\\" - else: return "/tmp/" - -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 paramCount*(): int {.rtl, extern: "nos$1", tags: [FReadIO].} = - ## Returns the number of `command line arguments`:idx: given to the - ## application. - if isNil(ownArgv): ownArgv = parseCmdLine($getCommandLine()) - result = ownArgv.len-1 - - proc paramStr*(i: int): TaintedString {.rtl, extern: "nos$1", - tags: [FReadIO].} = - ## Returns the `i`-th `command line argument`:idx: given to the - ## application. - ## - ## `i` should be in the range `1..paramCount()`, else - ## the `EOutOfIndex` exception is raised. - if isNil(ownArgv): ownArgv = parseCmdLine($getCommandLine()) - return TaintedString(ownArgv[i]) +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: + const KERN_PROC_PATHNAME = 9 -elif not defined(createNimRtl): - # On Posix, there is no portable way to get the command line from a DLL. - var - cmdCount {.importc: "cmdCount".}: cint - cmdLine {.importc: "cmdLine".}: cstringArray + proc getApplFreebsd(): string = + var pathLength = csize_t(0) + + 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] + + # first call to get the required length + var res = sysctl(addr req[0], 4, nil, addr pathLength, nil, 0) - proc paramStr*(i: int): TaintedString {.tags: [FReadIO].} = - if i < cmdCount and i >= 0: return TaintedString($cmdLine[i]) - raise newException(EInvalidIndex, "invalid index") + if res < 0: + return "" - proc paramCount*(): int {.tags: [FReadIO].} = return cmdCount-1 + result.setLen(pathLength) + res = sysctl(addr req[0], 4, addr result[0], addr pathLength, nil, 0) -when defined(linux) or defined(solaris) or defined(bsd) or defined(aix): + 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(256) - var len = readlink(procPath, result, 256) - if len > 256: + result = newString(maxSymlinkLen) + var len = readlink(procPath, result.cstring, maxSymlinkLen) + if len > maxSymlinkLen: result = newString(len+1) - len = readlink(procPath, result, len) - setlen(result, len) + 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 "" + + 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) {. @@ -1388,105 +606,427 @@ when defined(macosx): proc getExecPath2(c: cstring, size: var cuint32): bool {. importc: "_NSGetExecutablePath", header: "<mach-o/dyld.h>".} -proc getAppFilename*(): string {.rtl, extern: "nos$1", tags: [FReadIO].} = +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. - ## **Note**: This does not work reliably on BSD. + ## This proc will resolve symlinks. + ## + ## Returns empty string when name is unavailable + ## + ## 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) - # *BSD (and maybe Darwin too): - # /proc/<pid>/file when defined(windows): - when useWinUnicode: - var buf = cast[wideCString](alloc(256*2)) - var len = getModuleFileNameW(0, buf, 256) - result = buf$len - else: - 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(freebsd): - result = getApplAux("/proc/" & $getpid() & "/file") + 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 + var size = cuint32(0) getExecPath1(nil, size) result = newString(int(size)) - if getExecPath2(result, size): + if getExecPath2(result.cstring, size): result = "" # error! + if result.len > 0: + try: + result = result.expandFilename + except OSError: + result = "" else: - # little heuristic that may work on other POSIX-like systems: - result = string(getEnv("_")) - if len(result) == 0: - result = string(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(string(getEnv("PATH")), {PathSep}): - var x = joinPath(p, result) - if ExistsFile(x): return x - -proc getApplicationFilename*(): string {.rtl, extern: "nos$1", deprecated.} = - ## Returns the filename of the application's executable. - ## **Deprecated since version 0.8.12**: use ``getAppFilename`` - ## instead. - result = getAppFilename() + 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 = "" -proc getApplicationDir*(): string {.rtl, extern: "nos$1", deprecated.} = - ## Returns the directory of the application's executable. - ## **Deprecated since version 0.8.12**: use ``getAppDir`` - ## instead. - result = splitFile(getAppFilename()).dir + # 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: [FReadIO].} = +proc getAppDir*(): string {.rtl, extern: "nos$1", tags: [ReadIOEffect], noWeirdTarget.} = ## Returns the directory of the application's executable. - ## **Note**: This does not work reliably on BSD. + ## + ## See also: + ## * `getAppFilename proc`_ result = splitFile(getAppFilename()).dir -proc sleep*(milsecs: int) {.rtl, extern: "nos$1", tags: [FTime].} = - ## sleeps `milsecs` milliseconds. +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 a, b: Ttimespec - a.tv_sec = TTime(milsecs div 1000) + 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: [FReadIO].} = - ## returns the file size of `file`. Can raise ``EOS``. +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: TWin32FindData - var resA = findfirstFile(file, a) - if resA == -1: OSError() + var a: WIN32_FIND_DATA + var resA = findFirstFile(file, a) + if resA == -1: raiseOSError(osLastError(), file) result = rdFileSize(a) - findclose(resA) + findClose(resA) else: - var f: TFile - if open(f, file): - result = getFileSize(f) - close(f) - else: OSError() + var rawInfo: Stat + if stat(file, rawInfo) < 0'i32: + raiseOSError(osLastError(), file) + rawInfo.st_size -proc findExe*(exe: string): string {.tags: [FReadDir, FReadEnv].} = - ## 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. On DOS-like platforms, `exe` - ## is added an ``.exe`` file extension if it has no extension. - result = addFileExt(exe, os.exeExt) - if ExistsFile(result): return - var path = string(os.getEnv("PATH")) - for candidate in split(path, pathSep): - var x = candidate / result - if ExistsFile(x): return x - result = "" +when defined(windows) or weirdTarget: + type + DeviceId* = int32 + FileId* = int64 +else: + type + DeviceId* = Dev + FileId* = Ino + +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): + 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) -{.pop.} + else: + 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. + ## + ## 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: + 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. + ## + ## 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`. + ## + ## 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. + ## + ## 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). + ## + ## 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 + 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 rawInfo: Stat + if followSymlink: + if stat(path, rawInfo) < 0'i32: + raiseOSError(osLastError(), path) + else: + if lstat(path, rawInfo) < 0'i32: + raiseOSError(osLastError(), path) + rawToFormalFileInfo(rawInfo, path, result) + +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. + ## + ## See also: + ## * `sameFile proc`_ + var + a, b: File + if not open(a, path1): return false + if not open(b, path2): + close(a) + return false + 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 + if readA == 0: + result = true + break + result = equalMem(bufA, bufB, readA) + if not result: break + if readA != bufSize: break # end of file + dealloc(bufA) + dealloc(bufB) + close(a) + close(b) + +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>`_. + ## + ## On Windows: returns true if it exists and its "hidden" attribute is set. + ## + ## On posix: returns true if ``lastPathPart(path)`` starts with ``.`` and is + ## not ``.`` or ``..``. + ## + ## **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 + + when defined(windows): + wrapUnary(attributes, getFileAttributesW, path) + if attributes != -1'i32: + result = (attributes and FILE_ATTRIBUTE_HIDDEN) != 0'i32 + else: + let fileName = lastPathPart(path) + result = len(fileName) >= 2 and fileName[0] == '.' and fileName != ".." + +proc getCurrentProcessId*(): int {.noWeirdTarget.} = + ## Return current process ID. + ## + ## See also: + ## * `osproc.processID(p: Process) <osproc.html#processID,Process>`_ + when defined(windows): + proc GetCurrentProcessId(): DWORD {.stdcall, dynlib: "kernel32", + importc: "GetCurrentProcessId".} + result = GetCurrentProcessId().int + else: + 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: + 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) + + +func isValidFilename*(filename: string, maxLen = 259.Positive): bool {.since: (1, 1).} = + ## Returns `true` if `filename` is valid for crossplatform use. + ## + ## 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`. + ## + ## See also: + ## + ## * 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 + ## + ## .. 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) |