refactor os.nim and ospaths.nim
This commit is contained in:
parent
8ce4273864
commit
50666a1f8b
4 changed files with 852 additions and 847 deletions
159
lib/pure/includes/osenv.nim
Normal file
159
lib/pure/includes/osenv.nim
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
## Include file that implements 'getEnv' and friends. Do not import it!
|
||||
|
||||
when not declared(ospaths):
|
||||
{.error: "This is an include file for ospaths.nim!".}
|
||||
|
||||
proc c_getenv(env: cstring): cstring {.
|
||||
importc: "getenv", header: "<stdlib.h>".}
|
||||
proc c_putenv(env: cstring): cint {.
|
||||
importc: "putenv", header: "<stdlib.h>".}
|
||||
|
||||
# Environment handling cannot be put into RTL, because the ``envPairs``
|
||||
# iterator depends on ``environment``.
|
||||
|
||||
var
|
||||
envComputed {.threadvar.}: bool
|
||||
environment {.threadvar.}: seq[string]
|
||||
|
||||
when defined(windows):
|
||||
# because we support Windows GUI applications, things get really
|
||||
# messy here...
|
||||
when useWinUnicode:
|
||||
when defined(cpp):
|
||||
proc strEnd(cstr: WideCString, c = 0'i32): WideCString {.
|
||||
importcpp: "(NI16*)wcschr((const wchar_t *)#, #)", header: "<string.h>".}
|
||||
else:
|
||||
proc strEnd(cstr: WideCString, c = 0'i32): WideCString {.
|
||||
importc: "wcschr", header: "<string.h>".}
|
||||
else:
|
||||
proc strEnd(cstr: cstring, c = 0'i32): cstring {.
|
||||
importc: "strchr", header: "<string.h>".}
|
||||
|
||||
proc getEnvVarsC() =
|
||||
if not envComputed:
|
||||
environment = @[]
|
||||
when useWinUnicode:
|
||||
var
|
||||
env = getEnvironmentStringsW()
|
||||
e = env
|
||||
if e == nil: return # an error occurred
|
||||
while true:
|
||||
var eend = strEnd(e)
|
||||
add(environment, $e)
|
||||
e = cast[WideCString](cast[ByteAddress](eend)+2)
|
||||
if eend[1].int == 0: break
|
||||
discard freeEnvironmentStringsW(env)
|
||||
else:
|
||||
var
|
||||
env = getEnvironmentStringsA()
|
||||
e = env
|
||||
if e == nil: return # an error occurred
|
||||
while true:
|
||||
var eend = strEnd(e)
|
||||
add(environment, $e)
|
||||
e = cast[cstring](cast[ByteAddress](eend)+1)
|
||||
if eend[1] == '\0': break
|
||||
discard freeEnvironmentStringsA(env)
|
||||
envComputed = true
|
||||
|
||||
else:
|
||||
const
|
||||
useNSGetEnviron = defined(macosx) and not defined(ios)
|
||||
|
||||
when useNSGetEnviron:
|
||||
# From the manual:
|
||||
# Shared libraries and bundles don't have direct access to environ,
|
||||
# which is only available to the loader ld(1) when a complete program
|
||||
# is being linked.
|
||||
# The environment routines can still be used, but if direct access to
|
||||
# environ is needed, the _NSGetEnviron() routine, defined in
|
||||
# <crt_externs.h>, can be used to retrieve the address of environ
|
||||
# at runtime.
|
||||
proc NSGetEnviron(): ptr cstringArray {.
|
||||
importc: "_NSGetEnviron", header: "<crt_externs.h>".}
|
||||
else:
|
||||
var gEnv {.importc: "environ".}: cstringArray
|
||||
|
||||
proc getEnvVarsC() =
|
||||
# retrieves the variables of char** env of C's main proc
|
||||
if not envComputed:
|
||||
environment = @[]
|
||||
when useNSGetEnviron:
|
||||
var gEnv = NSGetEnviron()[]
|
||||
var i = 0
|
||||
while true:
|
||||
if gEnv[i] == nil: break
|
||||
add environment, $gEnv[i]
|
||||
inc(i)
|
||||
envComputed = true
|
||||
|
||||
proc findEnvVar(key: string): int =
|
||||
getEnvVarsC()
|
||||
var temp = key & '='
|
||||
for i in 0..high(environment):
|
||||
if startsWith(environment[i], temp): return i
|
||||
return -1
|
||||
|
||||
proc getEnv*(key: string): TaintedString {.tags: [ReadEnvEffect].} =
|
||||
## Returns the value of the `environment variable`:idx: named `key`.
|
||||
##
|
||||
## If the variable does not exist, "" is returned. To distinguish
|
||||
## whether a variable exists or it's value is just "", call
|
||||
## `existsEnv(key)`.
|
||||
when nimvm:
|
||||
discard "built into the compiler"
|
||||
else:
|
||||
var i = findEnvVar(key)
|
||||
if i >= 0:
|
||||
return TaintedString(substr(environment[i], find(environment[i], '=')+1))
|
||||
else:
|
||||
var env = c_getenv(key)
|
||||
if env == nil: return TaintedString("")
|
||||
result = TaintedString($env)
|
||||
|
||||
proc existsEnv*(key: string): bool {.tags: [ReadEnvEffect].} =
|
||||
## Checks whether the environment variable named `key` exists.
|
||||
## Returns true if it exists, false otherwise.
|
||||
when nimvm:
|
||||
discard "built into the compiler"
|
||||
else:
|
||||
if c_getenv(key) != nil: return true
|
||||
else: return findEnvVar(key) >= 0
|
||||
|
||||
proc putEnv*(key, val: string) {.tags: [WriteEnvEffect].} =
|
||||
## Sets the value of the `environment variable`:idx: named `key` to `val`.
|
||||
## If an error occurs, `EInvalidEnvVar` is raised.
|
||||
|
||||
# Note: by storing the string in the environment sequence,
|
||||
# we guarantee that we don't free the memory before the program
|
||||
# ends (this is needed for POSIX compliance). It is also needed so that
|
||||
# the process itself may access its modified environment variables!
|
||||
when nimvm:
|
||||
discard "built into the compiler"
|
||||
else:
|
||||
var indx = findEnvVar(key)
|
||||
if indx >= 0:
|
||||
environment[indx] = key & '=' & val
|
||||
else:
|
||||
add environment, (key & '=' & val)
|
||||
indx = high(environment)
|
||||
when defined(windows):
|
||||
when useWinUnicode:
|
||||
var k = newWideCString(key)
|
||||
var v = newWideCString(val)
|
||||
if setEnvironmentVariableW(k, v) == 0'i32: raiseOSError(osLastError())
|
||||
else:
|
||||
if setEnvironmentVariableA(key, val) == 0'i32: raiseOSError(osLastError())
|
||||
else:
|
||||
if c_putenv(environment[indx]) != 0'i32:
|
||||
raiseOSError(osLastError())
|
||||
|
||||
iterator envPairs*(): tuple[key, value: TaintedString] {.tags: [ReadEnvEffect].} =
|
||||
## Iterate over all `environments variables`:idx:. In the first component
|
||||
## of the tuple is the name of the current variable stored, in the second
|
||||
## its value.
|
||||
getEnvVarsC()
|
||||
for i in 0..high(environment):
|
||||
var p = find(environment[i], '=')
|
||||
yield (TaintedString(substr(environment[i], 0, p-1)),
|
||||
TaintedString(substr(environment[i], p+1)))
|
||||
132
lib/pure/includes/oserr.nim
Normal file
132
lib/pure/includes/oserr.nim
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
## Include file that implements 'osErrorMsg' and friends. Do not import it!
|
||||
|
||||
when not declared(ospaths):
|
||||
{.error: "This is an include file for ospaths.nim!".}
|
||||
|
||||
when not defined(nimscript):
|
||||
var errno {.importc, header: "<errno.h>".}: cint
|
||||
|
||||
proc c_strerror(errnum: cint): cstring {.
|
||||
importc: "strerror", header: "<string.h>".}
|
||||
|
||||
proc osErrorMsg*(): string {.rtl, extern: "nos$1", deprecated.} =
|
||||
## Retrieves the operating system's error flag, ``errno``.
|
||||
## On Windows ``GetLastError`` is checked before ``errno``.
|
||||
## Returns "" if no error occurred.
|
||||
##
|
||||
## **Deprecated since version 0.9.4**: use the other ``osErrorMsg`` proc.
|
||||
|
||||
result = ""
|
||||
when defined(Windows) and not defined(nimscript):
|
||||
var err = getLastError()
|
||||
if err != 0'i32:
|
||||
when useWinUnicode:
|
||||
var msgbuf: WideCString
|
||||
if formatMessageW(0x00000100 or 0x00001000 or 0x00000200 or 0x000000FF,
|
||||
nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
|
||||
result = $msgbuf
|
||||
if msgbuf != nil: localFree(cast[pointer](msgbuf))
|
||||
else:
|
||||
var msgbuf: cstring
|
||||
if formatMessageA(0x00000100 or 0x00001000 or 0x00000200 or 0x000000FF,
|
||||
nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
|
||||
result = $msgbuf
|
||||
if msgbuf != nil: localFree(msgbuf)
|
||||
when not defined(nimscript):
|
||||
if errno != 0'i32:
|
||||
result = $c_strerror(errno)
|
||||
|
||||
{.push warning[deprecated]: off.}
|
||||
proc raiseOSError*(msg: string = "") {.noinline, rtl, extern: "nos$1",
|
||||
deprecated.} =
|
||||
## raises an OSError exception with the given message ``msg``.
|
||||
## If ``msg == ""``, the operating system's error flag
|
||||
## (``errno``) is converted to a readable error message. On Windows
|
||||
## ``GetLastError`` is checked before ``errno``.
|
||||
## If no error flag is set, the message ``unknown OS error`` is used.
|
||||
##
|
||||
## **Deprecated since version 0.9.4**: use the other ``raiseOSError`` proc.
|
||||
if len(msg) == 0:
|
||||
var m = osErrorMsg()
|
||||
raise newException(OSError, if m.len > 0: m else: "unknown OS error")
|
||||
else:
|
||||
raise newException(OSError, msg)
|
||||
{.pop.}
|
||||
|
||||
when not defined(nimfix):
|
||||
{.deprecated: [osError: raiseOSError].}
|
||||
|
||||
proc `==`*(err1, err2: OSErrorCode): bool {.borrow.}
|
||||
proc `$`*(err: OSErrorCode): string {.borrow.}
|
||||
|
||||
proc osErrorMsg*(errorCode: OSErrorCode): string =
|
||||
## Converts an OS error code into a human readable string.
|
||||
##
|
||||
## The error code can be retrieved using the ``osLastError`` proc.
|
||||
##
|
||||
## If conversion fails, or ``errorCode`` is ``0`` then ``""`` will be
|
||||
## returned.
|
||||
##
|
||||
## On Windows, the ``-d:useWinAnsi`` compilation flag can be used to
|
||||
## make this procedure use the non-unicode Win API calls to retrieve the
|
||||
## message.
|
||||
result = ""
|
||||
when defined(nimscript):
|
||||
discard
|
||||
elif defined(Windows):
|
||||
if errorCode != OSErrorCode(0'i32):
|
||||
when useWinUnicode:
|
||||
var msgbuf: WideCString
|
||||
if formatMessageW(0x00000100 or 0x00001000 or 0x00000200,
|
||||
nil, errorCode.int32, 0, addr(msgbuf), 0, nil) != 0'i32:
|
||||
result = $msgbuf
|
||||
if msgbuf != nil: localFree(cast[pointer](msgbuf))
|
||||
else:
|
||||
var msgbuf: cstring
|
||||
if formatMessageA(0x00000100 or 0x00001000 or 0x00000200,
|
||||
nil, errorCode.int32, 0, addr(msgbuf), 0, nil) != 0'i32:
|
||||
result = $msgbuf
|
||||
if msgbuf != nil: localFree(msgbuf)
|
||||
else:
|
||||
if errorCode != OSErrorCode(0'i32):
|
||||
result = $c_strerror(errorCode.int32)
|
||||
|
||||
proc raiseOSError*(errorCode: OSErrorCode; additionalInfo = "") {.noinline.} =
|
||||
## Raises an ``OSError`` exception. The ``errorCode`` will determine the
|
||||
## message, ``osErrorMsg`` will be used to get this message.
|
||||
##
|
||||
## The error code can be retrieved using the ``osLastError`` proc.
|
||||
##
|
||||
## If the error code is ``0`` or an error message could not be retrieved,
|
||||
## the message ``unknown OS error`` will be used.
|
||||
var e: ref OSError; new(e)
|
||||
e.errorCode = errorCode.int32
|
||||
if additionalInfo.len == 0:
|
||||
e.msg = osErrorMsg(errorCode)
|
||||
else:
|
||||
e.msg = osErrorMsg(errorCode) & "\nAdditional info: " & additionalInfo
|
||||
if e.msg == "":
|
||||
e.msg = "unknown OS error"
|
||||
raise e
|
||||
|
||||
{.push stackTrace:off.}
|
||||
proc osLastError*(): OSErrorCode =
|
||||
## Retrieves the last operating system error code.
|
||||
##
|
||||
## This procedure is useful in the event when an OS call fails. In that case
|
||||
## this procedure will return the error code describing the reason why the
|
||||
## OS call failed. The ``OSErrorMsg`` procedure can then be used to convert
|
||||
## this code into a string.
|
||||
##
|
||||
## **Warning**:
|
||||
## The behaviour of this procedure varies between Windows and POSIX systems.
|
||||
## On Windows some OS calls can reset the error code to ``0`` causing this
|
||||
## procedure to return ``0``. It is therefore advised to call this procedure
|
||||
## immediately after an OS call fails. On POSIX systems this is not a problem.
|
||||
when defined(nimscript):
|
||||
discard
|
||||
elif defined(windows):
|
||||
result = OSErrorCode(getLastError())
|
||||
else:
|
||||
result = OSErrorCode(errno)
|
||||
{.pop.}
|
||||
320
lib/pure/os.nim
320
lib/pure/os.nim
|
|
@ -35,136 +35,11 @@ proc c_rename(oldname, newname: cstring): cint {.
|
|||
importc: "rename", header: "<stdio.h>".}
|
||||
proc c_system(cmd: cstring): cint {.
|
||||
importc: "system", header: "<stdlib.h>".}
|
||||
proc c_strerror(errnum: cint): cstring {.
|
||||
importc: "strerror", header: "<string.h>".}
|
||||
proc c_strlen(a: cstring): cint {.
|
||||
importc: "strlen", header: "<string.h>", noSideEffect.}
|
||||
proc c_getenv(env: cstring): cstring {.
|
||||
importc: "getenv", header: "<stdlib.h>".}
|
||||
proc c_putenv(env: cstring): cint {.
|
||||
importc: "putenv", header: "<stdlib.h>".}
|
||||
proc c_free(p: pointer) {.
|
||||
importc: "free", header: "<stdlib.h>".}
|
||||
|
||||
var errno {.importc, header: "<errno.h>".}: cint
|
||||
|
||||
proc osErrorMsg*(): string {.rtl, extern: "nos$1", deprecated.} =
|
||||
## Retrieves the operating system's error flag, ``errno``.
|
||||
## On Windows ``GetLastError`` is checked before ``errno``.
|
||||
## Returns "" if no error occurred.
|
||||
##
|
||||
## **Deprecated since version 0.9.4**: use the other ``osErrorMsg`` proc.
|
||||
|
||||
result = ""
|
||||
when defined(Windows):
|
||||
var err = getLastError()
|
||||
if err != 0'i32:
|
||||
when useWinUnicode:
|
||||
var msgbuf: WideCString
|
||||
if formatMessageW(0x00000100 or 0x00001000 or 0x00000200 or 0x000000FF,
|
||||
nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
|
||||
result = $msgbuf
|
||||
if msgbuf != nil: localFree(cast[pointer](msgbuf))
|
||||
else:
|
||||
var msgbuf: cstring
|
||||
if formatMessageA(0x00000100 or 0x00001000 or 0x00000200 or 0x000000FF,
|
||||
nil, err, 0, addr(msgbuf), 0, nil) != 0'i32:
|
||||
result = $msgbuf
|
||||
if msgbuf != nil: localFree(msgbuf)
|
||||
if errno != 0'i32:
|
||||
result = $os.c_strerror(errno)
|
||||
|
||||
{.push warning[deprecated]: off.}
|
||||
proc raiseOSError*(msg: string = "") {.noinline, rtl, extern: "nos$1",
|
||||
deprecated.} =
|
||||
## raises an OSError exception with the given message ``msg``.
|
||||
## If ``msg == ""``, the operating system's error flag
|
||||
## (``errno``) is converted to a readable error message. On Windows
|
||||
## ``GetLastError`` is checked before ``errno``.
|
||||
## If no error flag is set, the message ``unknown OS error`` is used.
|
||||
##
|
||||
## **Deprecated since version 0.9.4**: use the other ``raiseOSError`` proc.
|
||||
if len(msg) == 0:
|
||||
var m = osErrorMsg()
|
||||
raise newException(OSError, if m.len > 0: m else: "unknown OS error")
|
||||
else:
|
||||
raise newException(OSError, msg)
|
||||
{.pop.}
|
||||
|
||||
when not defined(nimfix):
|
||||
{.deprecated: [osError: raiseOSError].}
|
||||
|
||||
proc `==`*(err1, err2: OSErrorCode): bool {.borrow.}
|
||||
proc `$`*(err: OSErrorCode): string {.borrow.}
|
||||
|
||||
proc osErrorMsg*(errorCode: OSErrorCode): string =
|
||||
## Converts an OS error code into a human readable string.
|
||||
##
|
||||
## The error code can be retrieved using the ``osLastError`` proc.
|
||||
##
|
||||
## If conversion fails, or ``errorCode`` is ``0`` then ``""`` will be
|
||||
## returned.
|
||||
##
|
||||
## On Windows, the ``-d:useWinAnsi`` compilation flag can be used to
|
||||
## make this procedure use the non-unicode Win API calls to retrieve the
|
||||
## message.
|
||||
result = ""
|
||||
when defined(Windows):
|
||||
if errorCode != OSErrorCode(0'i32):
|
||||
when useWinUnicode:
|
||||
var msgbuf: WideCString
|
||||
if formatMessageW(0x00000100 or 0x00001000 or 0x00000200,
|
||||
nil, errorCode.int32, 0, addr(msgbuf), 0, nil) != 0'i32:
|
||||
result = $msgbuf
|
||||
if msgbuf != nil: localFree(cast[pointer](msgbuf))
|
||||
else:
|
||||
var msgbuf: cstring
|
||||
if formatMessageA(0x00000100 or 0x00001000 or 0x00000200,
|
||||
nil, errorCode.int32, 0, addr(msgbuf), 0, nil) != 0'i32:
|
||||
result = $msgbuf
|
||||
if msgbuf != nil: localFree(msgbuf)
|
||||
else:
|
||||
if errorCode != OSErrorCode(0'i32):
|
||||
result = $os.c_strerror(errorCode.int32)
|
||||
|
||||
proc raiseOSError*(errorCode: OSErrorCode; additionalInfo = "") {.noinline.} =
|
||||
## Raises an ``OSError`` exception. The ``errorCode`` will determine the
|
||||
## message, ``osErrorMsg`` will be used to get this message.
|
||||
##
|
||||
## The error code can be retrieved using the ``osLastError`` proc.
|
||||
##
|
||||
## If the error code is ``0`` or an error message could not be retrieved,
|
||||
## the message ``unknown OS error`` will be used.
|
||||
var e: ref OSError; new(e)
|
||||
e.errorCode = errorCode.int32
|
||||
if additionalInfo.len == 0:
|
||||
e.msg = osErrorMsg(errorCode)
|
||||
else:
|
||||
e.msg = osErrorMsg(errorCode) & "\nAdditional info: " & additionalInfo
|
||||
if e.msg == "":
|
||||
e.msg = "unknown OS error"
|
||||
raise e
|
||||
|
||||
{.push stackTrace:off.}
|
||||
proc osLastError*(): OSErrorCode =
|
||||
## Retrieves the last operating system error code.
|
||||
##
|
||||
## This procedure is useful in the event when an OS call fails. In that case
|
||||
## this procedure will return the error code describing the reason why the
|
||||
## OS call failed. The ``OSErrorMsg`` procedure can then be used to convert
|
||||
## this code into a string.
|
||||
##
|
||||
## **Warning**:
|
||||
## The behaviour of this procedure varies between Windows and POSIX systems.
|
||||
## On Windows some OS calls can reset the error code to ``0`` causing this
|
||||
## procedure to return ``0``. It is therefore advised to call this procedure
|
||||
## immediately after an OS call fails. On POSIX systems this is not a problem.
|
||||
|
||||
when defined(windows):
|
||||
result = OSErrorCode(getLastError())
|
||||
else:
|
||||
result = OSErrorCode(errno)
|
||||
{.pop.}
|
||||
|
||||
when defined(windows):
|
||||
when useWinUnicode:
|
||||
|
|
@ -244,6 +119,60 @@ proc dirExists*(dir: string): bool {.inline.} =
|
|||
## Synonym for existsDir
|
||||
existsDir(dir)
|
||||
|
||||
when not defined(windows):
|
||||
proc checkSymlink(path: string): bool =
|
||||
var rawInfo: Stat
|
||||
if lstat(path, rawInfo) < 0'i32: result = false
|
||||
else: result = S_ISLNK(rawInfo.st_mode)
|
||||
|
||||
const
|
||||
ExeExts* = when defined(windows): ["exe", "cmd", "bat"] else: [""] ## \
|
||||
## platform specific file extension for executables. On Windows
|
||||
## ``["exe", "cmd", "bat"]``, on Posix ``[""]``.
|
||||
|
||||
proc findExe*(exe: string, followSymlinks: bool = true;
|
||||
extensions: openarray[string]=ExeExts): string {.
|
||||
tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect].} =
|
||||
## Searches for `exe` in the current working directory and then
|
||||
## in directories listed in the ``PATH`` environment variable.
|
||||
## Returns "" if the `exe` cannot be found. `exe`
|
||||
## is added the `ExeExts <#ExeExts>`_ file extensions if it has none.
|
||||
## If the system supports symlinks it also resolves them until it
|
||||
## meets the actual file. This behavior can be disabled if desired.
|
||||
for ext in extensions:
|
||||
result = addFileExt(exe, ext)
|
||||
if existsFile(result): return
|
||||
var path = string(getEnv("PATH"))
|
||||
for candidate in split(path, PathSep):
|
||||
when defined(windows):
|
||||
var x = (if candidate[0] == '"' and candidate[^1] == '"':
|
||||
substr(candidate, 1, candidate.len-2) else: candidate) /
|
||||
exe
|
||||
else:
|
||||
var x = expandTilde(candidate) / exe
|
||||
for ext in extensions:
|
||||
var x = addFileExt(x, ext)
|
||||
if existsFile(x):
|
||||
when not defined(windows):
|
||||
while followSymlinks: # doubles as if here
|
||||
if x.checkSymlink:
|
||||
var r = newString(256)
|
||||
var len = readlink(x, r, 256)
|
||||
if len < 0:
|
||||
raiseOSError(osLastError())
|
||||
if len > 256:
|
||||
r = newString(len+1)
|
||||
len = readlink(x, r, len)
|
||||
setLen(r, len)
|
||||
if isAbsolute(r):
|
||||
x = r
|
||||
else:
|
||||
x = parentDir(x) / r
|
||||
else:
|
||||
break
|
||||
return x
|
||||
result = ""
|
||||
|
||||
proc getLastModificationTime*(file: string): Time {.rtl, extern: "nos$1".} =
|
||||
## Returns the `file`'s last modification time.
|
||||
when defined(posix):
|
||||
|
|
@ -706,147 +635,6 @@ proc execShellCmd*(command: string): int {.rtl, extern: "nos$1",
|
|||
else:
|
||||
result = c_system(command)
|
||||
|
||||
# Environment handling cannot be put into RTL, because the ``envPairs``
|
||||
# iterator depends on ``environment``.
|
||||
|
||||
var
|
||||
envComputed {.threadvar.}: bool
|
||||
environment {.threadvar.}: seq[string]
|
||||
|
||||
when defined(windows):
|
||||
# because we support Windows GUI applications, things get really
|
||||
# messy here...
|
||||
when useWinUnicode:
|
||||
when defined(cpp):
|
||||
proc strEnd(cstr: WideCString, c = 0'i32): WideCString {.
|
||||
importcpp: "(NI16*)wcschr((const wchar_t *)#, #)", header: "<string.h>".}
|
||||
else:
|
||||
proc strEnd(cstr: WideCString, c = 0'i32): WideCString {.
|
||||
importc: "wcschr", header: "<string.h>".}
|
||||
else:
|
||||
proc strEnd(cstr: cstring, c = 0'i32): cstring {.
|
||||
importc: "strchr", header: "<string.h>".}
|
||||
|
||||
proc getEnvVarsC() =
|
||||
if not envComputed:
|
||||
environment = @[]
|
||||
when useWinUnicode:
|
||||
var
|
||||
env = getEnvironmentStringsW()
|
||||
e = env
|
||||
if e == nil: return # an error occurred
|
||||
while true:
|
||||
var eend = strEnd(e)
|
||||
add(environment, $e)
|
||||
e = cast[WideCString](cast[ByteAddress](eend)+2)
|
||||
if eend[1].int == 0: break
|
||||
discard freeEnvironmentStringsW(env)
|
||||
else:
|
||||
var
|
||||
env = getEnvironmentStringsA()
|
||||
e = env
|
||||
if e == nil: return # an error occurred
|
||||
while true:
|
||||
var eend = strEnd(e)
|
||||
add(environment, $e)
|
||||
e = cast[cstring](cast[ByteAddress](eend)+1)
|
||||
if eend[1] == '\0': break
|
||||
discard freeEnvironmentStringsA(env)
|
||||
envComputed = true
|
||||
|
||||
else:
|
||||
const
|
||||
useNSGetEnviron = defined(macosx) and not defined(ios)
|
||||
|
||||
when useNSGetEnviron:
|
||||
# From the manual:
|
||||
# Shared libraries and bundles don't have direct access to environ,
|
||||
# which is only available to the loader ld(1) when a complete program
|
||||
# is being linked.
|
||||
# The environment routines can still be used, but if direct access to
|
||||
# environ is needed, the _NSGetEnviron() routine, defined in
|
||||
# <crt_externs.h>, can be used to retrieve the address of environ
|
||||
# at runtime.
|
||||
proc NSGetEnviron(): ptr cstringArray {.
|
||||
importc: "_NSGetEnviron", header: "<crt_externs.h>".}
|
||||
else:
|
||||
var gEnv {.importc: "environ".}: cstringArray
|
||||
|
||||
proc getEnvVarsC() =
|
||||
# retrieves the variables of char** env of C's main proc
|
||||
if not envComputed:
|
||||
environment = @[]
|
||||
when useNSGetEnviron:
|
||||
var gEnv = NSGetEnviron()[]
|
||||
var i = 0
|
||||
while true:
|
||||
if gEnv[i] == nil: break
|
||||
add environment, $gEnv[i]
|
||||
inc(i)
|
||||
envComputed = true
|
||||
|
||||
proc findEnvVar(key: string): int =
|
||||
getEnvVarsC()
|
||||
var temp = key & '='
|
||||
for i in 0..high(environment):
|
||||
if startsWith(environment[i], temp): return i
|
||||
return -1
|
||||
|
||||
proc getEnv*(key: string): TaintedString {.tags: [ReadEnvEffect].} =
|
||||
## Returns the value of the `environment variable`:idx: named `key`.
|
||||
##
|
||||
## If the variable does not exist, "" is returned. To distinguish
|
||||
## whether a variable exists or it's value is just "", call
|
||||
## `existsEnv(key)`.
|
||||
var i = findEnvVar(key)
|
||||
if i >= 0:
|
||||
return TaintedString(substr(environment[i], find(environment[i], '=')+1))
|
||||
else:
|
||||
var env = c_getenv(key)
|
||||
if env == nil: return TaintedString("")
|
||||
result = TaintedString($env)
|
||||
|
||||
proc existsEnv*(key: string): bool {.tags: [ReadEnvEffect].} =
|
||||
## Checks whether the environment variable named `key` exists.
|
||||
## Returns true if it exists, false otherwise.
|
||||
if c_getenv(key) != nil: return true
|
||||
else: return findEnvVar(key) >= 0
|
||||
|
||||
proc putEnv*(key, val: string) {.tags: [WriteEnvEffect].} =
|
||||
## Sets the value of the `environment variable`:idx: named `key` to `val`.
|
||||
## If an error occurs, `EInvalidEnvVar` is raised.
|
||||
|
||||
# Note: by storing the string in the environment sequence,
|
||||
# we guarantee that we don't free the memory before the program
|
||||
# ends (this is needed for POSIX compliance). It is also needed so that
|
||||
# the process itself may access its modified environment variables!
|
||||
var indx = findEnvVar(key)
|
||||
if indx >= 0:
|
||||
environment[indx] = key & '=' & val
|
||||
else:
|
||||
add environment, (key & '=' & val)
|
||||
indx = high(environment)
|
||||
when defined(windows):
|
||||
when useWinUnicode:
|
||||
var k = newWideCString(key)
|
||||
var v = newWideCString(val)
|
||||
if setEnvironmentVariableW(k, v) == 0'i32: raiseOSError(osLastError())
|
||||
else:
|
||||
if setEnvironmentVariableA(key, val) == 0'i32: raiseOSError(osLastError())
|
||||
else:
|
||||
if c_putenv(environment[indx]) != 0'i32:
|
||||
raiseOSError(osLastError())
|
||||
|
||||
iterator envPairs*(): tuple[key, value: TaintedString] {.tags: [ReadEnvEffect].} =
|
||||
## Iterate over all `environments variables`:idx:. In the first component
|
||||
## of the tuple is the name of the current variable stored, in the second
|
||||
## its value.
|
||||
getEnvVarsC()
|
||||
for i in 0..high(environment):
|
||||
var p = find(environment[i], '=')
|
||||
yield (TaintedString(substr(environment[i], 0, p-1)),
|
||||
TaintedString(substr(environment[i], p+1)))
|
||||
|
||||
# Templates for filtering directories and files
|
||||
when defined(windows):
|
||||
template isDir(f: WIN32_FIND_DATA): bool =
|
||||
|
|
|
|||
|
|
@ -7,19 +7,14 @@
|
|||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
# Included by the ``os`` module but a module in its own right for NimScript
|
||||
# Forwarded by the ``os`` module but a module in its own right for NimScript
|
||||
# support.
|
||||
|
||||
when not declared(os):
|
||||
{.pragma: rtl.}
|
||||
import strutils
|
||||
include "system/inclrtl"
|
||||
|
||||
when defined(nimscript) or (defined(nimdoc) and not declared(os)):
|
||||
{.pragma: rtl.}
|
||||
{.push hint[ConvFromXtoItselfNotNeeded]:off.}
|
||||
import strutils
|
||||
|
||||
when not declared(getEnv) or defined(nimscript):
|
||||
type
|
||||
type
|
||||
ReadEnvEffect* = object of ReadIOEffect ## effect that denotes a read
|
||||
## from an environment variable
|
||||
WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
|
||||
|
|
@ -34,15 +29,15 @@ when not declared(getEnv) or defined(nimscript):
|
|||
|
||||
OSErrorCode* = distinct int32 ## Specifies an OS Error Code.
|
||||
|
||||
{.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
|
||||
{.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
|
||||
FReadDir: ReadDirEffect,
|
||||
FWriteDir: WriteDirEffect,
|
||||
TOSErrorCode: OSErrorCode
|
||||
].}
|
||||
const
|
||||
].}
|
||||
const
|
||||
doslikeFileSystem* = defined(windows) or defined(OS2) or defined(DOS)
|
||||
|
||||
when defined(Nimdoc): # only for proper documentation:
|
||||
when defined(Nimdoc): # only for proper documentation:
|
||||
const
|
||||
CurDir* = '.'
|
||||
## The constant string used by the operating system to refer to the
|
||||
|
|
@ -88,7 +83,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
## The format string to turn a filename into a `DLL`:idx: file (also
|
||||
## called `shared object`:idx: on some operating systems).
|
||||
|
||||
elif defined(macos):
|
||||
elif defined(macos):
|
||||
const
|
||||
CurDir* = ':'
|
||||
ParDir* = "::"
|
||||
|
|
@ -119,7 +114,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
# waterproof. In case of equal names the first volume found will do.
|
||||
# Two colons "::" are the relative path to the parent. Three is to the
|
||||
# grandparent etc.
|
||||
elif doslikeFileSystem:
|
||||
elif doslikeFileSystem:
|
||||
const
|
||||
CurDir* = '.'
|
||||
ParDir* = ".."
|
||||
|
|
@ -130,7 +125,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
ExeExt* = "exe"
|
||||
ScriptExt* = "bat"
|
||||
DynlibFormat* = "$1.dll"
|
||||
elif defined(PalmOS) or defined(MorphOS):
|
||||
elif defined(PalmOS) or defined(MorphOS):
|
||||
const
|
||||
DirSep* = '/'
|
||||
AltSep* = Dirsep
|
||||
|
|
@ -140,7 +135,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
ExeExt* = ""
|
||||
ScriptExt* = ""
|
||||
DynlibFormat* = "$1.prc"
|
||||
elif defined(RISCOS):
|
||||
elif defined(RISCOS):
|
||||
const
|
||||
DirSep* = '.'
|
||||
AltSep* = '.'
|
||||
|
|
@ -150,7 +145,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
ExeExt* = ""
|
||||
ScriptExt* = ""
|
||||
DynlibFormat* = "lib$1.so"
|
||||
else: # UNIX-like operating system
|
||||
else: # UNIX-like operating system
|
||||
const
|
||||
CurDir* = '.'
|
||||
ParDir* = ".."
|
||||
|
|
@ -162,13 +157,13 @@ when not declared(getEnv) or defined(nimscript):
|
|||
ScriptExt* = ""
|
||||
DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"
|
||||
|
||||
const
|
||||
const
|
||||
ExtSep* = '.'
|
||||
## The character which separates the base filename from the extension;
|
||||
## for example, the '.' in ``os.nim``.
|
||||
|
||||
|
||||
proc joinPath*(head, tail: string): string {.
|
||||
proc joinPath*(head, tail: string): string {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Joins two directory names to one.
|
||||
##
|
||||
|
|
@ -206,7 +201,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
else:
|
||||
result = head & DirSep & tail
|
||||
|
||||
proc joinPath*(parts: varargs[string]): string {.noSideEffect,
|
||||
proc joinPath*(parts: varargs[string]): string {.noSideEffect,
|
||||
rtl, extern: "nos$1OpenArray".} =
|
||||
## The same as `joinPath(head, tail)`, but works with any number of
|
||||
## directory parts. You need to pass at least one element or the proc
|
||||
|
|
@ -215,7 +210,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
for i in 1..high(parts):
|
||||
result = joinPath(result, parts[i])
|
||||
|
||||
proc `/` * (head, tail: string): string {.noSideEffect.} =
|
||||
proc `/` * (head, tail: string): string {.noSideEffect.} =
|
||||
## The same as ``joinPath(head, tail)``
|
||||
##
|
||||
## Here are some examples for Unix:
|
||||
|
|
@ -227,7 +222,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
## assert "usr/" / "/lib" == "usr/lib"
|
||||
return joinPath(head, tail)
|
||||
|
||||
proc splitPath*(path: string): tuple[head, tail: string] {.
|
||||
proc splitPath*(path: string): tuple[head, tail: string] {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Splits a directory into (head, tail), so that
|
||||
## ``head / tail == path`` (except for edge cases like "/usr").
|
||||
|
|
@ -252,14 +247,14 @@ when not declared(getEnv) or defined(nimscript):
|
|||
result.head = ""
|
||||
result.tail = path
|
||||
|
||||
proc parentDirPos(path: string): int =
|
||||
proc parentDirPos(path: string): int =
|
||||
var q = 1
|
||||
if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
|
||||
for i in countdown(len(path)-q, 0):
|
||||
if path[i] in {DirSep, AltSep}: return i
|
||||
result = -1
|
||||
|
||||
proc parentDir*(path: string): string {.
|
||||
proc parentDir*(path: string): string {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Returns the parent directory of `path`.
|
||||
##
|
||||
|
|
@ -273,7 +268,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
else:
|
||||
result = ""
|
||||
|
||||
proc tailDir*(path: string): string {.
|
||||
proc tailDir*(path: string): string {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Returns the tail part of `path`..
|
||||
##
|
||||
|
|
@ -287,12 +282,12 @@ when not declared(getEnv) or defined(nimscript):
|
|||
return substr(path, i+1)
|
||||
result = ""
|
||||
|
||||
proc isRootDir*(path: string): bool {.
|
||||
proc isRootDir*(path: string): bool {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Checks whether a given `path` is a root directory
|
||||
result = parentDirPos(path) < 0
|
||||
|
||||
iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
|
||||
iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
|
||||
## Walks over all parent directories of a given `path`
|
||||
##
|
||||
## If `fromRoot` is set, the traversal will start from the file system root
|
||||
|
|
@ -317,7 +312,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
|
||||
if inclusive: yield path
|
||||
|
||||
proc `/../` * (head, tail: string): string {.noSideEffect.} =
|
||||
proc `/../`*(head, tail: string): string {.noSideEffect.} =
|
||||
## The same as ``parentDir(head) / tail`` unless there is no parent
|
||||
## directory. Then ``head / tail`` is performed instead.
|
||||
let sepPos = parentDirPos(head)
|
||||
|
|
@ -326,11 +321,11 @@ when not declared(getEnv) or defined(nimscript):
|
|||
else:
|
||||
result = head / tail
|
||||
|
||||
proc normExt(ext: string): string =
|
||||
proc normExt(ext: string): string =
|
||||
if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
|
||||
else: result = ExtSep & ext
|
||||
|
||||
proc searchExtPos*(path: string): int =
|
||||
proc searchExtPos*(path: string): int =
|
||||
## Returns index of the '.' char in `path` if it signifies the beginning
|
||||
## of extension. Returns -1 otherwise.
|
||||
# BUGFIX: do not search until 0! .DS_Store is no file extension!
|
||||
|
|
@ -342,7 +337,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
elif path[i] in {DirSep, AltSep}:
|
||||
break # do not skip over path
|
||||
|
||||
proc splitFile*(path: string): tuple[dir, name, ext: string] {.
|
||||
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`.
|
||||
|
|
@ -375,7 +370,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
result.name = substr(path, sepPos+1, dotPos-1)
|
||||
result.ext = substr(path, dotPos)
|
||||
|
||||
proc extractFilename*(path: string): string {.
|
||||
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)``.
|
||||
|
|
@ -385,7 +380,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
result = splitPath(path).tail
|
||||
|
||||
|
||||
proc changeFileExt*(filename, ext: string): string {.
|
||||
proc changeFileExt*(filename, ext: string): string {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Changes the file extension to `ext`.
|
||||
##
|
||||
|
|
@ -398,7 +393,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
if extPos < 0: result = filename & normExt(ext)
|
||||
else: result = substr(filename, 0, extPos-1) & normExt(ext)
|
||||
|
||||
proc addFileExt*(filename, ext: string): string {.
|
||||
proc addFileExt*(filename, ext: string): string {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Adds the file extension `ext` to `filename`, unless
|
||||
## `filename` already has an extension.
|
||||
|
|
@ -410,7 +405,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
if extPos < 0: result = filename & normExt(ext)
|
||||
else: result = filename
|
||||
|
||||
proc cmpPaths*(pathA, pathB: string): int {.
|
||||
proc cmpPaths*(pathA, pathB: string): int {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Compares two paths.
|
||||
##
|
||||
|
|
@ -429,7 +424,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
else:
|
||||
result = cmpIgnoreCase(pathA, pathB)
|
||||
|
||||
proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} =
|
||||
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.
|
||||
|
|
@ -444,7 +439,7 @@ when not declared(getEnv) or defined(nimscript):
|
|||
elif defined(posix):
|
||||
result = path[0] == '/'
|
||||
|
||||
proc unixToNativePath*(path: string, drive=""): string {.
|
||||
proc unixToNativePath*(path: string, drive=""): string {.
|
||||
noSideEffect, rtl, extern: "nos$1".} =
|
||||
## Converts an UNIX-like path to a native one.
|
||||
##
|
||||
|
|
@ -499,12 +494,10 @@ when not declared(getEnv) or defined(nimscript):
|
|||
add result, path[i]
|
||||
inc(i)
|
||||
|
||||
when defined(nimdoc) and not declared(os):
|
||||
proc getEnv(x: string): string = discard
|
||||
proc existsFile(x: string): bool = discard
|
||||
include "includes/oserr"
|
||||
include "includes/osenv"
|
||||
|
||||
when declared(getEnv) or defined(nimscript):
|
||||
proc getHomeDir*(): string {.rtl, extern: "nos$1",
|
||||
proc getHomeDir*(): string {.rtl, extern: "nos$1",
|
||||
tags: [ReadEnvEffect, ReadIOEffect].} =
|
||||
## Returns the home directory of the current user.
|
||||
##
|
||||
|
|
@ -513,7 +506,7 @@ when declared(getEnv) or defined(nimscript):
|
|||
when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
|
||||
else: return string(getEnv("HOME")) & "/"
|
||||
|
||||
proc getConfigDir*(): string {.rtl, extern: "nos$1",
|
||||
proc getConfigDir*(): string {.rtl, extern: "nos$1",
|
||||
tags: [ReadEnvEffect, ReadIOEffect].} =
|
||||
## Returns the config directory of the current user for applications.
|
||||
##
|
||||
|
|
@ -528,7 +521,7 @@ when declared(getEnv) or defined(nimscript):
|
|||
elif getEnv("XDG_CONFIG_DIR"): return string(getEnv("XDG_CONFIG_DIR")) & "/"
|
||||
else: return string(getEnv("HOME")) & "/.config/"
|
||||
|
||||
proc getTempDir*(): string {.rtl, extern: "nos$1",
|
||||
proc getTempDir*(): string {.rtl, extern: "nos$1",
|
||||
tags: [ReadEnvEffect, ReadIOEffect].} =
|
||||
## Returns the temporary directory of the current user for applications to
|
||||
## save temporary files in.
|
||||
|
|
@ -544,7 +537,7 @@ when declared(getEnv) or defined(nimscript):
|
|||
elif defined(android): return getHomeDir()
|
||||
else: return "/tmp/"
|
||||
|
||||
proc expandTilde*(path: string): string {.
|
||||
proc expandTilde*(path: string): string {.
|
||||
tags: [ReadEnvEffect, ReadIOEffect].} =
|
||||
## Expands a path starting with ``~/`` to a full path.
|
||||
##
|
||||
|
|
@ -564,70 +557,3 @@ when declared(getEnv) or defined(nimscript):
|
|||
result = getHomeDir() / path.substr(2)
|
||||
else:
|
||||
result = path
|
||||
|
||||
when not declared(split):
|
||||
iterator split(s: string, sep: char): string =
|
||||
var last = 0
|
||||
if len(s) > 0:
|
||||
while last <= len(s):
|
||||
var first = last
|
||||
while last < len(s) and s[last] != sep: inc(last)
|
||||
yield substr(s, first, last-1)
|
||||
inc(last)
|
||||
|
||||
when not defined(windows) and declared(os):
|
||||
proc checkSymlink(path: string): bool =
|
||||
var rawInfo: Stat
|
||||
if lstat(path, rawInfo) < 0'i32: result = false
|
||||
else: result = S_ISLNK(rawInfo.st_mode)
|
||||
|
||||
const
|
||||
ExeExts* = when defined(windows): ["exe", "cmd", "bat"] else: [""] ## \
|
||||
## platform specific file extension for executables. On Windows
|
||||
## ``["exe", "cmd", "bat"]``, on Posix ``[""]``.
|
||||
|
||||
proc findExe*(exe: string, followSymlinks: bool = true;
|
||||
extensions: openarray[string]=ExeExts): string {.
|
||||
tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect].} =
|
||||
## Searches for `exe` in the current working directory and then
|
||||
## in directories listed in the ``PATH`` environment variable.
|
||||
## Returns "" if the `exe` cannot be found. `exe`
|
||||
## is added the `ExeExts <#ExeExts>`_ file extensions if it has none.
|
||||
## If the system supports symlinks it also resolves them until it
|
||||
## meets the actual file. This behavior can be disabled if desired.
|
||||
for ext in extensions:
|
||||
result = addFileExt(exe, ext)
|
||||
if existsFile(result): return
|
||||
var path = string(getEnv("PATH"))
|
||||
for candidate in split(path, PathSep):
|
||||
when defined(windows):
|
||||
var x = (if candidate[0] == '"' and candidate[^1] == '"':
|
||||
substr(candidate, 1, candidate.len-2) else: candidate) /
|
||||
exe
|
||||
else:
|
||||
var x = expandTilde(candidate) / exe
|
||||
for ext in extensions:
|
||||
var x = addFileExt(x, ext)
|
||||
if existsFile(x):
|
||||
when not defined(windows) and declared(os):
|
||||
while followSymlinks: # doubles as if here
|
||||
if x.checkSymlink:
|
||||
var r = newString(256)
|
||||
var len = readlink(x, r, 256)
|
||||
if len < 0:
|
||||
raiseOSError(osLastError())
|
||||
if len > 256:
|
||||
r = newString(len+1)
|
||||
len = readlink(x, r, len)
|
||||
setLen(r, len)
|
||||
if isAbsolute(r):
|
||||
x = r
|
||||
else:
|
||||
x = parentDir(x) / r
|
||||
else:
|
||||
break
|
||||
return x
|
||||
result = ""
|
||||
|
||||
when defined(nimscript) or (defined(nimdoc) and not declared(os)):
|
||||
{.pop.} # hint[ConvFromXtoItselfNotNeeded]:off
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue