summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--changelog.md4
-rw-r--r--lib/std/envvars.nim212
-rw-r--r--tests/stdlib/tenvvars.nim59
3 files changed, 273 insertions, 2 deletions
diff --git a/changelog.md b/changelog.md
index 82f9daa1e..bbbaf6e97 100644
--- a/changelog.md
+++ b/changelog.md
@@ -36,6 +36,8 @@ becomes an alias for `addr`.
 - Added `getIsoWeekAndYear` in `times` to get an ISO week number along with the corresponding ISO week-based year from a datetime.
 - Added `getIsoWeeksInYear` in `times` to return the number of weeks in an ISO week-based year.
 
+- Added `std/oserrors` for OS error reporting. Added `std/envvars` for environment variables handling.
+
 ## Language changes
 
 - Pragma macros on type definitions can now return `nnkTypeSection` nodes as well as `nnkTypeDef`,
@@ -74,8 +76,6 @@ becomes an alias for `addr`.
   for the right-hand side of type definitions in type sections. Previously
   they would error with "invalid indentation".
 
-- Added `std/oserrors` for OS error reporting.
-
 ## Compiler changes
 
 - `nim` can now compile version 1.4.0 as follows: `nim c --lib:lib --stylecheck:off compiler/nim`,
diff --git a/lib/std/envvars.nim b/lib/std/envvars.nim
new file mode 100644
index 000000000..5b135cbd3
--- /dev/null
+++ b/lib/std/envvars.nim
@@ -0,0 +1,212 @@
+#
+#
+#            Nim's Runtime Library
+#        (c) Copyright 2022 Nim contributors
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+
+## The `std/envvars` module implements environment variables handling.
+import std/oserrors
+
+type
+  ReadEnvEffect* = object of ReadIOEffect   ## Effect that denotes a read
+                                            ## from an environment variable.
+  WriteEnvEffect* = object of WriteIOEffect ## Effect that denotes a write
+                                            ## to an environment variable.
+
+
+when not defined(nimscript):
+  when defined(nodejs):
+    proc getEnv*(key: string, default = ""): string {.tags: [ReadEnvEffect].} =
+      var ret = default.cstring
+      let key2 = key.cstring
+      {.emit: "const value = process.env[`key2`];".}
+      {.emit: "if (value !== undefined) { `ret` = value };".}
+      result = $ret
+
+    proc existsEnv*(key: string): bool {.tags: [ReadEnvEffect].} =
+      var key2 = key.cstring
+      var ret: bool
+      {.emit: "`ret` = `key2` in process.env;".}
+      result = ret
+
+    proc putEnv*(key, val: string) {.tags: [WriteEnvEffect].} =
+      var key2 = key.cstring
+      var val2 = val.cstring
+      {.emit: "process.env[`key2`] = `val2`;".}
+
+    proc delEnv*(key: string) {.tags: [WriteEnvEffect].} =
+      var key2 = key.cstring
+      {.emit: "delete process.env[`key2`];".}
+
+    iterator envPairsImpl(): tuple[key, value: string] {.tags: [ReadEnvEffect].} =
+      var num: int
+      var keys: RootObj
+      {.emit: "`keys` = Object.keys(process.env); `num` = `keys`.length;".}
+      for i in 0..<num:
+        var key, value: cstring
+        {.emit: "`key` = `keys`[`i`]; `value` = process.env[`key`];".}
+        yield ($key, $value)
+
+  # commented because it must keep working with js+VM
+  # elif defined(js):
+  #   {.error: "requires -d:nodejs".}
+
+  else:
+
+    proc c_getenv(env: cstring): cstring {.
+      importc: "getenv", header: "<stdlib.h>".}
+    when defined(windows):
+      proc c_putenv(envstring: cstring): cint {.importc: "_putenv", header: "<stdlib.h>".}
+      from std/private/win_setenv import setEnvImpl
+      import winlean
+    else:
+      proc c_setenv(envname: cstring, envval: cstring, overwrite: cint): cint {.importc: "setenv", header: "<stdlib.h>".}
+      proc c_unsetenv(env: cstring): cint {.importc: "unsetenv", header: "<stdlib.h>".}
+
+    proc getEnv*(key: string, default = ""): string {.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) proc`_.
+      ##
+      ## See also:
+      ## * `existsEnv proc`_
+      ## * `putEnv proc`_
+      ## * `delEnv proc`_
+      ## * `envPairs iterator`_
+      runnableExamples:
+        assert getEnv("unknownEnv") == ""
+        assert getEnv("unknownEnv", "doesn't exist") == "doesn't exist"
+
+      let env = c_getenv(key)
+      if env == nil: return default
+      result = $env
+
+    proc existsEnv*(key: string): bool {.tags: [ReadEnvEffect].} =
+      ## Checks whether the environment variable named `key` exists.
+      ## Returns true if it exists, false otherwise.
+      ##
+      ## See also:
+      ## * `getEnv proc`_
+      ## * `putEnv proc`_
+      ## * `delEnv proc`_
+      ## * `envPairs iterator`_
+      runnableExamples:
+        assert not existsEnv("unknownEnv")
+
+      return c_getenv(key) != nil
+
+    proc putEnv*(key, val: string) {.tags: [WriteEnvEffect].} =
+      ## Sets the value of the `environment variable`:idx: named `key` to `val`.
+      ## If an error occurs, `OSError` is raised.
+      ##
+      ## See also:
+      ## * `getEnv proc`_
+      ## * `existsEnv proc`_
+      ## * `delEnv proc`_
+      ## * `envPairs iterator`_
+      when defined(windows):
+        if key.len == 0 or '=' in key:
+          raise newException(OSError, "invalid key, got: " & $(key, val))
+        if setEnvImpl(key, val, 1'i32) != 0'i32:
+          raiseOSError(osLastError(), $(key, val))
+      else:
+        if c_setenv(key, val, 1'i32) != 0'i32:
+          raiseOSError(osLastError(), $(key, val))
+
+    proc delEnv*(key: string) {.tags: [WriteEnvEffect].} =
+      ## Deletes the `environment variable`:idx: named `key`.
+      ## If an error occurs, `OSError` is raised.
+      ##
+      ## See also:ven
+      ## * `getEnv proc`_
+      ## * `existsEnv proc`_
+      ## * `putEnv proc`_
+      ## * `envPairs iterator`_
+      template bail = raiseOSError(osLastError(), key)
+      when defined(windows):
+        #[ 
+        # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/putenv-s-wputenv-s?view=msvc-160
+        > You can remove a variable from the environment by specifying an empty string (that is, "") for value_string
+        note that nil is not legal
+        ]#
+        if key.len == 0 or '=' in key:
+          raise newException(OSError, "invalid key, got: " & key)
+        let envToDel = key & "="
+        if c_putenv(cstring envToDel) != 0'i32: bail
+      else:
+        if c_unsetenv(key) != 0'i32: bail
+
+    when defined(windows):
+      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>".}
+    elif defined(macosx) and not defined(ios) and not defined(emscripten):
+      # 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>".}
+    elif defined(haiku):
+      var gEnv {.importc: "environ", header: "<stdlib.h>".}: cstringArray
+    else:
+      var gEnv {.importc: "environ".}: cstringArray
+
+    iterator envPairsImpl(): tuple[key, value: string] {.tags: [ReadEnvEffect].} =
+      when defined(windows):
+        block:
+          template impl(get_fun, typ, size, zero, free_fun) =
+            let env = get_fun()
+            var e = env
+            if e == nil: break
+            while true:
+              let eend = strEnd(e)
+              let kv = $e
+              let p = find(kv, '=')
+              yield (substr(kv, 0, p-1), substr(kv, p+1))
+              e = cast[typ](cast[ByteAddress](eend)+size)
+              if typeof(zero)(eend[1]) == zero: break
+            discard free_fun(env)
+          impl(getEnvironmentStringsW, WideCString, 2, 0, freeEnvironmentStringsW)
+      else:
+        var i = 0
+        when defined(macosx) and not defined(ios) and not defined(emscripten):
+          var gEnv = NSGetEnviron()[]
+        while gEnv[i] != nil:
+          let kv = $gEnv[i]
+          inc(i)
+          let p = find(kv, '=')
+          yield (substr(kv, 0, p-1), substr(kv, p+1))
+
+proc envPairsImplSeq(): seq[tuple[key, value: string]] = discard # vmops
+
+iterator envPairs*(): tuple[key, value: string] {.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.
+  ##
+  ## Works in native backends, nodejs and vm, like the following APIs:
+  ## * `getEnv proc`_
+  ## * `existsEnv proc`_
+  ## * `putEnv proc`_
+  ## * `delEnv proc`_
+  when nimvm:
+    for ai in envPairsImplSeq(): yield ai
+  else:
+    when defined(nimscript): discard
+    else:
+      for ai in envPairsImpl(): yield ai
diff --git a/tests/stdlib/tenvvars.nim b/tests/stdlib/tenvvars.nim
new file mode 100644
index 000000000..406aa3230
--- /dev/null
+++ b/tests/stdlib/tenvvars.nim
@@ -0,0 +1,59 @@
+discard """
+  matrix: "--threads:on"
+  joinable: false
+  targets: "c js cpp"
+"""
+
+import std/envvars
+from std/sequtils import toSeq
+import stdtest/testutils
+
+template main =
+  block: # delEnv, existsEnv, getEnv, envPairs
+    for val in ["val", ""]: # ensures empty val works too
+      const key = "NIM_TESTS_TOSENV_KEY"
+      doAssert not existsEnv(key)
+
+      putEnv(key, "tempval")
+      doAssert existsEnv(key)
+      doAssert getEnv(key) == "tempval"
+
+      putEnv(key, val) # change a key that already exists
+      doAssert existsEnv(key)
+      doAssert getEnv(key) == val
+
+      doAssert (key, val) in toSeq(envPairs())
+      delEnv(key)
+      doAssert (key, val) notin toSeq(envPairs())
+      doAssert not existsEnv(key)
+      delEnv(key) # deleting an already deleted env var
+      doAssert not existsEnv(key)
+
+    block:
+      doAssert getEnv("NIM_TESTS_TOSENV_NONEXISTENT", "") == ""
+      doAssert getEnv("NIM_TESTS_TOSENV_NONEXISTENT", " ") == " "
+      doAssert getEnv("NIM_TESTS_TOSENV_NONEXISTENT", "defval") == "defval"
+
+    whenVMorJs: discard # xxx improve
+    do:
+      doAssertRaises(OSError, putEnv("NIM_TESTS_TOSENV_PUT=DUMMY_VALUE", "NEW_DUMMY_VALUE"))
+      doAssertRaises(OSError, putEnv("", "NEW_DUMMY_VALUE"))
+      doAssert not existsEnv("")
+      doAssert not existsEnv("NIM_TESTS_TOSENV_PUT=DUMMY_VALUE")
+      doAssert not existsEnv("NIM_TESTS_TOSENV_PUT")
+
+main()
+
+when not defined(js) and not defined(nimscript):
+  block: # bug #18533
+    proc c_getenv(env: cstring): cstring {.importc: "getenv", header: "<stdlib.h>".}
+    var thr: Thread[void]
+    proc threadFunc {.thread.} = putEnv("foo", "fooVal2")
+
+    putEnv("foo", "fooVal1")
+    doAssert getEnv("foo") == "fooVal1"
+    createThread(thr, threadFunc)
+    joinThreads(thr)
+    doAssert getEnv("foo") == $c_getenv("foo")
+
+    doAssertRaises(OSError): delEnv("foo=bar")