Skip to content

Commit

Permalink
feat(cli): support multiple env file argument (denoland#26527)
Browse files Browse the repository at this point in the history
Closes denoland#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 <[email protected]>
  • Loading branch information
bp7968h and bartlomieju authored Nov 17, 2024
1 parent 73411bb commit cff6e28
Show file tree
Hide file tree
Showing 18 changed files with 93 additions and 44 deletions.
48 changes: 37 additions & 11 deletions cli/args/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ pub struct Flags {
pub internal: InternalFlags,
pub ignore: Vec<String>,
pub import_map_path: Option<String>,
pub env_file: Option<String>,
pub env_file: Option<Vec<String>>,
pub inspect_brk: Option<SocketAddr>,
pub inspect_wait: Option<SocketAddr>,
pub inspect: Option<SocketAddr>,
Expand Down Expand Up @@ -3775,12 +3775,14 @@ fn env_file_arg() -> Arg {
.help(cstr!(
"Load environment variables from local file
<p(245)>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 {
Expand Down Expand Up @@ -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::<String>("env-file");
flags.env_file = matches
.get_many::<String>("env-file")
.map(|values| values.cloned().collect());
}

fn reload_arg_parse(
Expand Down Expand Up @@ -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()
}
);
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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()
}
Expand All @@ -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()
}
Expand Down Expand Up @@ -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()
}
Expand All @@ -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()
}
Expand Down Expand Up @@ -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()
}
);
Expand Down Expand Up @@ -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()
}
);
Expand Down
17 changes: 10 additions & 7 deletions cli/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>> {
self.flags.env_file.as_ref()
}

Expand Down Expand Up @@ -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<String>>) {
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),
}
}
}
}
}
Expand Down
12 changes: 9 additions & 3 deletions cli/standalone/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand Down
10 changes: 0 additions & 10 deletions tests/integration/run_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions tests/specs/run/env_file/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
4 changes: 4 additions & 0 deletions tests/specs/run/env_file/env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FOO=BAR
ANOTHER_FOO=ANOTHER_${FOO}
MULTILINE="First Line
Second Line"
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions tests/specs/run/env_file/env_one
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FOO=BARBAR
ANOTHER_FOO=OVERRIDEN_BY_ENV_ONE
1 change: 1 addition & 0 deletions tests/specs/run/env_file/env_two
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO=OVERRIDEN_BY_ENV_TWO
File renamed without changes.
4 changes: 4 additions & 0 deletions tests/specs/run/env_file/env_unparseable.out
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions tests/specs/run/env_file/multiple_env_file.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
OVERRIDEN_BY_ENV_TWO
OVERRIDEN_BY_ENV_ONE
First Line
Second Line
4 changes: 0 additions & 4 deletions tests/specs/run/env_unparsable_file/__test__.jsonc

This file was deleted.

3 changes: 0 additions & 3 deletions tests/specs/run/env_unparsable_file/main.js

This file was deleted.

4 changes: 0 additions & 4 deletions tests/specs/run/env_unparsable_file/main.out

This file was deleted.

2 changes: 1 addition & 1 deletion tools/lint.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit cff6e28

Please sign in to comment.