Skip to content

Commit

Permalink
Added std/envvars for env vars handling (#19421)
Browse files Browse the repository at this point in the history
  • Loading branch information
ringabout authored Jan 20, 2022
1 parent ce44cf0 commit 4a38092
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 2 deletions.
4 changes: 2 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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`,
Expand Down
212 changes: 212 additions & 0 deletions lib/std/envvars.nim
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions tests/stdlib/tenvvars.nim
Original file line number Diff line number Diff line change
@@ -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")

0 comments on commit 4a38092

Please sign in to comment.