Skip to content

Commit cff6e28

Browse files
bp7968hbartlomieju
andauthored
feat(cli): support multiple env file argument (#26527)
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 <[email protected]>
1 parent 73411bb commit cff6e28

18 files changed

+93
-44
lines changed

cli/args/flags.rs

+37-11
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ pub struct Flags {
613613
pub internal: InternalFlags,
614614
pub ignore: Vec<String>,
615615
pub import_map_path: Option<String>,
616-
pub env_file: Option<String>,
616+
pub env_file: Option<Vec<String>>,
617617
pub inspect_brk: Option<SocketAddr>,
618618
pub inspect_wait: Option<SocketAddr>,
619619
pub inspect: Option<SocketAddr>,
@@ -3775,12 +3775,14 @@ fn env_file_arg() -> Arg {
37753775
.help(cstr!(
37763776
"Load environment variables from local file
37773777
<p(245)>Only the first environment variable with a given key is used.
3778-
Existing process environment variables are not overwritten.</>"
3778+
Existing process environment variables are not overwritten, so if variables with the same names already exist in the environment, their values will be preserved.
3779+
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.</>"
37793780
))
37803781
.value_hint(ValueHint::FilePath)
37813782
.default_missing_value(".env")
37823783
.require_equals(true)
37833784
.num_args(0..=1)
3785+
.action(ArgAction::Append)
37843786
}
37853787

37863788
fn reload_arg() -> Arg {
@@ -5487,7 +5489,9 @@ fn import_map_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) {
54875489
}
54885490

54895491
fn env_file_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) {
5490-
flags.env_file = matches.remove_one::<String>("env-file");
5492+
flags.env_file = matches
5493+
.get_many::<String>("env-file")
5494+
.map(|values| values.cloned().collect());
54915495
}
54925496

54935497
fn reload_arg_parse(
@@ -7423,7 +7427,7 @@ mod tests {
74237427
allow_all: true,
74247428
..Default::default()
74257429
},
7426-
env_file: Some(".example.env".to_owned()),
7430+
env_file: Some(vec![".example.env".to_owned()]),
74277431
..Flags::default()
74287432
}
74297433
);
@@ -7517,7 +7521,7 @@ mod tests {
75177521
allow_all: true,
75187522
..Default::default()
75197523
},
7520-
env_file: Some(".example.env".to_owned()),
7524+
env_file: Some(vec![".example.env".to_owned()]),
75217525
unsafely_ignore_certificate_errors: Some(vec![]),
75227526
..Flags::default()
75237527
}
@@ -8165,7 +8169,7 @@ mod tests {
81658169
subcommand: DenoSubcommand::Run(RunFlags::new_default(
81668170
"script.ts".to_string(),
81678171
)),
8168-
env_file: Some(".env".to_owned()),
8172+
env_file: Some(vec![".env".to_owned()]),
81698173
code_cache_enabled: true,
81708174
..Flags::default()
81718175
}
@@ -8181,7 +8185,7 @@ mod tests {
81818185
subcommand: DenoSubcommand::Run(RunFlags::new_default(
81828186
"script.ts".to_string(),
81838187
)),
8184-
env_file: Some(".env".to_owned()),
8188+
env_file: Some(vec![".env".to_owned()]),
81858189
code_cache_enabled: true,
81868190
..Flags::default()
81878191
}
@@ -8214,7 +8218,7 @@ mod tests {
82148218
subcommand: DenoSubcommand::Run(RunFlags::new_default(
82158219
"script.ts".to_string(),
82168220
)),
8217-
env_file: Some(".another_env".to_owned()),
8221+
env_file: Some(vec![".another_env".to_owned()]),
82188222
code_cache_enabled: true,
82198223
..Flags::default()
82208224
}
@@ -8235,7 +8239,29 @@ mod tests {
82358239
subcommand: DenoSubcommand::Run(RunFlags::new_default(
82368240
"script.ts".to_string(),
82378241
)),
8238-
env_file: Some(".another_env".to_owned()),
8242+
env_file: Some(vec![".another_env".to_owned()]),
8243+
code_cache_enabled: true,
8244+
..Flags::default()
8245+
}
8246+
);
8247+
}
8248+
8249+
#[test]
8250+
fn run_multiple_env_file_defined() {
8251+
let r = flags_from_vec(svec![
8252+
"deno",
8253+
"run",
8254+
"--env-file",
8255+
"--env-file=.two_env",
8256+
"script.ts"
8257+
]);
8258+
assert_eq!(
8259+
r.unwrap(),
8260+
Flags {
8261+
subcommand: DenoSubcommand::Run(RunFlags::new_default(
8262+
"script.ts".to_string(),
8263+
)),
8264+
env_file: Some(vec![".env".to_owned(), ".two_env".to_owned()]),
82398265
code_cache_enabled: true,
82408266
..Flags::default()
82418267
}
@@ -8378,7 +8404,7 @@ mod tests {
83788404
allow_read: Some(vec![]),
83798405
..Default::default()
83808406
},
8381-
env_file: Some(".example.env".to_owned()),
8407+
env_file: Some(vec![".example.env".to_owned()]),
83828408
..Flags::default()
83838409
}
83848410
);
@@ -10053,7 +10079,7 @@ mod tests {
1005310079
unsafely_ignore_certificate_errors: Some(vec![]),
1005410080
v8_flags: svec!["--help", "--random-seed=1"],
1005510081
seed: Some(1),
10056-
env_file: Some(".example.env".to_owned()),
10082+
env_file: Some(vec![".example.env".to_owned()]),
1005710083
..Flags::default()
1005810084
}
1005910085
);

cli/args/mod.rs

+10-7
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ impl CliOptions {
11281128
self.flags.otel_config()
11291129
}
11301130

1131-
pub fn env_file_name(&self) -> Option<&String> {
1131+
pub fn env_file_name(&self) -> Option<&Vec<String>> {
11321132
self.flags.env_file.as_ref()
11331133
}
11341134

@@ -1935,19 +1935,22 @@ pub fn config_to_deno_graph_workspace_member(
19351935
})
19361936
}
19371937

1938-
fn load_env_variables_from_env_file(filename: Option<&String>) {
1939-
let Some(env_file_name) = filename else {
1938+
fn load_env_variables_from_env_file(filename: Option<&Vec<String>>) {
1939+
let Some(env_file_names) = filename else {
19401940
return;
19411941
};
1942-
match from_filename(env_file_name) {
1943-
Ok(_) => (),
1944-
Err(error) => {
1945-
match error {
1942+
1943+
for env_file_name in env_file_names.iter().rev() {
1944+
match from_filename(env_file_name) {
1945+
Ok(_) => (),
1946+
Err(error) => {
1947+
match error {
19461948
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),
19471949
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),
19481950
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),
19491951
_ => log::info!("{} Unknown failure occurred with the specified environment file: {}", colors::yellow("Warning"), env_file_name),
19501952
}
1953+
}
19511954
}
19521955
}
19531956
}

cli/standalone/binary.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -659,9 +659,15 @@ impl<'a> DenoCompileBinaryWriter<'a> {
659659
remote_modules_store.add_redirects(&graph.redirects);
660660

661661
let env_vars_from_env_file = match cli_options.env_file_name() {
662-
Some(env_filename) => {
663-
log::info!("{} Environment variables from the file \"{}\" were embedded in the generated executable file", crate::colors::yellow("Warning"), env_filename);
664-
get_file_env_vars(env_filename.to_string())?
662+
Some(env_filenames) => {
663+
let mut aggregated_env_vars = IndexMap::new();
664+
for env_filename in env_filenames.iter().rev() {
665+
log::info!("{} Environment variables from the file \"{}\" were embedded in the generated executable file", crate::colors::yellow("Warning"), env_filename);
666+
667+
let env_vars = get_file_env_vars(env_filename.to_string())?;
668+
aggregated_env_vars.extend(env_vars);
669+
}
670+
aggregated_env_vars
665671
}
666672
None => Default::default(),
667673
};

tests/integration/run_tests.rs

-10
Original file line numberDiff line numberDiff line change
@@ -418,16 +418,6 @@ fn permissions_cache() {
418418
});
419419
}
420420

421-
itest!(env_file {
422-
args: "run --env=env --allow-env run/env_file.ts",
423-
output: "run/env_file.out",
424-
});
425-
426-
itest!(env_file_missing {
427-
args: "run --env=missing --allow-env run/env_file.ts",
428-
output: "run/env_file_missing.out",
429-
});
430-
431421
itest!(lock_write_fetch {
432422
args:
433423
"run --quiet --allow-import --allow-read --allow-write --allow-env --allow-run run/lock_write_fetch/main.ts",
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"tests": {
3+
"basic": {
4+
"args": "run --env=./env --allow-env env_file.ts",
5+
"output": "env_file.out"
6+
},
7+
"missing": {
8+
"args": "run --env=./missing --allow-env env_file.ts",
9+
"output": "env_file_missing.out"
10+
},
11+
"multiple": {
12+
"args": "run --env=./env --env=./env_one --env=./env_two --allow-env env_file.ts",
13+
"output": "multiple_env_file.out"
14+
},
15+
"unparseable": {
16+
"args": "run --env=./env_unparseable --allow-env env_file.ts",
17+
"output": "env_unparseable.out"
18+
}
19+
}
20+
}

tests/specs/run/env_file/env

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FOO=BAR
2+
ANOTHER_FOO=ANOTHER_${FOO}
3+
MULTILINE="First Line
4+
Second Line"
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Warning The `--env-file` flag was used, but the environment file specified 'missing' was not found.
1+
Warning The `--env-file` flag was used, but the environment file specified './missing' was not found.
22
undefined
33
undefined
44
undefined

tests/specs/run/env_file/env_one

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FOO=BARBAR
2+
ANOTHER_FOO=OVERRIDEN_BY_ENV_ONE

tests/specs/run/env_file/env_two

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FOO=OVERRIDEN_BY_ENV_TWO
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Warning Parsing failed within the specified environment file: ./env_unparseable at index: 3 of the value: c:\path
2+
valid
3+
undefined
4+
undefined
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
OVERRIDEN_BY_ENV_TWO
2+
OVERRIDEN_BY_ENV_ONE
3+
First Line
4+
Second Line

tests/specs/run/env_unparsable_file/__test__.jsonc

-4
This file was deleted.

tests/specs/run/env_unparsable_file/main.js

-3
This file was deleted.

tests/specs/run/env_unparsable_file/main.out

-4
This file was deleted.

tools/lint.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ async function ensureNoNewITests() {
219219
"pm_tests.rs": 0,
220220
"publish_tests.rs": 0,
221221
"repl_tests.rs": 0,
222-
"run_tests.rs": 20,
222+
"run_tests.rs": 18,
223223
"shared_library_tests.rs": 0,
224224
"task_tests.rs": 2,
225225
"test_tests.rs": 0,

0 commit comments

Comments
 (0)