From cff6e280c77afb0bc42a10348eeef5360db8f361 Mon Sep 17 00:00:00 2001 From: Bhuwan Pandit Date: Sun, 17 Nov 2024 22:49:35 +0000 Subject: [PATCH] feat(cli): support multiple env file argument (#26527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #26425 ## Overview This PR adds support for specifying multiple environment files as arguments when using the Deno CLI. Subsequent files override pre-existing variables defined in previous files. If the same variable is defined in the environment and in the file, the value from the environment takes precedence. ## Example Usage ```bash deno run --allow-env --env-file --env-file=".env.one" --env-file=".env.two" script.ts ``` --------- Co-authored-by: Bartek IwaƄczuk --- cli/args/flags.rs | 48 ++++++++++++++----- cli/args/mod.rs | 17 ++++--- cli/standalone/binary.rs | 12 +++-- tests/integration/run_tests.rs | 10 ---- tests/specs/run/env_file/__test__.jsonc | 20 ++++++++ tests/specs/run/env_file/env | 4 ++ .../run => specs/run/env_file}/env_file.out | 0 .../run => specs/run/env_file}/env_file.ts | 0 .../run/env_file}/env_file_missing.out | 2 +- tests/specs/run/env_file/env_one | 2 + tests/specs/run/env_file/env_two | 1 + .../run/env_file/env_unparseable} | 0 tests/specs/run/env_file/env_unparseable.out | 4 ++ .../specs/run/env_file/multiple_env_file.out | 4 ++ .../run/env_unparsable_file/__test__.jsonc | 4 -- tests/specs/run/env_unparsable_file/main.js | 3 -- tests/specs/run/env_unparsable_file/main.out | 4 -- tools/lint.js | 2 +- 18 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 tests/specs/run/env_file/__test__.jsonc create mode 100644 tests/specs/run/env_file/env rename tests/{testdata/run => specs/run/env_file}/env_file.out (100%) rename tests/{testdata/run => specs/run/env_file}/env_file.ts (100%) rename tests/{testdata/run => specs/run/env_file}/env_file_missing.out (71%) create mode 100644 tests/specs/run/env_file/env_one create mode 100644 tests/specs/run/env_file/env_two rename tests/{testdata/env_unparsable => specs/run/env_file/env_unparseable} (100%) create mode 100644 tests/specs/run/env_file/env_unparseable.out create mode 100644 tests/specs/run/env_file/multiple_env_file.out delete mode 100644 tests/specs/run/env_unparsable_file/__test__.jsonc delete mode 100644 tests/specs/run/env_unparsable_file/main.js delete mode 100644 tests/specs/run/env_unparsable_file/main.out diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 720d8db3b0..bd6b30e417 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -613,7 +613,7 @@ pub struct Flags { pub internal: InternalFlags, pub ignore: Vec, pub import_map_path: Option, - pub env_file: Option, + pub env_file: Option>, pub inspect_brk: Option, pub inspect_wait: Option, pub inspect: Option, @@ -3775,12 +3775,14 @@ fn env_file_arg() -> Arg { .help(cstr!( "Load environment variables from local file Only the first environment variable with a given key is used. - Existing process environment variables are not overwritten." + Existing process environment variables are not overwritten, so if variables with the same names already exist in the environment, their values will be preserved. + Where multiple declarations for the same environment variable exist in your .env file, the first one encountered is applied. This is determined by the order of the files you pass as arguments." )) .value_hint(ValueHint::FilePath) .default_missing_value(".env") .require_equals(true) .num_args(0..=1) + .action(ArgAction::Append) } fn reload_arg() -> Arg { @@ -5487,7 +5489,9 @@ fn import_map_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) { } fn env_file_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) { - flags.env_file = matches.remove_one::("env-file"); + flags.env_file = matches + .get_many::("env-file") + .map(|values| values.cloned().collect()); } fn reload_arg_parse( @@ -7423,7 +7427,7 @@ mod tests { allow_all: true, ..Default::default() }, - env_file: Some(".example.env".to_owned()), + env_file: Some(vec![".example.env".to_owned()]), ..Flags::default() } ); @@ -7517,7 +7521,7 @@ mod tests { allow_all: true, ..Default::default() }, - env_file: Some(".example.env".to_owned()), + env_file: Some(vec![".example.env".to_owned()]), unsafely_ignore_certificate_errors: Some(vec![]), ..Flags::default() } @@ -8165,7 +8169,7 @@ mod tests { subcommand: DenoSubcommand::Run(RunFlags::new_default( "script.ts".to_string(), )), - env_file: Some(".env".to_owned()), + env_file: Some(vec![".env".to_owned()]), code_cache_enabled: true, ..Flags::default() } @@ -8181,7 +8185,7 @@ mod tests { subcommand: DenoSubcommand::Run(RunFlags::new_default( "script.ts".to_string(), )), - env_file: Some(".env".to_owned()), + env_file: Some(vec![".env".to_owned()]), code_cache_enabled: true, ..Flags::default() } @@ -8214,7 +8218,7 @@ mod tests { subcommand: DenoSubcommand::Run(RunFlags::new_default( "script.ts".to_string(), )), - env_file: Some(".another_env".to_owned()), + env_file: Some(vec![".another_env".to_owned()]), code_cache_enabled: true, ..Flags::default() } @@ -8235,7 +8239,29 @@ mod tests { subcommand: DenoSubcommand::Run(RunFlags::new_default( "script.ts".to_string(), )), - env_file: Some(".another_env".to_owned()), + env_file: Some(vec![".another_env".to_owned()]), + code_cache_enabled: true, + ..Flags::default() + } + ); + } + + #[test] + fn run_multiple_env_file_defined() { + let r = flags_from_vec(svec![ + "deno", + "run", + "--env-file", + "--env-file=.two_env", + "script.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags::new_default( + "script.ts".to_string(), + )), + env_file: Some(vec![".env".to_owned(), ".two_env".to_owned()]), code_cache_enabled: true, ..Flags::default() } @@ -8378,7 +8404,7 @@ mod tests { allow_read: Some(vec![]), ..Default::default() }, - env_file: Some(".example.env".to_owned()), + env_file: Some(vec![".example.env".to_owned()]), ..Flags::default() } ); @@ -10053,7 +10079,7 @@ mod tests { unsafely_ignore_certificate_errors: Some(vec![]), v8_flags: svec!["--help", "--random-seed=1"], seed: Some(1), - env_file: Some(".example.env".to_owned()), + env_file: Some(vec![".example.env".to_owned()]), ..Flags::default() } ); diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 318a6ca76b..ec75d7a100 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -1128,7 +1128,7 @@ impl CliOptions { self.flags.otel_config() } - pub fn env_file_name(&self) -> Option<&String> { + pub fn env_file_name(&self) -> Option<&Vec> { self.flags.env_file.as_ref() } @@ -1935,19 +1935,22 @@ pub fn config_to_deno_graph_workspace_member( }) } -fn load_env_variables_from_env_file(filename: Option<&String>) { - let Some(env_file_name) = filename else { +fn load_env_variables_from_env_file(filename: Option<&Vec>) { + let Some(env_file_names) = filename else { return; }; - match from_filename(env_file_name) { - Ok(_) => (), - Err(error) => { - match error { + + for env_file_name in env_file_names.iter().rev() { + match from_filename(env_file_name) { + Ok(_) => (), + Err(error) => { + match error { dotenvy::Error::LineParse(line, index)=> log::info!("{} Parsing failed within the specified environment file: {} at index: {} of the value: {}",colors::yellow("Warning"), env_file_name, index, line), dotenvy::Error::Io(_)=> log::info!("{} The `--env-file` flag was used, but the environment file specified '{}' was not found.",colors::yellow("Warning"),env_file_name), dotenvy::Error::EnvVar(_)=> log::info!("{} One or more of the environment variables isn't present or not unicode within the specified environment file: {}",colors::yellow("Warning"),env_file_name), _ => log::info!("{} Unknown failure occurred with the specified environment file: {}", colors::yellow("Warning"), env_file_name), } + } } } } diff --git a/cli/standalone/binary.rs b/cli/standalone/binary.rs index ebcbf3ee62..3efd8ee141 100644 --- a/cli/standalone/binary.rs +++ b/cli/standalone/binary.rs @@ -659,9 +659,15 @@ impl<'a> DenoCompileBinaryWriter<'a> { remote_modules_store.add_redirects(&graph.redirects); let env_vars_from_env_file = match cli_options.env_file_name() { - Some(env_filename) => { - log::info!("{} Environment variables from the file \"{}\" were embedded in the generated executable file", crate::colors::yellow("Warning"), env_filename); - get_file_env_vars(env_filename.to_string())? + Some(env_filenames) => { + let mut aggregated_env_vars = IndexMap::new(); + for env_filename in env_filenames.iter().rev() { + log::info!("{} Environment variables from the file \"{}\" were embedded in the generated executable file", crate::colors::yellow("Warning"), env_filename); + + let env_vars = get_file_env_vars(env_filename.to_string())?; + aggregated_env_vars.extend(env_vars); + } + aggregated_env_vars } None => Default::default(), }; diff --git a/tests/integration/run_tests.rs b/tests/integration/run_tests.rs index 549b88bac4..c97b700c5d 100644 --- a/tests/integration/run_tests.rs +++ b/tests/integration/run_tests.rs @@ -418,16 +418,6 @@ fn permissions_cache() { }); } -itest!(env_file { - args: "run --env=env --allow-env run/env_file.ts", - output: "run/env_file.out", -}); - -itest!(env_file_missing { - args: "run --env=missing --allow-env run/env_file.ts", - output: "run/env_file_missing.out", -}); - itest!(lock_write_fetch { args: "run --quiet --allow-import --allow-read --allow-write --allow-env --allow-run run/lock_write_fetch/main.ts", diff --git a/tests/specs/run/env_file/__test__.jsonc b/tests/specs/run/env_file/__test__.jsonc new file mode 100644 index 0000000000..6420621690 --- /dev/null +++ b/tests/specs/run/env_file/__test__.jsonc @@ -0,0 +1,20 @@ +{ + "tests": { + "basic": { + "args": "run --env=./env --allow-env env_file.ts", + "output": "env_file.out" + }, + "missing": { + "args": "run --env=./missing --allow-env env_file.ts", + "output": "env_file_missing.out" + }, + "multiple": { + "args": "run --env=./env --env=./env_one --env=./env_two --allow-env env_file.ts", + "output": "multiple_env_file.out" + }, + "unparseable": { + "args": "run --env=./env_unparseable --allow-env env_file.ts", + "output": "env_unparseable.out" + } + } +} diff --git a/tests/specs/run/env_file/env b/tests/specs/run/env_file/env new file mode 100644 index 0000000000..c41732d30b --- /dev/null +++ b/tests/specs/run/env_file/env @@ -0,0 +1,4 @@ +FOO=BAR +ANOTHER_FOO=ANOTHER_${FOO} +MULTILINE="First Line +Second Line" \ No newline at end of file diff --git a/tests/testdata/run/env_file.out b/tests/specs/run/env_file/env_file.out similarity index 100% rename from tests/testdata/run/env_file.out rename to tests/specs/run/env_file/env_file.out diff --git a/tests/testdata/run/env_file.ts b/tests/specs/run/env_file/env_file.ts similarity index 100% rename from tests/testdata/run/env_file.ts rename to tests/specs/run/env_file/env_file.ts diff --git a/tests/testdata/run/env_file_missing.out b/tests/specs/run/env_file/env_file_missing.out similarity index 71% rename from tests/testdata/run/env_file_missing.out rename to tests/specs/run/env_file/env_file_missing.out index f50c1789ee..34b2bf810e 100644 --- a/tests/testdata/run/env_file_missing.out +++ b/tests/specs/run/env_file/env_file_missing.out @@ -1,4 +1,4 @@ -Warning The `--env-file` flag was used, but the environment file specified 'missing' was not found. +Warning The `--env-file` flag was used, but the environment file specified './missing' was not found. undefined undefined undefined diff --git a/tests/specs/run/env_file/env_one b/tests/specs/run/env_file/env_one new file mode 100644 index 0000000000..c26038a677 --- /dev/null +++ b/tests/specs/run/env_file/env_one @@ -0,0 +1,2 @@ +FOO=BARBAR +ANOTHER_FOO=OVERRIDEN_BY_ENV_ONE diff --git a/tests/specs/run/env_file/env_two b/tests/specs/run/env_file/env_two new file mode 100644 index 0000000000..fe8392c3a5 --- /dev/null +++ b/tests/specs/run/env_file/env_two @@ -0,0 +1 @@ +FOO=OVERRIDEN_BY_ENV_TWO diff --git a/tests/testdata/env_unparsable b/tests/specs/run/env_file/env_unparseable similarity index 100% rename from tests/testdata/env_unparsable rename to tests/specs/run/env_file/env_unparseable diff --git a/tests/specs/run/env_file/env_unparseable.out b/tests/specs/run/env_file/env_unparseable.out new file mode 100644 index 0000000000..0a88d164e3 --- /dev/null +++ b/tests/specs/run/env_file/env_unparseable.out @@ -0,0 +1,4 @@ +Warning Parsing failed within the specified environment file: ./env_unparseable at index: 3 of the value: c:\path +valid +undefined +undefined diff --git a/tests/specs/run/env_file/multiple_env_file.out b/tests/specs/run/env_file/multiple_env_file.out new file mode 100644 index 0000000000..3fa97d5994 --- /dev/null +++ b/tests/specs/run/env_file/multiple_env_file.out @@ -0,0 +1,4 @@ +OVERRIDEN_BY_ENV_TWO +OVERRIDEN_BY_ENV_ONE +First Line +Second Line diff --git a/tests/specs/run/env_unparsable_file/__test__.jsonc b/tests/specs/run/env_unparsable_file/__test__.jsonc deleted file mode 100644 index bed1506356..0000000000 --- a/tests/specs/run/env_unparsable_file/__test__.jsonc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "args": "run --env=../../../testdata/env_unparsable --allow-env main.js", - "output": "main.out" -} diff --git a/tests/specs/run/env_unparsable_file/main.js b/tests/specs/run/env_unparsable_file/main.js deleted file mode 100644 index 48488ce721..0000000000 --- a/tests/specs/run/env_unparsable_file/main.js +++ /dev/null @@ -1,3 +0,0 @@ -console.log(Deno.env.get("FOO")); -console.log(Deno.env.get("ANOTHER_FOO")); -console.log(Deno.env.get("MULTILINE")); diff --git a/tests/specs/run/env_unparsable_file/main.out b/tests/specs/run/env_unparsable_file/main.out deleted file mode 100644 index a19ff4dd6c..0000000000 --- a/tests/specs/run/env_unparsable_file/main.out +++ /dev/null @@ -1,4 +0,0 @@ -Warning Parsing failed within the specified environment file: ../../../testdata/env_unparsable at index: 3 of the value: c:\path -valid -undefined -undefined diff --git a/tools/lint.js b/tools/lint.js index 21064a05bf..604dee9b30 100755 --- a/tools/lint.js +++ b/tools/lint.js @@ -219,7 +219,7 @@ async function ensureNoNewITests() { "pm_tests.rs": 0, "publish_tests.rs": 0, "repl_tests.rs": 0, - "run_tests.rs": 20, + "run_tests.rs": 18, "shared_library_tests.rs": 0, "task_tests.rs": 2, "test_tests.rs": 0,