From 99eec73b4b8813c6db7cae83f5415b031de0c2c7 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 2 Oct 2019 11:55:28 -0400 Subject: [PATCH] feat: Add support for passing a key to Deno.env() (#2952) This adds a new op to get a single env var. --- cli/deno_error.rs | 12 ++++++ cli/ops/os.rs | 19 ++++++++ cli/worker.rs | 4 ++ js/dispatch.ts | 1 + js/lib.deno_runtime.d.ts | 10 +++++ js/os.ts | 14 +++++- js/os_test.ts | 93 ++++++++++++++++++++++++++++++++++++---- 7 files changed, 144 insertions(+), 9 deletions(-) diff --git a/cli/deno_error.rs b/cli/deno_error.rs index eb885adb85..551547e269 100644 --- a/cli/deno_error.rs +++ b/cli/deno_error.rs @@ -11,6 +11,7 @@ use hyper; use reqwest; use rustyline::error::ReadlineError; use std; +use std::env::VarError; use std::error::Error; use std::fmt; use std::io; @@ -136,6 +137,16 @@ impl GetErrorKind for ModuleResolutionError { } } +impl GetErrorKind for VarError { + fn kind(&self) -> ErrorKind { + use VarError::*; + match self { + NotPresent => ErrorKind::NotFound, + NotUnicode(..) => ErrorKind::InvalidData, + } + } +} + impl GetErrorKind for io::Error { fn kind(&self) -> ErrorKind { use io::ErrorKind::*; @@ -294,6 +305,7 @@ impl GetErrorKind for dyn AnyError { .or_else(|| self.downcast_ref::().map(Get::kind)) .or_else(|| self.downcast_ref::().map(Get::kind)) .or_else(|| self.downcast_ref::().map(Get::kind)) + .or_else(|| self.downcast_ref::().map(Get::kind)) .or_else(|| self.downcast_ref::().map(Get::kind)) .or_else(|| { self diff --git a/cli/ops/os.rs b/cli/ops/os.rs index 770af404c7..b35b76c2a2 100644 --- a/cli/ops/os.rs +++ b/cli/ops/os.rs @@ -102,6 +102,25 @@ pub fn op_env( Ok(JsonOp::Sync(json!(v))) } +#[derive(Deserialize)] +struct GetEnv { + key: String, +} + +pub fn op_get_env( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: GetEnv = serde_json::from_value(args)?; + state.check_env()?; + let r = match env::var(args.key) { + Err(env::VarError::NotPresent) => json!([]), + v => json!([v?]), + }; + Ok(JsonOp::Sync(r)) +} + #[derive(Deserialize)] struct Exit { code: i32, diff --git a/cli/worker.rs b/cli/worker.rs index 6f5e6af41a..9bdf2ae08b 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -63,6 +63,10 @@ impl Worker { "set_env", state_.cli_op(json_op(state_.stateful_op(os::op_set_env))), ); + i.register_op( + "get_env", + state_.cli_op(json_op(state_.stateful_op(os::op_get_env))), + ); i.register_op( "home_dir", state_.cli_op(json_op(state_.stateful_op(os::op_home_dir))), diff --git a/js/dispatch.ts b/js/dispatch.ts index 67c831e095..7f30707a7f 100644 --- a/js/dispatch.ts +++ b/js/dispatch.ts @@ -11,6 +11,7 @@ export let OP_ENV: number; export let OP_EXEC_PATH: number; export let OP_UTIME: number; export let OP_SET_ENV: number; +export let OP_GET_ENV: number; export let OP_HOME_DIR: number; export let OP_START: number; export let OP_APPLY_SOURCE_MAP: number; diff --git a/js/lib.deno_runtime.d.ts b/js/lib.deno_runtime.d.ts index d0a720769c..7805031063 100644 --- a/js/lib.deno_runtime.d.ts +++ b/js/lib.deno_runtime.d.ts @@ -44,6 +44,16 @@ declare namespace Deno { export function env(): { [index: string]: string; }; + /** Returns the value of an environment variable at invocation. + * If the variable is not present, `undefined` will be returned. + * + * const myEnv = Deno.env(); + * console.log(myEnv.SHELL); + * myEnv.TEST_VAR = "HELLO"; + * const newEnv = Deno.env(); + * console.log(myEnv.TEST_VAR == newEnv.TEST_VAR); + */ + export function env(key: string): string | undefined; /** * Returns the current user's home directory. * Requires the `--allow-env` flag. diff --git a/js/os.ts b/js/os.ts index df30b3906e..2fc06434a8 100644 --- a/js/os.ts +++ b/js/os.ts @@ -37,18 +37,30 @@ function setEnv(key: string, value: string): void { sendSync(dispatch.OP_SET_ENV, { key, value }); } +function getEnv(key: string): string | undefined { + return sendSync(dispatch.OP_GET_ENV, { key })[0]; +} + /** Returns a snapshot of the environment variables at invocation. Mutating a * property in the object will set that variable in the environment for * the process. The environment object will only accept `string`s * as values. * + * console.log(Deno.env("SHELL")); * const myEnv = Deno.env(); * console.log(myEnv.SHELL); * myEnv.TEST_VAR = "HELLO"; * const newEnv = Deno.env(); * console.log(myEnv.TEST_VAR == newEnv.TEST_VAR); */ -export function env(): { [index: string]: string } { +export function env(): { [index: string]: string }; +export function env(key: string): string | undefined; +export function env( + key?: string +): { [index: string]: string } | string | undefined { + if (key) { + return getEnv(key); + } const env = sendSync(dispatch.OP_ENV); return new Proxy(env, { set(obj, prop: string, value: string): boolean { diff --git a/js/os_test.ts b/js/os_test.ts index ad37726313..0d07df1b49 100644 --- a/js/os_test.ts +++ b/js/os_test.ts @@ -14,21 +14,98 @@ testPerm({ env: true }, function envSuccess(): void { env.test_var = "Hello World"; const newEnv = Deno.env(); assertEquals(env.test_var, newEnv.test_var); + assertEquals(Deno.env("test_var"), env.test_var); }); -test(function envFailure(): void { - let caughtError = false; +testPerm({ env: true }, function envNotFound(): void { + const r = Deno.env("env_var_does_not_exist!"); + assertEquals(r, undefined); +}); + +test(function envPermissionDenied1(): void { + let err; try { Deno.env(); - } catch (err) { - caughtError = true; - assertEquals(err.kind, Deno.ErrorKind.PermissionDenied); - assertEquals(err.name, "PermissionDenied"); + } catch (e) { + err = e; } - - assert(caughtError); + assertNotEquals(err, undefined); + assertEquals(err.kind, Deno.ErrorKind.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); }); +test(function envPermissionDenied2(): void { + let err; + try { + Deno.env("PATH"); + } catch (e) { + err = e; + } + assertNotEquals(err, undefined); + assertEquals(err.kind, Deno.ErrorKind.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +if (Deno.build.os === "win") { + // This test verifies that on Windows, environment variables are + // case-insensitive. Case normalization needs be done using the collation + // that Windows uses, rather than naively using String.toLowerCase(). + testPerm({ env: true, run: true }, async function envCaseInsensitive() { + // Utility function that runs a Deno subprocess with the environment + // specified in `inputEnv`. The subprocess reads the environment variables + // which are in the keys of `expectedEnv` and writes them to stdout as JSON. + // It is then verified that these match with the values of `expectedEnv`. + const checkChildEnv = async (inputEnv, expectedEnv): Promise => { + const src = ` + console.log( + ${JSON.stringify(Object.keys(expectedEnv))}.map(k => Deno.env(k)) + )`; + const proc = Deno.run({ + args: [Deno.execPath(), "eval", src], + env: inputEnv, + stdout: "piped" + }); + const status = await proc.status(); + assertEquals(status.success, true); + const expectedValues = Object.values(expectedEnv); + const actualValues = JSON.parse( + new TextDecoder().decode(await proc.output()) + ); + assertEquals(actualValues, expectedValues); + }; + + assertEquals(Deno.env("path"), Deno.env("PATH")); + assertEquals(Deno.env("Path"), Deno.env("PATH")); + + // Check 'foo', 'Foo' and 'Foo' are case folded. + await checkChildEnv({ foo: "X" }, { foo: "X", Foo: "X", FOO: "X" }); + + // Check that 'µ' and 'Μ' are not case folded. + const lc1 = "µ"; + const uc1 = lc1.toUpperCase(); + assertNotEquals(lc1, uc1); + await checkChildEnv( + { [lc1]: "mu", [uc1]: "MU" }, + { [lc1]: "mu", [uc1]: "MU" } + ); + + // Check that 'dž' and 'DŽ' are folded, but 'Dž' is preserved. + const c2 = "Dž"; + const lc2 = c2.toLowerCase(); + const uc2 = c2.toUpperCase(); + assertNotEquals(c2, lc2); + assertNotEquals(c2, uc2); + await checkChildEnv( + { [c2]: "Dz", [lc2]: "dz" }, + { [c2]: "Dz", [lc2]: "dz", [uc2]: "dz" } + ); + await checkChildEnv( + { [c2]: "Dz", [uc2]: "DZ" }, + { [c2]: "Dz", [uc2]: "DZ", [lc2]: "DZ" } + ); + }); +} + test(function osPid(): void { console.log("pid", Deno.pid); assert(Deno.pid > 0);